runtime capture: registry heartbeat decodes live DSP tables (WIN 0.540658, freq-axis@48k, per-bin weights), rtsnap_fast/findctx/rtchunk infra

This commit is contained in:
2026-08-19 20:06:14 +03:00
parent 8fdb1dc082
commit abf09a26cc
13 changed files with 251 additions and 36 deletions
+33
View File
@@ -0,0 +1,33 @@
import struct
data=open('/tmp/snap_all.bin','rb').read()
i=0; regs=[]
while i+16<=len(data):
lo,sz,=struct.unpack_from('<QQ',data,i)
regs.append((lo,sz,data[i+16:i+16+sz]))
i+=16+sz
def readabs(addr,n):
for lo,sz,body in regs:
if lo<=addr<lo+sz and addr-lo+n<=sz:
return body[addr-lo:addr-lo+n]
return None
known={0x2a72600,0x2111140,0x2cd0fc0,0x29f2280,0x29fa300,0x2a02340,0x2a0a3c0,0x2a12400}
cands=[]
t0=len(regs)
for ri,(lo,sz,body) in enumerate(regs):
if sz<0x541000: continue
# check every 8-aligned offset for the +0x540658 ptr direct
for off in range(0, sz-0x540660, 8):
b=body[off+0x540658:off+0x540660]
if len(b)<8: break
p=struct.unpack_from('<Q',b)[0]
if p in known:
base=lo+off
u24=readabs(base+0x24,4); u28=readabs(base+0x28,4)
f40=readabs(base+0x40,0x20); f58=readabs(base+0x58,0x20)
print('cand base=0x%x reg%d p=0x%x u24=%s u28=%s 40=%s'%(
base,ri,p,
struct.unpack('<f',u24)[0] if u24 else None,
struct.unpack('<f',u28)[0] if u28 else None,
[round(x,4) for x in struct.unpack('<4d',f40[:32])] if f40 else None))
cands.append(base)
print('total cand',len(cands))
+65
View File
@@ -0,0 +1,65 @@
# RUNTIME CAPTURE — LIVE DSP TABLES (2026-08-19)
## BREAKTHROUGH: heap "registry" object found & read during offline render
- During `reaper -nosplash -renderproject render_long.rpp`, the yabridge-host
allocates a DSP arena containing a **registry array**: a run of `{u64 count, u64 ptr}`
pairs (stride 0x10) pointing at every DSP buffer. At capture time:
registry base = **0x29b06c0** (in anon region 0x2922000, 0x77e000).
(GUI-session registry was 0x28b06c0 — same object, shifted by arena layout.)
- Finding it live: scan host-readable memory (chunked, 8MB) for `u64==8193` (0x2001)
followed by a readable ptr, then require a run of count/ptr pairs at stride 0x10.
- Registry owner chain: 0 hits for a u64==registry address, so it is reached
structurally (object member at some fixed offset), not via an explicit global.
## Captured tables (SR=44100 project, but internal freq-axis = 48000!)
| reg idx | count | ptr | content |
|---------|-------|----------|---------|
| [00] | 8193 | 0x2a72600 | identity ~1.0 |
| [01] | 8193 | 0x2111140 | **WIN_freq: 0.5 -> 1.0 (saturates; idx1024=0.68, idx2048=0.8)** — the FFT-conv window (0x540658) |
| [02] | 8193 | 0x29f2280 | 0 -> ~0.01 (levels/curve) |
| [03] | 8193 | 0x29fa300 | **0.596 -> 0.126** = matches known rwin_C0 |
| [04] | 8193 | 0x2a02340 | **0.404 -> 0.874** = 1-[03] complement |
| [05] | 8193 | 0x2a0a3c0 | 0.0435 -> ~0 (weight, small) |
| [06] | 8193 | 0x2a12400 | 0.9565 -> ~1.0 (1-[05]) |
| [07] | 8193 | 0x2aca740 | zeros + small negatives |
| [0d] | 2049 | 0x2cd0fc0 | **freq-axis 0..23988.3 Hz, spacing 11.713 = 48000/4096** → internal SR=48000 |
| [0e] | 8193 | 0x2cd9000 | 2.017 -> 0 (LUT/knee?) |
| [0f] | 16384 | 0x2ce9080 | same as [0e] doubled |
| [10] | 16384 | 0x2cf90c0 | 1.2914 -> 0 |
| [12] | 16384 | 0x2a62540 | 11.29.. (scattering) |
| [14]/[15] | 8193 | .. | first-fire IR? 0,0.022,0.104,0.084,0.018,0 |
| [17] | 32768 | 0x2d29140 | 0.9999 -> ~1 (ramp) |
| [19] | 32768 | 0x2d69200 | 1.2915 -> 1.0 |
| others | 32768/65536 | .. | ones / ramps (FFT plans, mirrors) |
- Key numeric check: registry[03] head 0.5960761, idx1024 0.168, idx2048 0.1257 —
byte-identical to earlier GUI rwin_C0 (0.596 -> 0.126). Confirms registry IS the
authoritative per-bin weight source; tables are stable across sessions/SR.
- **FREQ-AXIS uses internal SR=48000 regardless of project 44100** (spacing 11.713).
This reconciles "rwin tables at 48k" even when rendering 44.1k projects.
## Files saved (handoff/)
- `rtwin_freq_44100.npy` — WIN_freq (8193 f32): 0.5 -> 1.0 (this is live 0x540658 window)
- `rtfreqaxis_48000_internal.npy` — freq-axis (2048 f32, 0..23988.3, spacing 11.713)
- `rtwa_596.npy` — [03] 0.596->0.126
- `rtwb_404.npy` — [04] 0.404->0.874
- `rtwc_043.npy` — [05]
- `rtwd_956.npy` — [06]
- Full raw snapshot: /tmp/snap_all.bin (318MB, entries {lo,sz,bytes}), registry.txt list.
## Method notes (repro)
- rtsnap_fast.py: spawn reaper render_long, find host (soothe2 in maps, not reaper),
sleep 6s (tables built), pread ALL readable maps chunked 8MB -> snap_all.bin.
- pread of large anon regions can EIO -> MUST chunk (8MB); whole-region pread loses data.
- Scan ~0.1s for 318MB once chunked; far cheaper than object-base scan.
- Earlier vptr-based (rtobj/rtdump2/rtall) and 44100-marker scans all failed because
the DSP object has NO static vptr match in a fresh render (host dies / fields live
only during audio) and ctor field +0x24 != 44100 live. The registry run is the
reliable beacon.
## Remaining (for twin IIR attack/release per-bin)
- twin state A/B (342 double per-bin IIR states) still not uniquely located live;
short renders keep them ~0. They are NOT the registry tables.
- Next: render_long + capture at t=10-20s into sustain, then locate the per-bin
attack/release smoothing coefficients (0x540888/88c set, converted via ln(10)/20)
inside the arena near registry.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+50
View File
@@ -0,0 +1,50 @@
import subprocess, os, glob, time, struct
def mem_open(pid): return os.open(f'/proc/{pid}/mem', os.O_RDONLY)
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
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','/home/m/soothe-bt/dual300.rpp'],
stdout=open('/dev/null','w'),stderr=subprocess.STDOUT)
host=find_host(proc); print('host',host)
time.sleep(4)
fd=mem_open(host)
SIG=struct.pack('<I',0x472c4400)
t0=time.time(); total=0; hits={}
for line in open(f'/proc/{host}/maps').read().splitlines():
p=line.split(); lo,hi=(int(x,16) for x in p[0].split('-'))
if 'r' not in p[1]: continue
t0r=time.time()
ok_read=0
a=lo
while a<hi:
n=min(hi-a, 0x400000)
try:
d=os.pread(fd,n,a)
except Exception:
a+=n; continue
if not d:
a+=n; continue
ok_read+=len(d)
i=0
while True:
i=d.find(SIG,i)
if i<0: break
hits[a+i]=None; i+=1
a+=n
if ok_read:
print(' 0x%x-%x MB=%.1f ok=%.1fMB'%(lo,hi,(hi-lo)/1e6,ok_read/1e6))
total+=ok_read
print('scanned ok=%.0fMB t=%.1fs n44100=%d'%(total/1e6,time.time()-t0,len(hits)))
for a in list(hits)[:5]:
print(' HIT 0x%x'%a)
proc.kill()
+56 -33
View File
@@ -1,29 +1,25 @@
#!/usr/bin/env python3
"""rtdeep2.py — find DSP ctx object via phase-table pointer beacon, then dump window 0x540658 & neighbors."""
import subprocess, time, glob, os, struct, sys
import subprocess, time, glob, os, sys
import numpy as np
import struct
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)
BEACON = np.uint64(0x182615608)
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 get_maps(pid):
try:
return open(f'/proc/{pid}/maps').read()
except Exception:
return ''
def readable_regions(pid):
out = []
for line in open(f'/proc/{pid}/maps').read().splitlines():
for line in get_maps(pid).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
@@ -35,17 +31,28 @@ def find_beacon(pid):
return []
res = []
for lo, hi in readable_regions(pid):
sz = (hi - lo) & ~7
sz = hi - lo
if sz <= 0 or sz > 0x200000000:
continue
try:
buf = os.pread(mem, sz, lo)
except Exception:
if sz < 65536:
continue
arr = np.frombuffer(buf, dtype='<u8')
off = 0
while off < sz:
chunk = min(sz - off, 4 << 20)
try:
buf = os.pread(mem, int(chunk), lo + off)
except Exception:
break
arr = np.frombuffer(buf[:len(buf) & ~7], dtype='<u8')
if len(arr):
idx = np.where(arr == BEACON)[0]
for i in idx:
res.append(lo + int(i) * 8)
res.append(lo + off + int(i) * 8)
off += int(chunk)
if len(res) > 64:
break
if len(res) > 64:
break
os.close(mem)
return res
@@ -57,26 +64,42 @@ def main():
host = None
while time.time() - t0 < 60:
for p in glob.glob('/proc/[0-9]*'):
try:
maps = open(f'/proc/{p}/maps').read()
except Exception:
continue
if 'soothe2' not in maps:
continue
host = int(os.path.basename(p))
pid = int(os.path.basename(p))
if 'soothe2' in (get_maps(pid) or ''):
host = pid
break
if host:
break
time.sleep(0.05)
print(f'host={host}')
time.sleep(0.01)
print(f'host={host}', flush=True)
if not host:
proc.kill()
proc.wait(timeout=10)
return
base = vst_base(host)
# allow module load: plugin init first
mem = os.open(f'/proc/{host}/mem', os.O_RDONLY)
t1 = time.time()
while time.time() - t1 < 50:
regs = []
for line in get_maps(host).splitlines():
p = line.split()[0]
lo, hi = int(p.split('-')[0], 16), int(p.split('-')[1], 16)
if 'vst3' in line or 'reaper' in line or 'soothe2' in line and 'rw' not in line.split()[1][:2]:
continue
if 'rw' not in line.split()[1][:2]:
continue
if hi - lo > 0x10000000:
continue
regs.append((lo, hi))
print(f'{len(regs)} rw regions to capture', flush=True)
with open('/tmp/host_rw.bin', 'wb') as f:
for lo, hi in regs:
try:
buf = os.pread(mem, hi - lo, lo)
f.write(lo.to_bytes(8, 'little'))
f.write((hi - lo).to_bytes(8, 'little'))
f.write(buf)
except Exception:
pass
print(f'captured {os.path.getsize("/tmp/host_rw.bin")} bytes', flush=True)
while time.time() - t1 < 30:
hits = find_beacon(host)
ctxs = set()
for p in hits:
@@ -96,7 +119,7 @@ def main():
blk = os.pread(mem, 0x8000, ctx + 0x540000)
open(f'/tmp/ctx_{ctx:x}_win.bin', 'wb').write(blk)
break
time.sleep(0.2)
time.sleep(0.05)
os.close(mem)
try:
proc.wait(timeout=5)
+44
View File
@@ -0,0 +1,44 @@
import subprocess, os, glob, time, struct, sys
def mem_open(pid): return os.open(f'/proc/{pid}/mem', os.O_RDONLY)
def find_hosts():
hosts=[]
for p in glob.glob('/proc/[0-9]*'):
pid=int(os.path.basename(p))
try:
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 'soothe2' in maps and 'reaper' not in cmd:
hosts.append(pid)
return hosts
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','/home/m/soothe-bt/render_long.rpp'],
stdout=open('/dev/null','w'),stderr=subprocess.STDOUT)
# wait for host
t0=time.time(); host=None
while time.time()-t0<60 and not host:
hs=find_hosts()
host=hs[0] if hs else None
time.sleep(0.2)
print('host',host, flush=True)
time.sleep(6) # let render warm up, tables built
fd=mem_open(host)
out=open('/tmp/snap_all.bin','wb')
idx=open('/tmp/snap_all.idx','wb')
t0=time.time(); nreg=0; nbytes=0
for line in open(f'/proc/{host}/maps').read().splitlines():
p=line.split(); lo,hi=(int(x,16) for x in p[0].split('-'))
if 'r' not in p[1]: continue
a=lo
while a<hi:
n=min(hi-a, 8*1024*1024)
try: d=os.pread(fd,n,a)
except Exception: a+=n; continue
if not d: a+=n; continue
out.write(struct.pack('<QQ',a,len(d))); out.write(d)
idx.write(struct.pack('<QQ',a,len(d)))
nreg+=1; nbytes+=len(d)
a+=n
out.close(); idx.close()
print('snapshot ok: %d regs %d MB in %.1fs'%(nreg,nbytes/1e6,time.time()-t0), flush=True)
if proc.poll() is None: proc.kill()