re: F3 sting half closed -- no sting, measured with a working positive control
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.
This commit is contained in:
151
tools/re-capture/f3_sting_probe.py
Normal file
151
tools/re-capture/f3_sting_probe.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user