Continues the static lead from two iterations ago
(f3-title-sting-mechanism-found-not-value.md) with the dynamic half it
named as the next step. Booted with --xma_param_probe=true (the same
census menu-audio-cues.md used for the menu's SE cues), no pad input,
recording continuously from window-open: a glyph time series (not a
threshold trigger) and every newly-seen XMA-PARAM stream, stamped on
arrival since Xenia's own log carries no timestamps.
Positive control, and a real one: the probe caught the title's two BGM
stems starting at t=147.6s, matching f3-title-plays-bgm-102-and-103.md's
already-established finding exactly -- the instrument finds a real stream
before being asked to find nothing.
From the plate's first visible activity through 68 seconds of build-in
plus fully-settled pulsing (killed at t=220.1s), zero new XMA streams
appeared beyond the two BGM stems and three unidentified early ones (boot
splash, not this question). No SE-range stream, no second BGM, nothing --
measured, not the prior static reach limit.
Refutation attempt this iteration, recorded either way: my first read of
the fine-grained glyph series said BGM and the plate's build-in start at
"essentially the same moment". Checking the raw per-sample data instead of
a coarse table refutes that -- first non-zero glyph reading is ~0.67s after
BGM onset, and immediately noisy rather than a clean climb. Corrected in
the doc rather than left as an overclaim for someone chasing frame-accurate
sync later.
Reference data: docs/re/data/f3-sting-{glyph-timeseries,xma-param-arrivals}.tsv
-- derived numeric/log-line data, not a capture of rendered game content.
152 lines
5.2 KiB
Python
152 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""F3, the sting half -- watch for a NEW XMA stream during the title's
|
|
build-in, with NO input, aligned against a continuous glyph time series so
|
|
"when did the plate reach full alpha" is measured, not assumed from a
|
|
threshold crossing.
|
|
|
|
Why continuous, not a threshold trigger: this container's own boot gate
|
|
(nav_repeat_and_b.py, f1_hold_capture.py) waits for the glyph count to HOLD
|
|
in [500,2500] for 12 samples before calling it "TITLE" -- which could
|
|
already be past the build-in's interesting part. This script starts
|
|
recording both streams (glyph count, XMA-PARAM arrivals) from the moment
|
|
Canary's window exists, so the whole rise from 0 can be read back, not just
|
|
the plateau.
|
|
|
|
Positive control, per R4: BGM cues 1102/1103 are already known to play on
|
|
the title (f3-title-plays-bgm-102-and-103.md) via this exact probe
|
|
mechanism (menu-audio-cues.md). If this run logs zero XMA-PARAM lines at
|
|
all, the probe found nothing INCLUDING the thing it's supposed to find, and
|
|
the run is void -- not a negative about a sting.
|
|
|
|
f3_sting_probe.py OUTDIR [duration_s]
|
|
"""
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
OUT = sys.argv[1]
|
|
DURATION = float(sys.argv[2]) if len(sys.argv) > 2 else 200.0
|
|
os.makedirs(OUT, exist_ok=True)
|
|
W, H = 1280, 720
|
|
|
|
env = dict(os.environ)
|
|
env["HOME"] = "/sylph-home/re"
|
|
env["SDL_AUDIODRIVER"] = "dummy"
|
|
env["DISPLAY"] = ":98"
|
|
env["XENIA_PAD_FILE"] = os.path.join(OUT, "pad.txt")
|
|
|
|
|
|
def glyph(a):
|
|
r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
|
|
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
|
|
|
|
|
|
def _open():
|
|
return subprocess.Popen(
|
|
["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0",
|
|
"-video_size", f"{W}x{H}", "-i", ":98", "-r", "6",
|
|
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
|
|
stdout=subprocess.PIPE, bufsize=W * H * 3 * 4)
|
|
|
|
|
|
def grab(p, n):
|
|
buf = p.stdout.read(n)
|
|
if len(buf) < n:
|
|
return None
|
|
return np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(float)
|
|
|
|
|
|
def main():
|
|
with open(env["XENIA_PAD_FILE"], "w"):
|
|
pass
|
|
subprocess.run(["xsetroot", "-solid", "black"], env=env, check=False)
|
|
|
|
xuid = os.environ.get("SYLPH_XUID", "")
|
|
if not xuid:
|
|
content = "/sylph-home/re/.local/share/Xenia/content"
|
|
entries = os.listdir(content) if os.path.isdir(content) else []
|
|
xuid = entries[0] if entries else ""
|
|
if not xuid:
|
|
print("FATAL: no profile signed in -- run: run-canary "
|
|
"--create_profile_if_none=Tag, wait ~5s, kill it", flush=True)
|
|
return
|
|
|
|
canary_log_path = os.path.join(OUT, "canary.stdout")
|
|
canary_log = open(canary_log_path, "w")
|
|
proc = subprocess.Popen(
|
|
["run-canary", f"--logged_profile_slot_0_xuid={xuid}",
|
|
"--xma_param_probe=true", "--log_level=2"],
|
|
cwd=OUT, env=env, stdout=canary_log, stderr=subprocess.STDOUT)
|
|
print(f"canary pid={proc.pid}, xma_param_probe=true, waiting for window",
|
|
flush=True)
|
|
|
|
T0 = time.time()
|
|
while not subprocess.run(
|
|
["xdotool", "search", "--name", "Xenia-canary"],
|
|
capture_output=True, text=True).stdout.strip():
|
|
if time.time() - T0 > 60:
|
|
print("FATAL: no window after 60s", flush=True)
|
|
return
|
|
time.sleep(1)
|
|
print(f"[{time.time()-T0:6.1f}s] window exists, recording", flush=True)
|
|
|
|
glyph_out = open(os.path.join(OUT, "glyph-timeseries.tsv"), "w")
|
|
glyph_out.write("# t_s\tglyph\n")
|
|
xma_seen = set()
|
|
xma_out = open(os.path.join(OUT, "xma-param-arrivals.tsv"), "w")
|
|
xma_out.write("# t_s\tline\n")
|
|
XMA_RE = re.compile(rb"XMA-PARAM.*")
|
|
|
|
p, n = _open(), W * H * 3
|
|
seg = time.time()
|
|
log_pos = 0
|
|
while time.time() - T0 < DURATION:
|
|
el = time.time() - T0
|
|
if time.time() - seg > 30:
|
|
p.kill(); p = _open(); seg = time.time()
|
|
a = grab(p, n)
|
|
if a is not None:
|
|
g = glyph(a)
|
|
glyph_out.write(f"{el:.2f}\t{g}\n")
|
|
glyph_out.flush()
|
|
else:
|
|
p.kill(); p = _open(); seg = time.time()
|
|
# Drain any new XMA-PARAM lines that arrived since last check --
|
|
# stamped on ARRIVAL (Xenia's own log lines carry no timestamp),
|
|
# same technique xma_readoff_trace.py already uses.
|
|
try:
|
|
with open(canary_log_path, "rb") as f:
|
|
f.seek(log_pos)
|
|
chunk = f.read()
|
|
log_pos = f.tell()
|
|
except FileNotFoundError:
|
|
chunk = b""
|
|
for line in chunk.splitlines():
|
|
if XMA_RE.search(line):
|
|
key = line
|
|
if key not in xma_seen:
|
|
xma_seen.add(key)
|
|
xma_out.write(f"{el:.2f}\t{line.decode('utf-8','replace')}\n")
|
|
xma_out.flush()
|
|
print(f"[{el:7.1f}s] NEW {line.decode('utf-8','replace')}",
|
|
flush=True)
|
|
p.kill()
|
|
glyph_out.close()
|
|
xma_out.close()
|
|
print(f"[{time.time()-T0:7.1f}s] killing emulator, "
|
|
f"{len(xma_seen)} distinct XMA-PARAM lines seen", flush=True)
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
canary_log.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|