Compare commits

...
6 Commits
35 changed files with 558992 additions and 38 deletions
+206
View File
@@ -0,0 +1,206 @@
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionManager;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.mem.Memory;
import java.io.PrintWriter;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.*;
/**
* DumpFuns2 - unfrozen BFS decompiler.
* Differences vs DumpFuns.java:
* 1. NO haveDec gate: always decompiles into decomp_funs2.txt.
* 2. Extended seeds: explicit critical DSP addresses + reverse callers
* (references-to) so indirect/vtable-dispatched functions enter closure.
* 3. Skip threshold lowered from 30 to 8 instructions (keeps dispatchers/stubs).
* Also writes fun_map2.txt (fresh) and re-scans .data float constants.
*/
public class DumpFuns2 extends GhidraScript {
final long LO = 0x180000000L;
final long HI = 0x182a00000L;
long parseAddr(String s) {
try {
return Long.parseLong(s.replaceAll("[^0-9a-fA-F]", ""), 16);
} catch (Exception e) { return -1; }
}
boolean inRange(long a) { return a >= LO && a <= HI; }
@Override
public void run() throws Exception {
FunctionManager fm = currentProgram.getFunctionManager();
AddressSpace as = currentProgram.getAddressFactory().getDefaultAddressSpace();
// --- seeds: all FUN_ from decomp_dsp.txt + explicit critical DSP addrs ---
Set<Long> seed = new LinkedHashSet<>();
BufferedReader br = new BufferedReader(new FileReader("/home/m/re-tools/decomp_dsp.txt"));
String line;
while ((line = br.readLine()) != null) {
int i = line.indexOf("FUN_");
while (i >= 0) {
int e = i + 4;
while (e < line.length() && "0123456789abcdefABCDEF".indexOf(line.charAt(e)) >= 0) e++;
long a = parseAddr(line.substring(i + 4, e));
if (inRange(a)) seed.add(a);
i = line.indexOf("FUN_", e);
}
}
br.close();
long[] critical = {
0x180529fe0L, // mask/coefficient setup (vtable-only, 2051 instr)
0x180563440L, // LUT curve + gamma + combine
0x180563ce0L, // IIR level-tracker INIT
0x18056e3e0L, // twin-mask factory
0x18052f500L, // interleave
0x18052b550L, // FFT-conv loop
0x18052ee70L, // per-bin gain
0x18052d650L, // band setup
0x18052d920L, // window
0x18052e190L, // buffer alloc
0x18052dc30L, // FFT plan builder
0x18052da00L, // ramp fill
0x18052dbc0L, // cplx-interleave
0x180535880L, // twin kernel
0x180536300L, // twin caller
0x180536f90L, // dead sibling
0x180530d30L, // level-weight formula
0x1805316e0L, // coeff writer (17 case)
0x180563fa0L, // IIR level-tracker UPDATE (caller chain 5631c0)
0x1805631c0L, // LUT+IIR-init+UPDATE dispatcher
0x180563260L, // param dispatcher
0x180563a60L // combine (called from 563440)
};
for (long a : critical) seed.add(a);
// --- vtable slots ---
long[] vts = {
0x1824abb90L, 0x1824ac7a8L, 0x1824ab7c0L, 0x1824ac638L, 0x1824ac5d8L,
0x1824be810L, 0x1824be228L, 0x1824be0a8L, 0x1824be7a0L, 0x1824ac210L,
0x1824ac248L, 0x1824b1ac8L, 0x1824b11c8L, 0x1824b0fd0L, 0x1824b1178L
};
for (long base : vts) {
for (int i = 0; i < 64; i++) {
try {
Address va = as.getAddress(base + i * 8L);
long tgt = currentProgram.getMemory().getLong(va);
if (inRange(tgt)) seed.add(tgt);
} catch (Exception e) { }
}
}
// --- BFS closure forward + REVERSE (indirect callers) ---
Set<Long> done = new TreeSet<>();
Deque<Long> queue = new ArrayDeque<>(seed);
List<Long> order = new ArrayList<>();
int cap = 8000;
while (!queue.isEmpty() && order.size() < cap) {
long a = queue.poll();
if (!done.add(a)) continue;
order.add(a);
Function f = fm.getFunctionAt(as.getAddress(a));
if (f == null) continue;
try {
for (Function c : f.getCalledFunctions(monitor)) {
long ca = c.getEntryPoint().getOffset();
if (inRange(ca) && !done.contains(ca)) queue.add(ca);
}
} catch (Exception e) { }
// reverse: any function referencing this entry (indirect dispatch callers)
try {
for (Reference r : currentProgram.getReferenceManager()
.getReferencesTo(as.getAddress(a))) {
Address from = r.getFromAddress();
if (from == null) continue;
Function cf = fm.getFunctionContaining(from);
if (cf != null) {
long ca = cf.getEntryPoint().getOffset();
if (inRange(ca) && !done.contains(ca)) queue.add(ca);
}
}
} catch (Exception e) { }
}
// --- fun_map2.txt fresh ---
PrintWriter pm = new PrintWriter(new java.io.BufferedWriter(
new FileWriter("/home/m/re-tools/fun_map2.txt", false)));
for (long a : order) {
Function f = fm.getFunctionAt(as.getAddress(a));
if (f == null) continue;
pm.println(Long.toHexString(a) + " " + f.getBody().getNumAddresses() + " " + f.getName());
}
pm.close();
// --- decompile ALWAYS into decomp_funs2.txt ---
DecompInterface di = new DecompInterface();
di.openProgram(currentProgram);
PrintWriter pw = new PrintWriter(new java.io.BufferedWriter(
new FileWriter("/home/m/re-tools/decomp_funs2.txt", false)));
int n = 0, skipped = 0;
for (long a : order) {
Function f = fm.getFunctionAt(as.getAddress(a));
if (f == null) continue;
long sz = f.getBody().getNumAddresses();
if (sz < 8) { skipped++; continue; }
DecompileResults res = di.decompileFunction(f, 120, monitor);
if (res != null && res.getDecompiledFunction() != null) {
n++;
pw.println("############ FUN_ " + Long.toHexString(a) + " size=" + sz + " ############");
pw.println(res.getDecompiledFunction().getC());
pw.println();
}
}
pw.close();
di.dispose();
println("DECOMP2_DONE n=" + n + " skipped=" + skipped + " order=" + order.size());
// --- constants (same as DumpFuns, scaled to what we touched) ---
Map<Long,String> consts = new TreeMap<>();
Memory mem = currentProgram.getMemory();
for (Function f : fm.getFunctions(true)) {
long fa = f.getEntryPoint().getOffset();
if (!order.contains(fa)) continue;
Iterator<Instruction> iit = currentProgram.getListing().getInstructions(f.getBody(), true);
while (iit.hasNext()) {
Instruction ins = iit.next();
for (Reference r : currentProgram.getReferenceManager().getReferencesFrom(ins.getAddress())) {
long ta = r.getToAddress().getOffset();
if ((ta >= 0x1824c0000L && ta <= 0x182700000L) ||
(ta >= 0x180000000L && ta <= 0x181200000L)) {
consts.merge(ta, Long.toHexString(fa), (x, y) -> x + ";" + y);
}
}
}
}
PrintWriter pc = new PrintWriter(new java.io.BufferedWriter(
new FileWriter("/home/m/re-tools/consts2.txt", false)));
for (Map.Entry<Long,String> e : consts.entrySet()) {
try {
long ta = e.getKey();
int[] b = new int[8];
boolean ok = true;
for (int k = 0; k < 8; k++) {
Address a = as.getAddress(ta + k);
if (!mem.getLoadedAndInitializedAddressSet().contains(a)) { ok = false; break; }
b[k] = mem.getByte(a) & 0xff;
}
if (!ok) continue;
float f32 = Float.intBitsToFloat(b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24));
float f32b = Float.intBitsToFloat(b[4] | (b[5] << 8) | (b[6] << 16) | (b[7] << 24));
pc.println(Long.toHexString(ta) + " f32=" + f32 + " f32b=" + f32b + " refs=" + e.getValue());
} catch (Exception ex) { }
}
pc.close();
println("CONSTS2_DONE consts=" + consts.size());
}
}
+38
View File
@@ -0,0 +1,38 @@
import ghidra.app.script.GhidraScript;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.ReferenceIterator;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionManager;
import java.io.PrintWriter;
public class XrefDsp extends GhidraScript {
@Override
public void run() throws Exception {
String[] targets = {"1805631c0","180563260","180563a60","180563fa0","180563ce0","180563440",
"18056e3e0","180529fe0","180509900","180564a00","180537410","1805374e0",
"18052e3e0","18052e9b0"};
AddressSpace as = currentProgram.getAddressFactory().getDefaultAddressSpace();
PrintWriter pw = new PrintWriter(new java.io.BufferedWriter(
new java.io.FileWriter("/home/m/re-tools/xrefs2.txt")));
FunctionManager fm = currentProgram.getFunctionManager();
for (String ts : targets) {
long ta = Long.parseLong(ts,16);
Address t = as.getAddress(ta);
pw.println("### TARGET " + ts);
ReferenceIterator it = currentProgram.getReferenceManager().getReferencesTo(t);
int n=0;
while (it.hasNext() && n<200) {
Reference r = it.next();
Function cf = null;
if (r.getFromAddress()!=null) cf = fm.getFunctionContaining(r.getFromAddress());
pw.println(" from " + r.getFromAddress() + " type=" + r.getReferenceType() +
" in=" + (cf!=null?Long.toHexString(cf.getEntryPoint().getOffset()):"?"));
n++;
}
}
pw.close();
println("XREFS2_DONE");
}
}
+41622
View File
File diff suppressed because one or more lines are too long
+508225
View File
File diff suppressed because it is too large Load Diff
+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))
+7928
View File
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
# 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.
## 2026-08-19b (two-point snapshots t1=8s, t2=40s of render_long 180s)
- rtsnap2.py: TWO snapshots of the SAME 180s render at t1/t2; each 318MB/547regs ~0.2s.
- Registry tables byte-identical between t1/t2 (stable per-band coeffs; confirmed authoritative).
- Diff of arbitrary 342-double windows = pure audio-buffer noise (21981 phantom matches; twin
per-band state is NOT a 342-dbl array a level away). Real twin kernel state per NOTES_TWIN:64
is {double A[3], double B[3]} per band (6 doubles), seeded in build_twin_coeff FUN_180533ec0.
- Conclusion: live per-note-band twin state not usefully separable via full-heap diff; the
attack/release input coeffs live in the DSP ctx scalars: 0x540888/0x54088c = expf(p*0.11513)
(static-derived), registry holds the per-bin WEIGHT tables (already captured). Twin kernels
themselves validated statically (twin_check max|err|=1.27e-5). => Step C goal (window + axes +
weights + kernel parity) is effectively CLOSED; only scalar A/R params remain, derived from RPP
params, no live capture needed.
+84
View File
@@ -556,3 +556,87 @@ Weighted refits move error around but never reduce max < 0.7:
rp(Q) ~ Q^0.216 means the res-power correction grows with Q: high-Q resonance pit rp(Q) ~ Q^0.216 means the res-power correction grows with Q: high-Q resonance pit
dips harder off-center. Consistent with decomp (res-weighted gain path in twin-mask dips harder off-center. Consistent with decomp (res-weighted gain path in twin-mask
factory). The q1@2000 residual is structural warp/model mismatch, not rp. factory). The q1@2000 residual is structural warp/model mismatch, not rp.
## ============ 2026-08-19: STATIC DECOMP LOCKED (all DSP bodies recovered) ============
New full decomp run (DumpFuns2.java, no haveDec gate, seeds=dsp+vtables+reverse-callers,
skip<8): decomp_funs2.txt (7746 bodies), fun_map2.txt (7928 funcs), consts2.txt (41622).
Previously-cold roots now present: 180529fe0 (2051in), 180563440, 180563ce0 (IIR INIT),
18056e3e0, 18052e9b0 (3167in), 18052e260, 18052e190/52e9b0 allocators.
### MAIN RENDER LOOP (found): FUN_18052e260
- Signature (param_1=DSP obj, param_2=nSamples). iVar5=NFFT/2+1; loop per band
(count=0x2404d0): FUN_180536300(param_1+0x3d8, band_in_ptr, 0x540758+bufoff, band, n).
Uses full-STFT frame budget 0x2404d8; tail marks bands needing rebuild (0x240467 flags).
- => 52e260 = per-frame dual-band SIDE-CHAIN driver; twin = per-band.
### FUN_180536300 (twin caller, size=475) — audio path per band:
- lVar1 normalize = FUN_18052da00(scratch, in, scale_bin, n): pointwise in[i]*scale.
- band active (0x814): twin kernel FUN_180535880/180536f90(lVar2, base, band_i, lVar1, n),
else fill FUN_18052db50 (1.0 / 0.0).
- combine lVar4/lVar3 (52d990, 0x814>1), output copy 52dbc0(base, lVar4, n).
- TWIN KERNELS (535880/536f90): Ghidra emits ONLY the tail-call frame —
"WARNING: Removing unreachable block ..." => state shown = {pdVar1,*pdVar2} double pairs
read from param_3/param_4, then noreturn tail into thunk_FUN_181ba94b0. **The recursive
per-bin IIR level-smoothing (tatt/trel) lives INSIDE these two tail-calls, which static
decomp cannot recover** — same family as 0x540658 window (step-5 window). Confirmed
statically-invisible frontier = {532a715 twin kernels, 0x540658}.
### Level-param setters (vtable stubs, auto-named .?AV?$Soothe2Module@M$01@@::vtbl_...):
- 18052bba0: 0x540870 = expf((p*DAT_1824c4348 + DAT_1824c44a4)*DAT_1824c3cd4) [gain/sens]
- 18052bb80/bb60: 0x540878 / 0x54087c = raw int [freq?/bandwidth?]
- 18052bb40/bb20: 0x540880 / 0x540884 = raw int [per-band level / dur]
- 18052bb00: 0x540874 = p (locked); 18052bad0/baa0: 0x540888 / 0x54088c =
expf(p*DAT_1824c3cd4) [attack / release coeff db]
- DAT_1824c3cd4 = 0.115129255 = ln(10)/20 => expf(p*0.11513) = 10^(p/20): **all these
setters convert dB-speed params to linear coeffs**. 0x540888/88c feed FUN_180529fe0
step-5 dry/wet (mask peak) — the real attack/release coeffs.
- FUN_180530d30 (0x5406b8/6c8/6d8/6e8 weights): fVar9 = 2000.0/NFFT? base; w8=pow(base,0.25);
v = level*0.25*w8*factor(4.0); q=1/(1+v/(level*4)); dVar1=(sr/0x1a0)*0x1ac*0.001;
w = 0.1^(1/(max(q*v)*dVar1)); pairs (w,1-w).
- Numeric check (this session, real values): bin500 w~10^-6, bin2000 similar => 0x530d30
does NOT tilt 2000>500. Tilt stays = warp (0x5406a8, K=7.942) + twin-mask curving.
Static decomp now COMPLETE up to the statically-invisible frontier confirmed above.
## ============ 2026-08-19 (runtime capture SETTLED): registry heartbeat + live tables ============
## **Window 0x540658 + freq-axis CAPTURED live. Internal DSP sample rate = 48000 (not 44100).**
- rtsnap_fast.py: spawn reaper render_long offline -> find host (soothe2 in /proc maps, not reaper
cmd) -> sleep 6s -> snapshot ALL readable maps chunked 8MB -> /tmp/snap_all.bin (318MB, 546 regs,
entries {lo,sz,bytes}+pad, idx in /tmp/snap_all.idx). Chunked pread REQUIRED: whole-region pread EIO.
- **registry heartbeat**: heap run of {u64 count, u64 ptr} pairs (stride 0x10) at **0x29b06c0**
(arena anon 0x2922000, 0x77e000), 36 entries — analog of GUI note 0x28b06c0. Every DSP buffer
base is a registry entry: [00] identity, **[01] = WIN_freq window 0.5->1.0 (saturates)** (this is
live 0x540658 8193 f32: 0.5, idx512=0.345?; idx1024=0.68, idx2048=0.8, tail 1.0),
[02] 0->~0.01, [03]=rwin_C0 0.596->0.126, [04]=0.404->0.874 complement, [05]=0.0435->~0,
[06]=0.9565->~1.0, [07] zeros+neg, [0d]=**freq-axis 0..23988.3 spacing 11.713 = 48000/4096**,
[0e]=2.017->0, [0f] doubled, [10]=1.2914->0, FFT work bufs 131072/32768/65536/16384.
- **CRITICAL**: freq-axis spacing 11.713 Hz => internal SR = 48000 regardless of project 44100.
Registry[01] window & weights are therefore the 48k tables; rwin_C0[03] byte-matches GUI 44100 file
(same table reused). Saved live tables:
- handoff/rtfreqaxis_48000_internal.npy (2048 f32, 0..23988.287, spacing 11.713)
- handoff/rtwin_freq_44100.npy (8193 f32 WIN window 0.5->1.0) <-- THE 0x540658 live capture
- handoff/rtwa_596.npy, rtwb_404.npy, rtwc_043.npy, rtwd_956.npy (registry [03][04][05][06])
- vptr/44100-marker object-scan (rtobj/rtdump2/rtall) remains DEAD: no vtable 0x1824abb90 bank hit in
heap, fresh-render host has no 44100.0 constants (internal is 48k). The registry run is the beacon.
- twin IIR per-bin attack/release: NOT in registry (those are {att,rel} pair floats at 0x540888/88c,
expf(p*0.11513), per-note-band smoothing inside tail-call thunks). Live capture needs the twin band
object; not yet located. Registry gives per-bin WEIGHTS (done) — attack/release remain static-only.
- Registry dump + all raw tables saved: /tmp/rtcapt/registry.txt, /tmp/rtcapt/*.f32.
- Scripts captured into repo: rtsnap_fast.py (snapshot), findctx.py (locate), rtchunk.py (chunked IO).
## ============ 2026-08-19 (STRUCTURAL LUT CURVE FUN_180563440 — EXACT FORMULAS) ============
- Verified against f_563440.dis + PE constants 0x1824c3c54=0.0009775171(=1/1023? 1/0x3ff),
0x1824c3ea4=1.0, 0x1824c41e0=2.0, 0x1824c4680=-1.0, 0x1824c3d8c=0.5, 0x1824c4f10=0x7fff.. (double |x| mask).
- Loop: 0x400 iterations (0..0x3ff), x = i*(1/1023)? const 0.0009775171 = 1/1023 → last bin x≈1.0.
(prior note said 1/1024 scale — corrected: constant value is 1/1023.)
- Curve config at ctx+0x188: word A(+0x00)=min, B(+0x04)=max, gamma(+0x0c), flag(+0x10),
callback(+0x50). Output written as double to ctx+0x198[i*8] (0x400 doubles).
- **Linear path (flag 0x10==0, 0x563595)**: t = x; if gamma!=1.0 and x>0: t = exp(log(x)/gamma);
val = A + (B-A)*t. (A=min of output, B=max).
- **Power-law path (flag!=0, 0x5635cd)**: t = 2x-1 (centered -1..1); if gamma!=1.0:
t = sign(t) * exp(log(|t|)/gamma) (abs-mask 0x24c4f10, log DIVSS gamma, exp, sign via xmm11/xmm8);
val = A + (B-A)*0.5*(1+t). When gamma=1.0 → val = A + (B-A)*x (identity linear).
- This is the exact runtime curve to replace Pchip/LUT empirical (roadmap Q1). Need live (A,B,gamma)
per band config from ctx+0x188 — requires DSP ctx base (registry doesn't own the curve struct).
- Level-tracker IIR (FUN_180563ce0) INIT confirmed: 341 bins x2?, state rows at +0x28/+0x40/+0x58
init [1,0,0,0]/[-1,0,0,0], level coeff 0.1 (3dcccccd) = attack/release α; UPDATE loop remains
unmapped (field for future live capture; per registry tables stable between snapshots).
+30
View File
@@ -0,0 +1,30 @@
import base64, re
def decode(path):
txt=open(path).read()
lines=txt.split('\n')
for i,l in enumerate(lines):
if 'soothe2_x64.vst3' in l:
b=[]
j=i+1
while j<len(lines):
t=lines[j].strip()
if t=='}': break
b.append(t); j+=1
raw=re.sub(r'\s','',''.join(b))
raw=raw.rstrip('=')
raw+='='*((-len(raw))%4)
dec=base64.b64decode(raw)
# dec is nested: outer base64 blob contains an inner chunked-xml whose first child is another base64 or actual xml
# find '<PARAM ...' anywhere after utf8 decode of inner
# Look for actual XML PARAM tags embedded (they are inside processorStateData attribute - base64 too)
# Simplest: find 'band1 mode' bytes
for pidname in [b'band1 mode', b'band1 q', b'band1 sens', b'band1 freq', b'band1 on']:
i2=dec.find(pidname)
if i2>=0:
seg=dec[i2-60:i2+80]
m=re.search(rb'<PARAM id="([^"]+)" value="([^"]+)"', seg)
if m:
print(f" {m.group(1).decode():16s} = {m.group(2).decode()}")
return
for f in ['/home/m/soothe-bt/t1kq_b1f_800.rpp','/home/m/soothe-bt/dual_b1q_0.1.rpp','/home/m/soothe-bt/dual_b1q_10.0.rpp','/home/m/soothe-bt/dual_b1q_1.0.rpp','/home/m/soothe-bt/t1k_b1f_1100.rpp']:
print("=== ",f.split('/')[-1]); decode(f)
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
import base64, re, sys
def full_decode(path):
txt = open(path).read()
lines = txt.split('\n')
for i, l in enumerate(lines):
if 'soothe2_x64.vst3' in l:
b = []
j = i + 1
while j < len(lines):
t = lines[j].strip()
if t == '}':
break
b.append(t)
j += 1
raw = re.sub(r'\s', '', ''.join(b))
raw = raw.rstrip('=')
raw += '=' * ((-len(raw)) % 4)
dec = base64.b64decode(raw)
# print first bytes overview
print('decoded blob len', len(dec))
# Look for inner base64 chunked xml (soothe2 vst uses nested b64)
# search for PARAM tags in raw decoded bytes
tags = re.findall(rb'<PARAM id="([^"]+)" value="([^"]+)"', dec)
if tags:
for pid, val in tags:
print(f' {pid.decode():16s} = {val.decode()}')
else:
# try inner b64
inner = re.sub(rb'[^A-Za-z0-9+/=]', b'', dec)
inner = inner.rstrip(b'=')
inner += b'=' * ((-len(inner)) % 4)
try:
dec2 = base64.b64decode(inner)
tags = re.findall(rb'<PARAM id="([^"]+)" value="([^"]+)"', dec2)
for pid, val in tags:
print(f' {pid.decode():16s} = {val.decode()}')
except Exception as e:
print(' inner b64 fail:', e)
return
print('no soothe2_x64.vst3 found')
for f in sys.argv[1:]:
print('===', f.split('/')[-1])
full_decode(f)
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Render-parity harness (Phase 5 step 5, dB stage).
Measures the steady-state per-tone reduction directly on the reference wavs in
/home/m/soothe-bt/ and compares with the B.12 bridge model (g*LUT + w*warp^a).
Model: C(f) = g*LUT(log10(L0/res_band(f,fc,Q))) + w*warp(f)^a -> red = -20*log10(1-C).
LUT is the frozen PCHIP (B.11 knots). g=1.221, w=0.358, a=3.143 (model_fir.py canonical).
Reference renders: 24-bit WAV, input sources 16-bit mono. Tone amplitude estimated by
Goertzel at the exact tone frequency over a late steady window (3.0-3.75 s region).
"""
import wave
import numpy as np
from scipy.interpolate import PchipInterpolator
BT = '/home/m/soothe-bt/'
FS = 44100.0
GAIN = 4.132 # 10^(sens_dB/40) with sens_dB=24.65
QM = [0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0]
FCS = [800., 900., 950., 1000., 1050., 1100., 1200.]
L0D = 10 ** (-7.142 / 20) # dual input tone level
L0Q = 10 ** (-18.063 / 20) # t1kq input tone level
L0T = 1.0 # t1k input tone level (0 dBFS)
# frozen LUT knots (B.11)
LX = np.array([-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.574, 0.61, 0.75, 1.0])
LY = np.array([0.4402, 0.4552, 0.4813, 0.5072, 0.5329, 0.5332, 0.5645, 0.6471, 0.6562, 0.6670])
LUT = PchipInterpolator(LX, LY)
def load(p):
w = wave.open(p, 'rb')
n, ch, sr, sw = w.getnframes(), w.getnchannels(), w.getframerate(), w.getsampwidth()
b = w.readframes(n)
N = n * ch
if sw == 2:
x = np.frombuffer(b, dtype='<i2').astype(np.float64) / 32768.0
else:
raw = np.frombuffer(b, dtype=np.uint8).reshape(N, 3)
x = (raw[:, 0].astype(np.int64) | (raw[:, 1].astype(np.int64) << 8)
| (raw[:, 2].astype(np.int64) << 16))
x = np.where(x >= 0x800000, x - 0x1000000, x).astype(np.float64) / 8388607.0
return x.reshape(-1, ch)
def tone_amp(p, f):
a = load(p)
end = min(len(a), int(3.5 * FS))
seg = a[:end][-int(0.75 * FS):]
x = np.mean(seg, axis=1)
n = len(x)
w = 2 * np.pi * f / FS
cw = 2 * np.cos(w)
s0 = s1 = s2 = 0.0
for v in x:
s2 = s1
s1 = s0
s0 = v + cw * s1 - s2
return np.sqrt(abs(s0 * s0 + s1 * s1 - 2 * cw * s0 * s1)) / n
def red(src, out, f):
return 20 * np.log10(tone_amp(src, f) / tone_amp(out, f))
def res_band(f, fc, Q):
w0 = fc * 2 * np.pi / FS
c, s = np.cos(w0), np.sin(w0)
p = (s * 0.5) / Q
a, a2 = p * GAIN, p / GAIN
A = [a + 1, -2 * c, 1 - a]
B = [a2 + 1, -2 * c, 1 - a2]
ww = 2 * np.pi * f / FS
z = np.exp(-1j * ww)
return abs(2 * (B[0] + B[1] * z + B[2] * z * z) / (A[0] + A[1] * z + A[2] * z * z))
def warp(f):
x = f / 2000.
return 0.87 * 7.942 * x / (7.942 + x)
def pred(f, fc, Q, L0):
xv = np.log10(L0 / res_band(f, fc, Q))
C = 1.221 * float(LUT(float(np.clip(xv, -1, 1.5)))) + 0.358 * warp(f) ** 3.143
return -20 * np.log10(1 - min(C, 0.999))
def main():
meas, pr = [], []
for q in QM:
for f in (500, 2000):
meas.append(red(BT + 'dual.wav', BT + f'dual_b1q_{q}.wav', f))
pr.append(pred(f, 500, q, L0D))
for fc in FCS:
meas.append(red(BT + 'tone1kq.wav', BT + f't1kq_b1f_{int(fc)}.wav', 1000))
pr.append(pred(1000, fc, 0.9999978, L0Q))
for fc in FCS:
meas.append(red(BT + 'tone1k.wav', BT + f't1k_b1f_{int(fc)}.wav', 1000))
pr.append(pred(1000, fc, 0.9999978, L0T))
meas = np.array(meas)
pr = np.array(pr)
print('RENDER-PARITY vs /home/m/soothe-bt (36 pts, real wavs), B.12 (g=1.221,w=0.358,a=3.143):')
print(' TOTAL rmse = %.4f dB' % np.sqrt(np.mean((pr - meas) ** 2)))
for name, sl in [('dual500', slice(0, 22, 2)), ('dual2000', slice(1, 22, 2)),
('t1kq', slice(22, 29)), ('t1k', slice(29, 36))]:
print(' %-9s rmse=%.4f' % (name, np.sqrt(np.mean((pr[sl] - meas[sl]) ** 2))))
print(' dual2000 resid:', ' '.join('%+.2f' % x for x in (pr[1:22:2] - meas[1:22:2])))
print(' t1kq resid: ', ' '.join('%+.2f' % x for x in (pr[22:29] - meas[22:29])))
print(' t1k resid: ', ' '.join('%+.2f' % x for x in (pr[29:] - meas[29:])))
if __name__ == '__main__':
main()
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
import base64, re, sys, html
def rpp_params(path):
txt = open(path, encoding='utf8', errors='ignore').read()
lines = txt.split('\n')
for i, l in enumerate(lines):
if 'soothe2_x64.vst3' in l:
break
b = []
for l in lines[i + 1:]:
if l.strip() == '}':
break
b.append(l.strip())
raw = re.sub(r'[^A-Za-z0-9+/=]', '', ''.join(b))
raw = raw.rstrip('=')
# drop chars until b64 len divisible by 4 (the leading binary header causes odd)
while (len(raw) % 4) != 0:
raw = raw[:-1]
padded = raw + '=' * ((-len(raw)) % 4)
dec = base64.b64decode(padded, validate=False)
p = dec.find(b'<?xml')
xml = dec[p:].decode('utf8', 'ignore')
xml = html.unescape(xml)
params = dict(re.findall(r'<PARAM id="([^"]+)" value="([^"]+)"', xml))
# also decode processorStateData inner
proc = re.search(r'processorStateData="([^"]+)"', xml)
if proc:
inner = proc.group(1)
try:
inner_dec = base64.b64decode(re.sub(r'\s', '', inner))
print(' [processorStateData inner]', inner_dec.decode('utf8', 'ignore')[:400])
except Exception as e:
print(' proc inner err', e)
return params
for f in sys.argv[1:]:
p = rpp_params(f)
print('===', f.split('/')[-1])
for k in sorted(p):
print(f' {k:24s} = {p[k]}')
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.
Binary file not shown.
BIN
View File
Binary file not shown.
+8 -2
View File
@@ -146,8 +146,14 @@ gain = max(1 - C, eps) * res_k**rp # rp = rp0 * Q**drp (res_power)
2. **warp-терм**: эмпирическая формула — аппроксимация runtime-вычисления FUN_180563440; 2. **warp-терм**: эмпирическая формула — аппроксимация runtime-вычисления FUN_180563440;
для bit-exact заменить на реальный расчёт. для bit-exact заменить на реальный расчёт.
3. **Стерео-верификация (M8)** — приоритет mono-путь или сразу стерео-граф. 3. **Стерео-верификация (M8)** — приоритет mono-путь или сразу стерео-граф.
4. **0x540658 window**статически невидим; живые копии `rwin_*.npy` (48k) требуют warp-нормализации 4. **0x540658 window****ЗАКРЫТ 2026-08-19**: live-захват через registry heartbeat
под 44.1k рендеры. (`{u64 count, u64 ptr}` run at psy. 0x29b06c0) во время offline-рендера. Окно = registry[01]
(8193 f32, 0.5→1.0, сатурация), freq-axis = registry[0d] (0..23988 Hz, spacing 11.713 ⇒
**внутренний SR=48000**), веса [03][04][05][06] (WA/WB/WC/WD). Сохранено: `handoff/rtwin_freq_44100.npy`,
`rtfreqaxis_48000_internal.npy`, `rtwa/rtwb/rtwc/rtwd_*.npy`. Инфра: `rtsnap_fast.py`,
`rtsnap2.py` (двухточечный diff подтвердил стабильность таблиц), `findctx.py`. Полное описание:
`handoff/NOTES_CAPTURE.md`. vptr/44100-marker объект-скан (rtobj) — тупик (производная vtable
не адресуется статически; регистр-бикон — рабочий путь).
## Где лежат детали ## Где лежат детали
+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()
+59 -36
View File
@@ -1,29 +1,25 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""rtdeep2.py — find DSP ctx object via phase-table pointer beacon, then dump window 0x540658 & neighbors.""" """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 numpy as np
import struct
RPP = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual300.rpp' 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): def get_maps(pid):
lo = None try:
for line in open(f'/proc/{pid}/maps').read().splitlines(): return open(f'/proc/{pid}/maps').read()
if 'soothe2' not in line: except Exception:
continue return ''
a = int(line.split()[0].split('-')[0], 16)
lo = a if lo is None else min(lo, a)
return lo
def readable_regions(pid): def readable_regions(pid):
out = [] out = []
for line in open(f'/proc/{pid}/maps').read().splitlines(): for line in get_maps(pid).splitlines():
p = line.split()[0] p = line.split()[0]
lo, hi = int(p.split('-')[0], 16), int(p.split('-')[1], 16) lo, hi = int(p.split('-')[0], 16), int(p.split('-')[1], 16)
if 'vst3' in line:
continue # skip module itself
out.append((lo, hi)) out.append((lo, hi))
return out return out
@@ -35,17 +31,28 @@ def find_beacon(pid):
return [] return []
res = [] res = []
for lo, hi in readable_regions(pid): for lo, hi in readable_regions(pid):
sz = (hi - lo) & ~7 sz = hi - lo
if sz <= 0 or sz > 0x200000000: if sz <= 0 or sz > 0x200000000:
continue continue
try: if sz < 65536:
buf = os.pread(mem, sz, lo)
except Exception:
continue continue
arr = np.frombuffer(buf, dtype='<u8') off = 0
idx = np.where(arr == BEACON)[0] while off < sz:
for i in idx: chunk = min(sz - off, 4 << 20)
res.append(lo + int(i) * 8) 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 + off + int(i) * 8)
off += int(chunk)
if len(res) > 64:
break
if len(res) > 64:
break
os.close(mem) os.close(mem)
return res return res
@@ -57,26 +64,42 @@ def main():
host = None host = None
while time.time() - t0 < 60: while time.time() - t0 < 60:
for p in glob.glob('/proc/[0-9]*'): for p in glob.glob('/proc/[0-9]*'):
try: pid = int(os.path.basename(p))
maps = open(f'/proc/{p}/maps').read() if 'soothe2' in (get_maps(pid) or ''):
except Exception: host = pid
continue break
if 'soothe2' not in maps:
continue
host = int(os.path.basename(p))
break
if host: if host:
break break
time.sleep(0.05) time.sleep(0.01)
print(f'host={host}') print(f'host={host}', flush=True)
if not host: if not host:
proc.kill() proc.wait(timeout=10)
return return
base = vst_base(host)
# allow module load: plugin init first
mem = os.open(f'/proc/{host}/mem', os.O_RDONLY) mem = os.open(f'/proc/{host}/mem', os.O_RDONLY)
t1 = time.time() 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) hits = find_beacon(host)
ctxs = set() ctxs = set()
for p in hits: for p in hits:
@@ -96,7 +119,7 @@ def main():
blk = os.pread(mem, 0x8000, ctx + 0x540000) blk = os.pread(mem, 0x8000, ctx + 0x540000)
open(f'/tmp/ctx_{ctx:x}_win.bin', 'wb').write(blk) open(f'/tmp/ctx_{ctx:x}_win.bin', 'wb').write(blk)
break break
time.sleep(0.2) time.sleep(0.05)
os.close(mem) os.close(mem)
try: try:
proc.wait(timeout=5) proc.wait(timeout=5)
+247
View File
@@ -0,0 +1,247 @@
#!/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()
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""rtsnap.py — differential snapshot of a live process for ctx discovery.
Captures readable regions page-wise (skipping EIO pages), stores: index(file) + regions.raw
Usage: rtsnap.py <pid> <outprefix>
"""
import os, struct, sys
pid = int(sys.argv[1])
pre = sys.argv[2]
mem = os.open(f'/proc/{pid}/mem', os.O_RDONLY)
regs = []
off = 0
for line in open(f'/proc/{pid}/maps'):
p = line.split()[0]
lo, hi = [int(x, 16) for x in p.split('-')]
perm = line.split()[1]
if perm[0] != 'r':
continue
if 'vst3' in line or 'dri' in line or 'shm' in line:
continue
if hi - lo > 0x40000000:
continue
regs.append((lo, hi))
print(f'{len(regs)} regions', flush=True)
idx = []
raw = bytearray()
for lo, hi in regs:
rstart = len(raw)
sz = 0
for pg in range(lo, hi, 0x1000):
try:
b = os.pread(mem, 0x1000, pg)
raw.extend(b)
sz += len(b)
except Exception:
raw.extend(b'\0' * 0x1000)
idx.append((lo, rstart, sz))
with open(pre + '.raw', 'wb') as f:
f.write(bytes(raw))
with open(pre + '.idx', 'wb') as f:
for lo, rstart, sz in idx:
f.write(struct.pack('<QQQ', lo, rstart, sz))
os.close(mem)
print(f'done: raw={len(raw)} bytes', flush=True)
+43
View File
@@ -0,0 +1,43 @@
import subprocess, os, glob, time, struct
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)
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)
fd=mem_open(host)
def snap(tag):
out=open('/tmp/snap_%s.bin'%tag,'wb'); idx=open('/tmp/snap_%s.idx'%tag,'wb')
t0=time.time(); nreg=0; nb=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; nb+=len(d); a+=n
out.close(); idx.close()
print('snap %s: %d MB %d regs in %.1fs'%(tag,nb/1e6,nreg,time.time()-t0), flush=True)
time.sleep(8); snap('t1')
time.sleep(32); snap('t2')
time.sleep(200)
if proc.poll() is None: proc.kill()
print('done')
+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()
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
### TARGET 1805631c0
from 1826a03cc type=DATA in=?
from 1824b14f0 type=DATA in=?
from 1824b14f8 type=DATA in=?
### TARGET 180563260
from 1826a03e4 type=DATA in=?
from 1824b1510 type=DATA in=?
### TARGET 180563a60
from 1826a0408 type=DATA in=?
from 1805636c2 type=UNCONDITIONAL_CALL in=180563440
### TARGET 180563fa0
from 1826a0420 type=DATA in=?
from 182537570 type=DATA in=?
from 182537580 type=DATA in=?
from 1805631e7 type=UNCONDITIONAL_CALL in=1805631c0
from 1805632b4 type=UNCONDITIONAL_CALL in=180563260
### TARGET 180563ce0
from 1826a0414 type=DATA in=?
from 1805631da type=UNCONDITIONAL_CALL in=1805631c0
from 1805632a7 type=UNCONDITIONAL_CALL in=180563260
### TARGET 180563440
from 1826a03fc type=DATA in=?
from 1805631c9 type=UNCONDITIONAL_CALL in=1805631c0
from 180563296 type=UNCONDITIONAL_CALL in=180563260
### TARGET 18056e3e0
from 1826a0fcc type=DATA in=?
from 1805636a8 type=UNCONDITIONAL_CALL in=180563440
### TARGET 180529fe0
from 18269dc54 type=DATA in=?
from 182531b78 type=DATA in=?
from 182531c1c type=DATA in=?
from 1824ac240 type=DATA in=?
### TARGET 180509900
from 18269c058 type=DATA in=?
from 1824a8b88 type=DATA in=?
### TARGET 180564a00
from 1826a0498 type=DATA in=?
from 1824b1750 type=DATA in=?
### TARGET 180537410
from 1824ac3a0 type=DATA in=?
### TARGET 1805374e0
from 18269e4ac type=DATA in=?
from 1824ab908 type=DATA in=?
### TARGET 18052e3e0
### TARGET 18052e9b0
from 18269dfa8 type=DATA in=?
from 1825320d8 type=DATA in=?
from 182532148 type=DATA in=?
from 18253215c type=DATA in=?
from 18052b942 type=UNCONDITIONAL_CALL in=18052b940
from 18052ba28 type=UNCONDITIONAL_CALL in=18052ba20