port: add Q10 -- what a music bank's sub-waves actually are
BGM_001.slb is three sub-waves (10 KB, 4.47 MB, 4.67 MB) and the decoder concatenates them into one 347 s track. That is a default nobody chose, not a decision: two near-equal halves could be intro + loop, two variations, or two halves of one piece, and a menu that loops its music needs to know which. Found while wiring the Audio Library up to the shared banks. Recorded in HANDOFF.md as a trap too, so the port does not build looping on top of the concatenated track before the question is answered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ authored version can be deleted.
|
||||
| Q7 | transitions | ❔ open | |
|
||||
| Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not |
|
||||
| Q9 | video binding + playback rules | 🟡 partial | `ADV.wmv` is the boot intro; new-game intro unidentified |
|
||||
| Q10 | music-bank sub-wave roles (intro+loop?) | ❔ open | we concatenate blindly today |
|
||||
| S1 | Ready Room go/no-go | ❔ open | probe not run |
|
||||
|
||||
## Already settled — the port can rely on these today
|
||||
@@ -76,6 +77,11 @@ authored version can be deleted.
|
||||
reader must allow a short read there and only there.
|
||||
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
|
||||
for spoken lines and discards half a music mix.
|
||||
* **A music bank has several sub-waves and we glue them together.** `BGM_001`
|
||||
is 10 KB + 4.47 MB + 4.67 MB, concatenated into one 347 s track. Nobody has
|
||||
established whether those are intro + loop, two variations, or two halves —
|
||||
see Q10. Do not build menu looping on the concatenated track until it is
|
||||
answered.
|
||||
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
|
||||
number of XMA1 packets from any known data offset.
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ alongside it.
|
||||
| **Q7** | **Transitions.** What happens visually between screens — the `pteff00.prm` quads, a fade, a cut — and its timing | Described and timed against a capture |
|
||||
| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each |
|
||||
| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules |
|
||||
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
|
||||
| **S1** | **Ready Room probe.** *Gated* — one iteration, then stop | A written go/no-go (see below) |
|
||||
|
||||
## Known unknowns — say so, do not fill them in
|
||||
|
||||
89
tools/re-capture/kf_time_probe.py
Executable file
89
tools/re-capture/kf_time_probe.py
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Per-frame quad geometry and vertex colour from a `log_ui_draws` capture.
|
||||
|
||||
`ui_draw_order.py` answers "in what order were these painted"; this answers
|
||||
"what did this quad look like on frame N, and on N+1". That is what a keyframe
|
||||
TIME unit has to be measured against: the bundle says an element ramps from
|
||||
t=31 to t=34, and the only way to learn what a `t` is worth is to count the
|
||||
rendered frames the same ramp takes in the running game.
|
||||
|
||||
kf_time_probe.py <capture.log> [--csv out.csv]
|
||||
|
||||
Emits one row per quad per frame: frame, draw index, pixel rect, whether the
|
||||
quad is axis-aligned, and the per-vertex colour word (whose alpha byte is the
|
||||
element's fade). Frame numbers are the emulator's VdSwap count, so they are
|
||||
SUBMITTED FRAMES, not wall-clock — which is the point: an emulator that runs
|
||||
at half speed does not move them.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
W, H = 1280, 720
|
||||
VERT = re.compile(r"\[(-?\d+\.\d+),(-?\d+\.\d+),z=(-?\d+\.\d+)(?:,col=([0-9A-F]{8}))?\]")
|
||||
|
||||
|
||||
def quads(log):
|
||||
"""Yield (frame, draw, x0, y0, w, h, rot, col) for every quad in the log."""
|
||||
frame, pending = None, None
|
||||
for line in open(log):
|
||||
if line.startswith("# every draw"):
|
||||
frame = int(line.rstrip().split()[-1].split("..")[0])
|
||||
continue
|
||||
if line.startswith("--- frame"):
|
||||
frame = int(line.split()[2])
|
||||
continue
|
||||
m = re.match(r"\s*(\d+) (prim=.*)", line)
|
||||
if m:
|
||||
pending = (int(m.group(1)), m.group(2))
|
||||
continue
|
||||
if "vb=0x" in line and pending:
|
||||
hits = VERT.findall(line)
|
||||
verts = [(float(a), float(b), c) for a, b, _z, c in hits]
|
||||
if verts:
|
||||
# Two conventions in one log: the UI sprite shader emits NDC,
|
||||
# the full-screen pass emits pixels already.
|
||||
if max(abs(v) for x, y, _c in verts for v in (x, y)) > 4.0:
|
||||
pts = [(x, y, c) for x, y, c in verts]
|
||||
else:
|
||||
pts = [((x + 1) / 2 * W, (1 - y) / 2 * H, c) for x, y, c in verts]
|
||||
per = 4 if "prim=13" in pending[1] else len(pts)
|
||||
for q in range(0, len(pts), per):
|
||||
chunk = pts[q:q + per]
|
||||
if not chunk:
|
||||
continue
|
||||
xs = [p[0] for p in chunk]
|
||||
ys = [p[1] for p in chunk]
|
||||
x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
|
||||
rot = all(
|
||||
(abs(x - x0) < 1.0 or abs(x - x1) < 1.0)
|
||||
and (abs(y - y0) < 1.0 or abs(y - y1) < 1.0)
|
||||
for x, y in zip(xs, ys)
|
||||
)
|
||||
cols = {p[2] for p in chunk if p[2]}
|
||||
col = sorted(cols)[0] if len(cols) == 1 else (
|
||||
"/".join(sorted(cols)) if cols else "")
|
||||
yield (frame, pending[0], round(x0), round(y0),
|
||||
round(x1 - x0), round(y1 - y0), "" if rot else "ROT",
|
||||
col)
|
||||
pending = None
|
||||
|
||||
|
||||
def main():
|
||||
log = sys.argv[1]
|
||||
out = None
|
||||
if "--csv" in sys.argv:
|
||||
out = open(sys.argv[sys.argv.index("--csv") + 1], "w")
|
||||
out.write("frame,draw,x,y,w,h,rot,col\n")
|
||||
for row in quads(log):
|
||||
line = "%d,%d,%d,%d,%d,%d,%s,%s" % row
|
||||
if out:
|
||||
out.write(line + "\n")
|
||||
else:
|
||||
print(line)
|
||||
if out:
|
||||
out.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
87
tools/re-capture/screen_build_capture.sh
Executable file
87
tools/re-capture/screen_build_capture.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Catch a screen being BUILT, not sitting still.
|
||||
#
|
||||
# `log_ui_draws` armed at the title only ever sees the steady state, and the
|
||||
# steady state is the one thing an animation question cannot be asked of. The
|
||||
# build — the wordmarks zooming in, the effect sprites wiping across, the black
|
||||
# fade lifting — happens in the seconds BEFORE the screen classifier can call it
|
||||
# a title.
|
||||
#
|
||||
# So arm repeatedly and keep every log: each F10 opens a new numbered file and
|
||||
# CLOSES the previous one (which stays on disk, complete). Re-arming every few
|
||||
# seconds through the boot therefore tiles the whole approach to the title, and
|
||||
# whichever file happens to straddle the build contains it. Stop re-arming the
|
||||
# instant the title is classified, so a press cannot land mid-build and abandon
|
||||
# the one file that matters.
|
||||
#
|
||||
# NO pad input at all. Ⓐ during the boot has ended a run on a permanent black
|
||||
# screen (docs/re/canary-scripted-input-traps.md), and nothing here needs it.
|
||||
#
|
||||
# Usage: screen_build_capture.sh [out_dir] [timeout_s]
|
||||
# FRAMES= frames per capture window (default 1200 ~ 20 s at 60 Hz)
|
||||
# MAXDRAWS= hard stop per capture (default 80000)
|
||||
# REARM= seconds between F10 presses (default 10)
|
||||
set -u
|
||||
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
|
||||
SD="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT="${1:-/sylph-home/re/buildcap}"
|
||||
TIMEOUT="${2:-600}"
|
||||
mkdir -p "$OUT"; rm -f "$OUT"/xenia_re_ui_draws_*.log
|
||||
|
||||
alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; }
|
||||
shot(){ screenshot "$1" >/dev/null 2>&1; }
|
||||
wide(){ [ "$(identify -format '%w' "$1" 2>/dev/null || echo 0)" -gt 1000 ]; }
|
||||
|
||||
( cd "$OUT" && nohup run-canary \
|
||||
--ui_draw_capture_frames="${FRAMES:-1200}" \
|
||||
--ui_draw_capture_max="${MAXDRAWS:-80000}" \
|
||||
--create_profile_if_none="${SYLPH_TAG:-SylphRE}" \
|
||||
--logged_profile_slot_0_xuid="${SYLPH_XUID:-B13EBABEBABEBABE}" \
|
||||
>"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & )
|
||||
sleep 8
|
||||
until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do
|
||||
[ -n "$(alive)" ] || { echo "EMULATOR GONE before the window appeared"; exit 4; }
|
||||
sleep 1
|
||||
done
|
||||
win="$(xdotool search --name "Xenia-canary" | tail -1)"
|
||||
echo "WINDOW=$win"
|
||||
|
||||
arm(){
|
||||
xdotool windowactivate "$win" 2>/dev/null
|
||||
xdotool key --window "$win" F10 2>/dev/null
|
||||
xdotool key F10 2>/dev/null
|
||||
}
|
||||
|
||||
last_arm=-999
|
||||
deadline=$(( SECONDS + TIMEOUT ))
|
||||
s=none
|
||||
while [ $SECONDS -lt $deadline ]; do
|
||||
[ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s"; exit 4; }
|
||||
shot /tmp/sbc.png
|
||||
if [ -s /tmp/sbc.png ]; then
|
||||
wide /tmp/sbc.png || { echo "GRAB IS NOT THE GAME SURFACE at ${SECONDS}s"; exit 6; }
|
||||
s="$(python3 "$SD/screen_id.py" /tmp/sbc.png | awk '{print $1}')"
|
||||
fi
|
||||
if [ "$s" = "${WANT:-title}" ]; then
|
||||
echo "t=${SECONDS}s $s <- STOP re-arming"
|
||||
cp /tmp/sbc.png "$OUT/reached.png"
|
||||
break
|
||||
fi
|
||||
if [ $(( SECONDS - last_arm )) -ge "${REARM:-10}" ]; then
|
||||
arm; last_arm=$SECONDS
|
||||
echo "t=${SECONDS}s $s (armed)"
|
||||
else
|
||||
echo "t=${SECONDS}s $s"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Let the straddling window run itself out rather than cutting it short.
|
||||
echo "waiting for the in-flight capture to close..."
|
||||
for _ in $(seq 1 40); do
|
||||
grep -q "UI-CAP. done" "$OUT/canary.stdout" 2>/dev/null && break
|
||||
sleep 1
|
||||
done
|
||||
grep -i "UI-CAP" "$OUT/canary.stdout" | tail -6
|
||||
ls -l "$OUT"/xenia_re_ui_draws_*.log 2>/dev/null || echo "NO CAPTURE LOG"
|
||||
echo "SCREEN BUILD CAPTURE DONE (screen=$s, emulator left running)"
|
||||
Reference in New Issue
Block a user