This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/tools/re-capture/findrot_global.py
Sylpheed RE agent 0ddeffa9a6 tools+docs: the flight pad was writing into a dead file - every autopilot input went nowhere
flight_probe.Pad opened /tmp/sylph-vgamepad.fifo, "the vgamepad server's FIFO".
That server was removed when the uinput pad was replaced by Canary's --hid=file
driver, because a uinput device is not namespaced and scripted presses leaked to
the host's desktop. The FIFO is now an ordinary 91-byte file nothing reads, and
/tmp/xenia_pad.txt - the file the emulator polls - was 0 bytes while the
"autopilot" was flying. So every axis, trigger and button from pilot.py,
autopilot3.py, aim_probe.py and flight_probe.py went nowhere, silently.

The corpus already carried this trap for the SHELL scripts
(canary-scripted-input-traps.md, "every call here failed silently"). This class
was missed, and every flight tool imports it.

Verified against the oracle rather than by inspection, on runs confirmed
animating at both ends of every phase: before, full stick produced 0.00 degrees
of heading change over 4 s while the ship travelled 350-735 units, and the
attitude matrix at pos-0x70 was byte-identical; after, LX=-1 turns 12.72 degrees
and LX=+1 swings the flight direction from [1,0,0] to [0.13,-0.14,-0.98], with
the matrix moving 0.44/0.31 under stick and 0.0000 at neutral.

That also REFUTES the "stale attitude matrix" suspicion from the earlier pass -
pos-0x70 is live and tracks the ship; it only looked dead because nothing was
turning the ship.

Still open and said plainly: with the pad fixed a 150 s pilot run still fires 0
times, with |aim yaw| still exactly 90.0 and the target 36-43 km away. Steering
works now, so what is left is the stick SIGN against the pilot's error convention
or a target selection that commits to something too far to close. Both are
finally testable.

findrot_global/findself/findspeed/selfstate still write to the dead FIFO and now
say so in place; they are not repaired because none has been re-run since.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-23 22:09:27 +00:00

155 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Scan ALL of guest RAM for 3x3 orthonormal float blocks (rotation matrices).
Vectorised with numpy: nine shifted views of each extent give every candidate
9-float window at once, so the whole 250 MB working set tests in seconds.
A rotation matrix is a very strong signature — unit rows, mutually orthogonal —
so the hits are almost entirely real orientations. Two passes taken a moment
apart, with a known stick input in between, then say which of them is *ours*.
Usage: findrot_global.py [--track seconds]
"""
import os
import struct
import sys
import time
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gmem # noqa: E402
# 🔴 DEAD FIFO. The vgamepad server this names does not exist any more - the
# uinput pad was replaced by Canary's `--hid=file` driver - so every write here
# goes into an ordinary file that nothing reads, silently. See the rewritten
# `Pad` in flight_probe.py for the format the emulator actually polls, and
# docs/re/pilot-never-fires.md for what this cost. NOT fixed here: these scripts
# have not been re-run since, and changing input plumbing under a tool whose
# results are unverified would only produce new unverified results.
FIFO = "/tmp/sylph-vgamepad.fifo"
TOL = 2e-3
def pad(line):
with open(FIFO, "w") as f:
f.write(line + "\n")
def scan(fd, size):
"""[(file_offset, 9 floats)] for every orthonormal 3x3 block."""
hits = []
for a, b in gmem.extents(fd, size):
n = (b - a) // 4 * 4
if n < 64:
continue
arr = np.frombuffer(os.pread(fd, n, a), dtype=">f4").astype(np.float32)
if arr.size < 16:
continue
m = arr.size - 8
cols = [arr[i:i + m] for i in range(9)]
with np.errstate(invalid="ignore", over="ignore"):
finite = np.ones(m, dtype=bool)
for c in cols:
finite &= np.isfinite(c) & (np.abs(c) <= 1.001)
# row norms
n0 = cols[0] ** 2 + cols[1] ** 2 + cols[2] ** 2
n1 = cols[3] ** 2 + cols[4] ** 2 + cols[5] ** 2
n2 = cols[6] ** 2 + cols[7] ** 2 + cols[8] ** 2
ok = finite
ok &= np.abs(n0 - 1) < TOL
ok &= np.abs(n1 - 1) < TOL
ok &= np.abs(n2 - 1) < TOL
d01 = cols[0] * cols[3] + cols[1] * cols[4] + cols[2] * cols[5]
d02 = cols[0] * cols[6] + cols[1] * cols[7] + cols[2] * cols[8]
d12 = cols[3] * cols[6] + cols[4] * cols[7] + cols[5] * cols[8]
ok &= np.abs(d01) < TOL
ok &= np.abs(d02) < TOL
ok &= np.abs(d12) < TOL
for i in np.flatnonzero(ok):
hits.append(a + int(i) * 4)
return hits
def read_mat(fd, off):
b = os.pread(fd, 36, off)
if len(b) < 36:
return None
return np.array(struct.unpack(">9f", b))
def main():
fd = os.open(gmem.mem_path(), os.O_RDONLY)
size = os.path.getsize(gmem.mem_path())
pad("reset")
time.sleep(0.6)
t0 = time.time()
hits = scan(fd, size)
print(f"# {len(hits)} orthonormal 3x3 blocks in RAM ({time.time()-t0:.1f}s)", flush=True)
if not hits:
return
# which of them rotate when WE yaw? measure change under left vs right
def deltas(lx, secs=2.5, hz=6.0):
"""Mean per-step rotation vector while the stick is held.
Sampled incrementally: at a few degrees per step the skew part of
A·Bᵀ is the rotation vector, which it is not over a 2 s turn. Any block
that stops being orthonormal mid-phase is memory that got reused, not an
orientation, and is dropped.
"""
pad(f"axis LX {lx}")
time.sleep(0.5)
seq = []
for _ in range(int(secs * hz)):
t = time.time()
seq.append({o: read_mat(fd, o) for o in hits})
time.sleep(max(0, 1.0 / hz - (time.time() - t)))
pad("axis LX 0")
time.sleep(1.0)
def ortho(m):
if m is None or not np.all(np.isfinite(m)):
return False
M = m.reshape(3, 3)
return np.max(np.abs(M @ M.T - np.eye(3))) < 5e-3
out = {}
for o in hits:
ms = [s[o] for s in seq]
if not all(ortho(m) for m in ms):
continue
ws = []
for a, b in zip(ms, ms[1:]):
M = a.reshape(3, 3) @ b.reshape(3, 3).T
w = np.array([M[2, 1] - M[1, 2], M[0, 2] - M[2, 0], M[1, 0] - M[0, 1]]) / 2
if np.linalg.norm(w) < 0.5: # small-angle regime only
ws.append(w)
if len(ws) >= 4:
out[o] = np.mean(ws, axis=0)
return out
dl = deltas(-0.9)
dr = deltas(+0.9)
rows = []
for o in hits:
if o not in dl or o not in dr:
continue
a, b = dl[o], dr[o]
na, nb = np.linalg.norm(a), np.linalg.norm(b)
if na < 0.01 or nb < 0.01:
continue
cos = float(a @ b / (na * nb))
rows.append((cos, o, na, nb))
rows.sort()
print("\n# orientation blocks that rotate OPPOSITE ways for left vs right stick")
for cos, o, na, nb in rows[:12]:
va = gmem.primary_va(o)
print(f" va {va:#010x} cos={cos:+.3f} |wL|={na:.3f} |wR|={nb:.3f}")
if not rows:
print(" (none)")
if __name__ == "__main__":
main()