From 1a57c48318626c48e506838131f2d74d548709c8 Mon Sep 17 00:00:00 2001 From: Matiq Date: Tue, 18 Aug 2026 21:11:29 +0300 Subject: [PATCH] runtime capture pt2: vst3 loads AT ImageBase 0x180000000; 0x540658 = field [ctx+0x540658] not RVA; beacon->ctx script (racy) --- handoff/NOTES_LEVEL.md | 28 ++++++++ rtcapture.py | 146 ++++++++++++++++++++++++++++++++++++++ rtcapture2.py | 129 +++++++++++++++++++++++++++++++++ rtdeep2.py | 157 +++++++++++++++++++++++++---------------- 4 files changed, 398 insertions(+), 62 deletions(-) create mode 100644 rtcapture.py create mode 100644 rtcapture2.py diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index bcffc99..5e4c032 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -349,3 +349,31 @@ getFunctionContaining(0x52ac64) and has the complete per-band loop + FFT-conv). ## опять +1.4 dB). ## => мультибанд: сумма displacements -> sat-кривая. Каждая полоса отдельно калибруется single- ## band рендерами; комбинация закрывается runtime или sat-подгонкой на b1on12_b2on12. + + +## ============ 2026-08-18h2: RUNTIME CAPTURE PARTIAL (base 0x180000000, 0x540658 = FIELD NOT RVA) ============ + +### Runtime capture infra (rtcapture2.py, WORKS): +- Launch: `reaper -nosplash -renderproject X.rpp`; host pid found by scanning /proc/*/maps for 'soothe2' + (lowest map addr with soothe2 in path = PE base). **vst3 loads AT ImageBase 0x180000000** (no reloc). +- Reading /proc/pid/mem (+0x180000000) works DURING render; process dies right after render finishes. +- Section table (Authoritative): .text rva 0x1000 raw 0x600 vsize 0x1a52000; IPPCODE 0x1a53000/0x1a51e00; + .rdata 0x1baa000/0x1ba8400 vsize 0xa60000; .data 0x260a000/0x2608200 vsize 0x72000; .pdata 0x267c000. +- FILE->RVA mapping: raw off F maps to rva = F - raw_sec + va_sec (e.g. .text: F=0x53F658<->rva=0x541058). + +### CRITICAL: 0x540658 (and 0x540678/0x5407c8/0x5408b0/etc) are FIELD OFFSETS, NOT RVAs! +- Disasm at rva 0x52b771: `mov rax,[rdi+0x540658]` -> window read is [ctx+0x540658] (ctx = DSP object). +- Static soothe_mem.bin is RVA-linear; reading at 0x540658 gives .text CODE bytes (garbage floats). +- So the "window" (FIR*=WINDOW in FFT-conv step 5) lives in the HEAP DSP object at offset 0x540658, + NOT in module image. Runtime must locate `ctx` = base of DSP object. +- Beacon idea: phase_table_1024 (twiddle doubles 0x182615608 in .data) is the FFT-plan table; + scan host heap for u64==0x182615608, then the pointer owner is plan (+0x18/+0x20/+0x68 fields) + -> ctx = byte_addr_of_(pointer_field) - field_offset; window at ctx+0x540658. + (rtdeep2.py implements this; currently racy - host may exit before numpy import + first scan.) + +### Notes for next session: +- Render lifetime is short (~2-19s); to win the race: import numpy BEFORE Popen, scan /proc instantly, + and re-run if host missed. Could also pre-load module into gdb for direct ctx inspection. +- Alternative: find ctx via 'consumers_out' alloc chain (note :119-135) if heap layout known. +- Verified render outputs: out_dual300.wav etc; run_sweep works; pkill -9 -x reaper hangs shell - + use `pkill -9 -f "reap[e]r"` style to avoid killing own bash. diff --git a/rtcapture.py b/rtcapture.py new file mode 100644 index 0000000..aaae651 --- /dev/null +++ b/rtcapture.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""rtcapture.py — runtime-захват soothe2 из yabridge-host во время offline-рендера. + +Запускает reaper -renderproject , находит yabridge-host.exe.so с soothe2_x64.vst3, +вычисляет runtime-базу модуля (по MZ/PE) и непрерывно сэмплирует целевую RAM плагина +(RVA = VA - 0x180000000). Печатает последние стабильные значения по окончании рендера. + +Цели (RVA от базы модуля): + 0x540658 окно/FIR (размер окна из plan) [искомое 0.540658] + 0x540678 per-bin уровень-трекеры + 0x5407c8 аккумулятор маски (C) + 0x540698 freq-axis значение + 0x540534 N/2 (FFT size) + 0x5408b0 узлы кривой 0x188 (20 doubles) + 0x540888 0x540880 ctor-константы + 0x540530..0x5405a0 план-таблицы FFT +""" +import os, sys, glob, time, struct, subprocess + +RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/render_long.rpp' +BASE = 0x180000000 +TARGETS = { + 'window_0x540658': (0x540658, 0x1000), + 'levels_0x540678': (0x540678, 0x400), + 'acc_0x5407c8': (0x5407c8, 0x40), + 'freqaxis_0x540698': (0x540698, 0x20), + 'n2_0x540534': (0x540534, 0x20), + 'curve_0x5408b0': (0x5408b0, 0x200), + 'ctor_0x540870': (0x540870, 0x60), + 'plan_0x540530': (0x540530, 0x80), +} + + +def find_base(pid): + maps = open(f'/proc/{pid}/maps').read() + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + seen = set() + for line in maps.splitlines(): + p = line.split() + lo = int(p[0].split('-')[0], 16) + if lo in seen: + continue + seen.add(lo) + try: + hdr = os.pread(mem, 0x1000, lo) + except Exception: + continue + if hdr[:2] != b'MZ': + continue + try: + x = struct.unpack(' 1e-9] + print(f'{name:20s} nonzero={len(nz)} first20={nz[:20]}') + elif name == 'acc_0x5407c8': + print(f'{name:20s} ints={[hex(v) for v in vals[:8]]}') + elif name == 'curve_0x5408b0': + dl = struct.unpack('<%dd' % (len(d) // 8), d[:len(d) // 8 * 8]) + print(f'{name:20s} doubles={[round(v, 6) for v in dl[:20]]}') + elif name == 'levels_0x540678': + fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4]) + print(f'{name:20s} floats={[round(v, 5) for v in fl[:40]]}') + elif name in ('ctor_0x540870',): + fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4]) + print(f'{name:20s} floats={[round(v, 5) for v in fl[:16]]}') + elif name in ('plan_0x540530',): + print(f'{name:20s} ints={[hex(v) for v in vals[:24]]}') + else: + print(f'{name:20s} ints={[hex(v) for v in vals[:8]]}') + print('done') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/rtcapture2.py b/rtcapture2.py new file mode 100644 index 0000000..6f7b737 --- /dev/null +++ b/rtcapture2.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""rtcapture2.py — robust runtime capture for a LONG render (host lives minutes). +Dumps sections + raw target buffers at module_base+rva (PE ImageBase=0x180000000). +""" +import subprocess, time, glob, os, struct, sys + +RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual300.rpp' +OUT = sys.argv[2] if len(sys.argv) > 2 else '/tmp/rtcap2.log' +RVAS = [0x541134, 0x541258, 0x541278, 0x5413c8, 0x541298, 0x541470, 0x541480, 0x541488, 0x5414b0] + + +def find_pe(pid): + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + seen = set() + for line in open(f'/proc/{pid}/maps').read().splitlines(): + lo = int(line.split()[0].split('-')[0], 16) + if lo in seen: + continue + seen.add(lo) + try: + hdr = os.pread(mem, 0x1000, lo) + except Exception: + continue + if hdr[:2] != b'MZ': + continue + try: + x = struct.unpack('9} vsize={hex(vs):>9} chars={hex(ch)}', flush=True) + mem = os.open(f'/proc/{host}/mem', os.O_RDONLY) + t3 = time.time() + last = 0 + while proc.poll() is None and time.time() - t3 < 60: + try: + d = os.pread(mem, 0x20, base + 0x541258) + win = struct.unpack('<8f', d[:32]) if len(d) >= 32 else None + except Exception: + win = None + now = time.time() - t3 + if now - last >= 2.0 or win is None: + print(f'sample t={now:.1f}s win[0:8]={[round(v,5) for v in win] if win else None}', flush=True) + for rva in RVAS: + d = os.pread(mem, 128, base + rva) + fl = struct.unpack('<%df' % (len(d) // 4), d[:len(d) // 4 * 4]) if len(d) >= 4 else [] + print(f' {hex(rva)} floats={[round(v,5) for v in fl[:4]]} raw={d[:24].hex()}', flush=True) + last = now + time.sleep(0.05) + os.close(mem) + print('render done', flush=True) + proc.wait(timeout=10) + print('done', flush=True) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/rtdeep2.py b/rtdeep2.py index 6c4a2db..635121d 100644 --- a/rtdeep2.py +++ b/rtdeep2.py @@ -1,76 +1,109 @@ #!/usr/bin/env python3 -import subprocess, os, glob, sys, time, struct +"""rtdeep2.py — find DSP ctx object via phase-table pointer beacon, then dump window 0x540658 & neighbors.""" +import subprocess, time, glob, os, struct, sys +import numpy as np -RPP = "/home/m/soothe-bt/render_long.rpp" -proc = subprocess.Popen(['reaper', '-nosplash', '-renderproject', RPP], - stdout=open('/tmp/rtdeep2.log', 'w'), stderr=subprocess.STDOUT) -print('reaper', proc.pid, flush=True) +RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual300.rpp' +BEACON = 0x182615608 # VA of phase_table_1024 (static .data) -def find_hosts(): +def vst_base(pid): + lo = None + for line in open(f'/proc/{pid}/maps').read().splitlines(): + if 'soothe2' not in line: + continue + a = int(line.split()[0].split('-')[0], 16) + lo = a if lo is None else min(lo, a) + return lo + + +def readable_regions(pid): out = [] - for p in glob.glob('/proc/[0-9]*'): - try: - pid = int(os.path.basename(p)) - m = open(f'/proc/{pid}/maps').read() - c = open(f'/proc/{pid}/cmdline', 'rb').read().decode('utf8', 'replace') - if 'soothe2' in m and 'yabridge-host.exe' in c: - out.append(pid) - except Exception: - pass + for line in open(f'/proc/{pid}/maps').read().splitlines(): + p = line.split()[0] + lo, hi = int(p.split('-')[0], 16), int(p.split('-')[1], 16) + if 'vst3' in line: + continue # skip module itself + out.append((lo, hi)) return out -def scan(pid): - # strategy: enumerate every readable chunk; build set of img addresses whose content is 0x18052xxxx - # then find heap words equal to any such address - mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) - img_sp = set() # vtbl-slot candidates: addrs in module image holding 0x18052xxxx - heap_addrs = [] # (va, chunk) heap chunks - maps = open(f'/proc/{pid}/maps').read() - for line in maps.splitlines(): - p = line.split() - lo, hi = (int(x, 16) for x in p[0].split('-')) - if 'r' not in p[1]: +def find_beacon(pid): + try: + mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY) + except Exception: + return [] + res = [] + for lo, hi in readable_regions(pid): + sz = (hi - lo) & ~7 + if sz <= 0 or sz > 0x200000000: continue try: - d = os.pread(mem, hi - lo, lo) + buf = os.pread(mem, sz, lo) except Exception: continue - if lo < 0x180000000 < hi or (0x180000000 <= lo < 0x183000000): - for off in range(0, len(d) - 7, 8): - q = struct.unpack_from('= -1 and fl.max() <= 2: + print(f' ctx={hex(ctx)} WIN@+0x540658 head={[round(float(x),4) for x in fl[:8]]} mid={[round(float(x),4) for x in fl[1024:1032]]}', flush=True) + blk = os.pread(mem, 0x8000, ctx + 0x540000) + open(f'/tmp/ctx_{ctx:x}_win.bin', 'wb').write(blk) + break + time.sleep(0.2) + os.close(mem) + try: + proc.wait(timeout=5) + except Exception: + proc.kill() + print('done', flush=True) + + +if __name__ == '__main__': + main() \ No newline at end of file