From 8ebbfc70c1d19d104de015968988dbf00264d224 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sun, 30 Aug 2026 21:54:32 +0000 Subject: [PATCH] re: the focus reader was two items out -- initial focus is NEW GAME, and my labels were wrong menu_focus.py's row centres are design-space rows from screenshot output; my probes fed it whole-display x11grab frames carrying Xenia's chrome and a surface scaled 1.060. Caught by ground truth, not by a control: the probe announced 'on EXTRAS', pressed A, and opened OPTIONS. Measured directly with ring_row.py: initial focus on a fresh boot is NEW GAME, 2/2 fresh boots, both the first menu entry. That agrees with boot_menu.sh's own line and menu-state-in-memory.md's four-downs, and withdraws this page's 'TUTORIAL 2/2' as the outlier. Persistence stands and is now geometry-free -- 384.0 vs 385.5, 1.5 px apart. An equality test is immune to a constant offset, which is why the conclusion survived a broken reader when the published item names did not. The control was structurally blind: 'two DOWNs move two items' tests relative motion, and a constant offset preserves it exactly. EXTRAS remains unmeasured; that run navigated to OPTIONS. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- docs/port/HANDOFF.md | 30 +++ docs/re/data/menu-focus-reader-offset.txt | 60 ++++++ docs/re/menu-navigation-semantics.md | 34 ++++ tools/re-capture/extras_focus_persistence.py | 185 +++++++++++++++++++ tools/re-capture/extras_focus_session.sh | 25 +++ tools/re-capture/ring_row.py | 78 ++++++++ 6 files changed, 412 insertions(+) create mode 100644 docs/re/data/menu-focus-reader-offset.txt create mode 100644 tools/re-capture/extras_focus_persistence.py create mode 100755 tools/re-capture/extras_focus_session.sh create mode 100755 tools/re-capture/ring_row.py diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 084bf188..f08c542c 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -11,6 +11,36 @@ delivered. + +## 🔴 2026-08-30 (later) — correcting today's focus delivery: the names were wrong + +**Read this before the persistence section above.** Two things change, one of +them a value you may have taken. + +* 🔴 **The item names I gave for the persistence run were two positions out.** + Reported `TUTORIAL → EXTRAS → EXTRAS`; the truth is + **`NEW GAME → TUTORIAL → TUTORIAL`**. My focus reader used design-space rows + against whole-display captures that carry Xenia's window chrome and a surface + scaled 1.060. +* ✅ **The persistence conclusion is unchanged and is now geometry-free** — the + ring sits at y 384.0 before the round trip and 385.5 after, 1.5 px apart. An + equality test is immune to a constant offset, which is exactly why the + conclusion survived a reader the labels did not. +* ✅ **Initial focus on a fresh boot is `NEW GAME`** — measured directly, 2/2 + fresh boots, both the *first* menu entry. **Your authored `NEW GAME` is now a + measured value.** I said earlier today that nothing confirmed it; that is no + longer true. +* ⚠️ **My control could not have caught this.** "Two DOWNs move two items" tests + relative motion, which a constant offset preserves. It passed on a reader two + items wrong. + +❔ **EXTRAS is still unmeasured** — the run that was to settle it navigated to +OPTIONS believing it was EXTRAS, so nothing about EXTRAS was observed and your +`initial_focus: ptbtn11` label remains undecided. + +[data](../re/data/menu-focus-reader-offset.txt) + + ## ✅ 2026-08-30 — the main menu remembers its cursor; re-entry is not a reset **measured, and it changes what `on_cancel` should do.** Ⓑ from the menu to the diff --git a/docs/re/data/menu-focus-reader-offset.txt b/docs/re/data/menu-focus-reader-offset.txt new file mode 100644 index 00000000..682c435d --- /dev/null +++ b/docs/re/data/menu-focus-reader-offset.txt @@ -0,0 +1,60 @@ +# The focus reader was wrong on x11grab frames -- and what that changes. +# MEASURED 2026-08-30. +# +# menu_focus.py's row centres YS = [166,241,315,390,465] are DESIGN-SPACE rows, +# derived from `screenshot` captures. My probes feed it ffmpeg x11grab frames of +# the whole X display, which include Xenia's title bar and menu bar and show the +# game surface SCALED. Same numbers, different coordinate system. +# +# CAUGHT BY GROUND TRUTH, not by a control: the probe announced "on EXTRAS", +# pressed A, and opened OPTIONS. I looked at the frame. +# +# THE OFFSET, measured directly rather than assumed -- brightest gutter cluster +# (x 500:542), excluding the y<50 window-decoration band and the footer: +# +# reach/1-F1.png ring y = 225.5 (first menu entry of a fresh boot) +# reach/2-F2.png ring y = 384.0 (after 2 delivery-confirmed DOWNs) +# reach/4-F3.png ring y = 385.5 (after B -> title -> A -> menu) +# +# spacing = (384.0 - 225.5) / 2 = 79.25 px per item +# design spacing is 74.75, so the surface is scaled by 1.060 -- UP, not down +# capture_y = 49.5 + 1.060 * design_y (checks: 241->305 vs 304.75 seen, +# 315->383.4 vs 384.0 seen) +# +# ⚠️ MY FIRST CORRECTION WAS ALSO WRONG. I assumed the 1280x675-inside-1280x720 +# note meant the surface was scaled DOWN by 0.9375. Applying that gave F2 = +# OPTIONS, i.e. three rows below F1 where only two DOWNs were delivered -- the +# arithmetic refused it. Measuring the spacing settled it in one step. +# +################################################################################ +# WHAT THIS CHANGES +# +# ✅ INITIAL FOCUS ON A FRESH BOOT IS **NEW GAME**, not TUTORIAL. +# F1's ring at y=225.5 is design row 166 = item 0. Two fresh boots, both the +# FIRST menu entry of the boot, both NEW GAME (this run, and the earlier run +# whose old-reader value of LOAD GAME is the same frame misread). +# +# That AGREES with boot_menu.sh's own closing line and with +# menu-state-in-memory.md's four-downs-to-EXTRAS, which only counts from NEW +# GAME. menu-navigation-semantics.md's "TUTORIAL, 2/2" is the outlier. +# +# ✅ THE PERSISTENCE FINDING STANDS, and is now confirmed WITHOUT any geometry: +# F2 ring y = 384.0, F3 = 385.5. 1.5 px apart. The cursor is where it was +# left. An equality test is immune to a constant offset, which is exactly why +# that conclusion survived a broken reader. +# +# 🔴 BUT THE ITEM NAMES I PUBLISHED FOR IT WERE WRONG. +# Reported: F1 TUTORIAL -> F2 EXTRAS -> F3 EXTRAS. +# Truth: F1 NEW GAME -> F2 TUTORIAL -> F3 TUTORIAL. +# Two positions out. The conclusion does not change; the labels do. +# +# 🔴 AND MY CONTROL COULD NOT HAVE CAUGHT IT. "Two DOWNs must move the cursor +# exactly two items" tests RELATIVE motion, and a constant offset preserves +# relative motion exactly. A control that only checks differences is blind to +# every error in the origin. It passed on a reader that was two items out. +# +# ❔ THE EXTRAS QUESTION IS STILL UNANSWERED. The probe navigated to OPTIONS +# believing it was EXTRAS, so E1/E2/E3 are the OPTIONS screen and a different +# screen respectively (E3 glyph 1776 is the title's plate band -- A on the +# wrong screen went somewhere else entirely). Nothing about EXTRAS was +# measured. sylpheed-port's `initial_focus: ptbtn11` label is still undecided. diff --git a/docs/re/menu-navigation-semantics.md b/docs/re/menu-navigation-semantics.md index 251c578d..98b8687b 100644 --- a/docs/re/menu-navigation-semantics.md +++ b/docs/re/menu-navigation-semantics.md @@ -30,6 +30,40 @@ with [`tools/re-capture/menu_focus.py`](../../tools/re-capture/menu_focus.py). Wrap holds on both screens tested — the 5-item main menu and the 3-item `EXTRAS` submenu — so it is a menu rule, not a per-screen table. +## 🔴 2026-08-30 (later) — the item NAMES below were wrong, and initial focus is NEW GAME + +**The reader was broken and my control could not see it.** `menu_focus.py`'s row +centres are **design-space** rows read off `screenshot` output; my probes fed it +whole-display `x11grab` frames, which carry Xenia's title bar and menu bar and +show the game surface scaled by **1.060**. Caught by ground truth, not by a +control: the probe announced *"on EXTRAS"*, pressed Ⓐ, and opened **OPTIONS**. + +Ring rows measured directly (`tools/re-capture/ring_row.py`), no assumed geometry: + +| frame | ring y | true item | +|---|---|---| +| first menu entry, fresh boot | **225.5** | **`NEW GAME`** | +| after 2 delivery-confirmed DOWNs | 384.0 | `TUTORIAL` | +| after Ⓑ → title → Ⓐ → menu | **385.5** | `TUTORIAL` | + +* ✅ **Initial focus on a fresh boot is `NEW GAME`** — 2/2 fresh boots, both the + *first* menu entry. That agrees with `boot_menu.sh`'s own closing line and with + [`menu-state-in-memory.md`](menu-state-in-memory.md)'s four-downs-to-`EXTRAS`, + which only counts from `NEW GAME`. ~~"initial focus, main menu — TUTORIAL"~~ in + the Q5 table below is **withdrawn**: it is the outlier, and this reading is the + direct one. +* ✅ **Persistence stands, and is now geometry-free**: 384.0 vs 385.5, 1.5 px + apart. An equality test is immune to a constant offset, which is why that + conclusion survived a broken reader when the labels did not. +* 🔴 **The names I published for it were two positions out** — reported + `TUTORIAL → EXTRAS → EXTRAS`, truth `NEW GAME → TUTORIAL → TUTORIAL`. +* ⚠️ **The control was structurally blind to this.** "Two DOWNs must move the + cursor exactly two items" tests *relative* motion, and a constant offset + preserves relative motion exactly. **A control that only checks differences + cannot see an error in the origin.** + +[data](data/menu-focus-reader-offset.txt) + ## ✅ 2026-08-30 — the menu REMEMBERS its cursor across menu → title → menu **measured** — [`data/focus-persists-across-title.txt`](data/focus-persists-across-title.txt), diff --git a/tools/re-capture/extras_focus_persistence.py b/tools/re-capture/extras_focus_persistence.py new file mode 100644 index 00000000..f51aded1 --- /dev/null +++ b/tools/re-capture/extras_focus_persistence.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Does the EXTRAS submenu remember its cursor across leave -> re-enter? + +sylpheed-port's contract-check asserted that EXTRAS does NOT persist. Nothing +measured that. The corpus has EXTRAS' initial focus as MISSION SELECT from a +SINGLE entry, and Ⓑ-from-a-submenu restoring the PARENT's focus 4/4 -- neither +says what a submenu's own cursor does on re-entry. Their label +`initial_focus: ptbtn11, kind: "measured"` is exposed to the same history problem +raised for the main menu: taken on one entry, it may be reading history. + +The main menu was measured to PERSIST (focus-persists-across-title.txt), so the +question is live in both directions. + +⚠️ NO HAND-GUESSED GEOMETRY. menu_focus.py's ys are the five-item main menu and +do not transfer to a three-item submenu, and the only EXTRAS capture in the +corpus is a 562x182 crop. So the ring is located by DIFFERENCE instead: the +pixels that change when the cursor moves ARE the ring's footprint. Persistence is +then decided inside that mask, which needs no coordinates at all. + +🔴 CONTROL, and the run stops on it: the E1->E2 difference must be ONE COMPACT +CLUSTER. If moving the cursor changes the whole frame, the mask is not the ring +and nothing after it may be read. (sylpheed-port measured one 46x44 cluster, +0.155 % of frame, across a full ring cycle on their renderer -- an independent +expectation for what this should look like.) + + extras_focus_persistence.py LOG OUTDIR [wait_s] +""" +import os, re, subprocess, sys, time +import numpy as np +from PIL import Image + +LOG, OUT = sys.argv[1], sys.argv[2] +WAIT = float(sys.argv[3]) if len(sys.argv) > 3 else 600 +W, H = 1280, 720 +MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6 +PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py") +NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"] +YS = [166, 241, 315, 390, 465] +T0 = time.time() + + +def deliveries(vk): + pat = re.compile((r"RE-INPUT\] XamInputGetKeystrokeEx -> user=\d+ vk=%s flags=0001" % vk).encode()) + try: + return len(pat.findall(open(LOG, "rb").read())) + except FileNotFoundError: + return 0 + + +def press(btn, vk, tries=5): + for k in range(tries): + before = deliveries(vk) + subprocess.run([sys.executable, PAD, "tap", btn, "0.5"], check=False) + for _ in range(20): + time.sleep(0.25) + if deliveries(vk) > before: + print(f"[{time.time()-T0:7.1f}s] {btn} delivered", flush=True) + return True + print(f"[{time.time()-T0:7.1f}s] {btn} NOT delivered (attempt {k+1})", flush=True) + return False + + +def _open(): + return subprocess.Popen( + ["ffmpeg", "-loglevel", "error", "-f", "x11grab", "-draw_mouse", "0", + "-video_size", f"{W}x{H}", "-i", ":98", "-r", "4", + "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], + stdout=subprocess.PIPE, bufsize=W * H * 3 * 2) + + +def fresh(): + """A CURRENT frame. Reading the pipe after a sleep returns a buffered one.""" + q = _open(); a = None + for _ in range(3): + buf = q.stdout.read(W * H * 3) + if len(buf) == W * H * 3: + a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(int) + q.kill() + return a + + +def glyph(a): + r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2] + return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum()) + + +def mainfocus(a): + g = np.asarray(Image.fromarray(a.astype(np.uint8)).convert("L"), dtype=float) + v = [g[y - 20:y + 20, 500:542].max() for y in YS] + return int(np.argmax(v)), v + + +def wait_menu(limit=90): + t = time.time(); streak = 0 + while time.time() - t < limit: + a = fresh() + if a is None: + continue + if MENU_LO <= glyph(a) <= MENU_HI: + streak += 1 + if streak >= 2: + return a + else: + streak = 0 + return None + + +os.makedirs(OUT, exist_ok=True) + +# ---- 1. on the main menu, walk to EXTRAS ------------------------------------ +a = wait_menu() +if a is None: + sys.exit("never saw the main menu") +f, v = mainfocus(a) +print(f"[{time.time()-T0:7.1f}s] MAIN MENU focus = {NAMES[f]} ring " + + " ".join(f"{x:5.0f}" for x in v), flush=True) +need = (4 - f) % 5 +print(f" {need} DOWN to reach EXTRAS", flush=True) +for _ in range(need): + if not press("DOWN", "5811"): + sys.exit("a DOWN was never delivered") + time.sleep(0.8) +time.sleep(1.5) +a = fresh(); f2, v2 = mainfocus(a) +print(f" now on {NAMES[f2]} ring " + " ".join(f"{x:5.0f}" for x in v2), flush=True) +if f2 != 4: + sys.exit(f"CONTROL FAILED: wanted EXTRAS, on {NAMES[f2]}") +print("✅ on EXTRAS", flush=True) + +# ---- 2. enter EXTRAS, capture E1, move, capture E2 -------------------------- +if not press("A", "5800"): + sys.exit("A never delivered") +time.sleep(6.0) +E1 = fresh(); Image.fromarray(E1.astype(np.uint8)).save(f"{OUT}/E1.png") +print(f"[{time.time()-T0:7.1f}s] IN EXTRAS (glyph {glyph(E1)}) — E1 captured", flush=True) + +if not press("DOWN", "5811"): + sys.exit("a DOWN was never delivered inside EXTRAS") +time.sleep(2.5) +E2 = fresh(); Image.fromarray(E2.astype(np.uint8)).save(f"{OUT}/E2.png") +print(f"[{time.time()-T0:7.1f}s] E2 captured after 1 DOWN", flush=True) + +# ---- 3. CONTROL: the difference must be one compact cluster ----------------- +d = (np.abs(E1 - E2).max(axis=2) > 24) +frac = d.mean() +ys, xs = np.nonzero(d) +if len(ys) == 0: + sys.exit("🔴 CONTROL FAILED: the cursor did not move at all (E1 == E2)") +bbox = (int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())) +bw, bh = bbox[2] - bbox[0] + 1, bbox[3] - bbox[1] + 1 +dens = len(ys) / (bw * bh) +print(f" diff: {100*frac:.3f}% of frame, bbox {bw}x{bh} at ({bbox[0]},{bbox[1]}), " + f"fill {100*dens:.0f}%", flush=True) +if frac > 0.05: + sys.exit(f"🔴 CONTROL FAILED: {100*frac:.1f}% of the frame changed — that is not a ring") +print("✅ CONTROL PASSED: the move changed one localised region", flush=True) + +# ---- 4. leave, re-enter, capture E3 ---------------------------------------- +if not press("B", "5801"): + sys.exit("B never delivered") +a = wait_menu() +if a is None: + sys.exit("B did not return to the main menu") +f3, _ = mainfocus(a) +print(f"[{time.time()-T0:7.1f}s] back on the MAIN MENU, focus = {NAMES[f3]}", flush=True) +if not press("A", "5800"): + sys.exit("A never delivered on re-entry") +time.sleep(6.0) +E3 = fresh(); Image.fromarray(E3.astype(np.uint8)).save(f"{OUT}/E3.png") +print(f"[{time.time()-T0:7.1f}s] RE-ENTERED EXTRAS — E3 captured", flush=True) + +# ---- 5. decide, inside the ring mask only ---------------------------------- +m = d +d1 = float(np.abs(E3 - E1).max(axis=2)[m].mean()) +d2 = float(np.abs(E3 - E2).max(axis=2)[m].mean()) +print(f"\n inside the ring mask ({int(m.sum())} px):") +print(f" mean |E3 - E1| = {d1:6.2f} (E1 = where EXTRAS opened)") +print(f" mean |E3 - E2| = {d2:6.2f} (E2 = where I left it)") +if d2 < d1 * 0.5: + print("\n=> EXTRAS PERSISTS: re-entry matches where I left the cursor") +elif d1 < d2 * 0.5: + print("\n=> EXTRAS RESETS: re-entry matches where it first opened") +else: + print(f"\n=> UNDECIDED: {d1:.2f} vs {d2:.2f} are too close to separate") +print("EXTRAS FOCUS RUN DONE", flush=True) diff --git a/tools/re-capture/extras_focus_session.sh b/tools/re-capture/extras_focus_session.sh new file mode 100755 index 00000000..b70946cc --- /dev/null +++ b/tools/re-capture/extras_focus_session.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Boot and run extras_focus_persistence.py. Plate-pulse path, not skip_intro. +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +SD="$(cd "$(dirname "$0")" && pwd)" +OUT="${OUT:-/sylph-home/re/extrasfocus}"; mkdir -p "$OUT" +LOG="$OUT/canary.stdout" +bash "$SD/ensure_single_emulator.sh" +if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then + rm -f "/tmp/.X${DISPLAY#:}-lock" 2>/dev/null || true + nohup bash -c 'Xvfb "$0" -screen 0 1280x720x24 -ac -nolisten tcp \ + +extension GLX +extension RANDR >/tmp/xvfb98.log 2>&1' "$DISPLAY" /dev/null 2>&1 & + for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; done + nohup env DISPLAY="$DISPLAY" HOME=/sylph-home openbox /tmp/openbox98.log 2>&1 & +fi +XUID="${SYLPH_XUID:-$(ls "${XENIA_CONTENT:-$HOME/.local/share/Xenia/content}" 2>/dev/null | head -1)}" +[ -n "$XUID" ] || { echo "NO PROFILE"; exit 2; } +echo "── EFFECTIVE CONFIG ──"; echo " out=$OUT profile=$XUID gate=plate pulse" +cd /sylph-home/re +nohup run-canary --apu=sdl --log_mask=13 --log_level=2 \ + --logged_profile_slot_0_xuid="$XUID" "$LOG" 2>&1 & +# reach the title and the menu with the probe that already does it +python3 "$SD/focus_persistence.py" "$LOG" "$OUT/reach" 900 >"$OUT/reach.log" 2>&1 +echo "-- reached the menu; now the EXTRAS question --" +python3 "$SD/extras_focus_persistence.py" "$LOG" "$OUT" 600 diff --git a/tools/re-capture/ring_row.py b/tools/re-capture/ring_row.py new file mode 100755 index 00000000..d30733a1 --- /dev/null +++ b/tools/re-capture/ring_row.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Locate the menu focus ring by MEASUREMENT, in whatever frame you have. + +🔴 Why this exists. `menu_focus.py`'s row centres [166,241,315,390,465] are +DESIGN-SPACE rows read off `screenshot` output. Feeding it an ffmpeg x11grab +frame of the whole X display silently reads the wrong rows: the frame carries +Xenia's title bar and menu bar, and the game surface is scaled. On 2026-08-30 a +probe announced "on EXTRAS", pressed Ⓐ, and opened OPTIONS -- two items out. + +⚠️ AND THE CONTROL COULD NOT CATCH IT. "Two DOWNs must move the cursor two +items" tests RELATIVE motion, which a constant offset preserves exactly. It +passed on a reader that was two items wrong. So this returns the ring's measured +ROW, and callers compare rows; naming an item needs a calibration, below. + +Measured on the x11grab frames of 2026-08-30: + spacing 79.25 px per item (design spacing 74.75 -> surface scaled 1.060) + capture_y = 49.5 + 1.060 * design_y +Checks: design 241 -> 305 predicted vs 304.75 measured; 315 -> 383.4 vs 384.0. + + ring_row.py FRAME.png [FRAME.png ...] +""" +import sys +import numpy as np +from PIL import Image + +GUTTER = (500, 542) +DECOR_ROWS = 50 # window title bar + menu bar live above this +FOOTER_Y = 620 # the button-legend strip is bright in the gutter too +NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"] +ROW0, SPACING = 225.5, 79.25 # measured, main menu, x11grab + + +def ring_row(img): + """The ring's y centre in THIS frame's own pixels, or None.""" + g = np.asarray(img.convert("L"), dtype=float) + col = g[:, GUTTER[0]:GUTTER[1]].max(axis=1) + col[:DECOR_ROWS] = 0 + ys = np.nonzero(col > 150)[0] + if len(ys) == 0: + return None + groups, cur = [], [int(ys[0])] + for y in ys[1:]: + if y - cur[-1] <= 4: + cur.append(int(y)) + else: + groups.append(cur) + cur = [int(y)] + groups.append(cur) + groups = [g_ for g_ in groups if len(g_) >= 5 and g_[0] < FOOTER_Y] + if not groups: + return None + b = max(groups, key=len) + return (b[0] + b[-1]) / 2 + + +def main_menu_item(y): + """Item index on the MAIN MENU, from the measured calibration. + + Only valid for the five-item main menu in an x11grab frame. Returns None if + the row is not within half a step of a row centre -- refusing is the point, + since a wrong name is what this file exists to prevent. + """ + if y is None: + return None + i = round((y - ROW0) / SPACING) + if not (0 <= i < len(NAMES)): + return None + if abs(y - (ROW0 + i * SPACING)) > SPACING * 0.4: + return None + return i + + +if __name__ == "__main__": + for p in sys.argv[1:]: + y = ring_row(Image.open(p)) + i = main_menu_item(y) + nm = NAMES[i] if i is not None else "" + print(f"{p.split('/')[-1]:22} ring y = {y} -> {nm}")