adhoc_session.py: Wait in emulated time, and start instances at a speed

wait, press --every, stick, type and until count the instances' emulated
time (the slowest one's), so a script means the same at any speed, and
start --speed PERCENT runs the instances sped up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-31 00:44:36 +01:00
1 parent acb2acf0b4
commit 1f42161f0e
1 file changed
+49 -14
+49 -14
View File
@@ -11,9 +11,11 @@
# python3 Tools/adhoc_session.py run script.txt (lines like the commands above, without the prefix)
# python3 Tools/adhoc_session.py stop
#
# The instances run freely at PSP speed (--realtime, and --lag-sync unless --no-lag-sync), and each one
# gets its own memory stick with a copy of the real one's savedata, so they can't overwrite each other's
# (or your) saves. The firmware comes from the real memory stick's NAND.
# The instances run freely at PSP speed (--realtime, and --lag-sync unless --no-lag-sync), or at
# --speed PERCENT of it, and each one gets its own memory stick with a copy of the real one's savedata,
# so they can't overwrite each other's (or your) saves. The firmware comes from the real memory stick's
# NAND. Times in commands (wait, --every, --timeout, --poll) are emulated seconds, so a script runs the
# same at any speed.
import argparse
import base64
@@ -162,6 +164,34 @@ def selected(session, which):
return [inst for inst in session["instances"] if inst["n"] in wanted]
def emulated_seconds(session):
# The slowest instance's clock: the one a script is waiting for.
times = []
for inst in session["instances"]:
dbg = Debugger(inst["port"])
times.append(dbg.request("cpu.status")["us"] / 1e6)
dbg.close()
return min(times)
def wall_seconds(session, emulated):
# About how long `emulated` seconds take, to pace polling.
return emulated * 100.0 / session.get("speed", 100)
def emulated_sleep(session, seconds):
# Script times are emulated seconds, so a script means the same at any --speed (and when the
# instances run slower than asked). A clock that stops (a paused instance) ends it after a while.
start = emulated_seconds(session)
give_up = time.time() + wall_seconds(session, seconds) * 20 + 30
while time.time() < give_up:
left = seconds - (emulated_seconds(session) - start)
if left <= 0:
return
time.sleep(min(0.05, max(0.005, wall_seconds(session, left))))
print("wait: the emulated clock stopped advancing", file=sys.stderr)
def find_headless():
candidates = glob.glob(os.path.join(ROOT, "build*", "PPSSPPHeadless")) + glob.glob(os.path.join(ROOT, "build*", "PPSSPPHeadless.exe"))
if not candidates:
@@ -206,6 +236,8 @@ def cmd_start(args):
"--appendconfig=" + ini, "--debugger-run=0", "--timeout-wall=%d" % args.timeout]
if not args.no_lag_sync:
cmd.append("--lag-sync")
if args.speed != 100:
cmd.append("--speed=%d" % args.speed)
if n == 1:
cmd.append("--adhoc-server")
cmd.append("--nickname=" + (names[n - 1] if n <= len(names) else "PPSSPP %d" % n))
@@ -232,7 +264,7 @@ def cmd_start(args):
time.sleep(0.1)
if inst["port"] is None:
sys.exit("Instance %d didn't open a debugger port; see %s" % (inst["n"], inst["log"]))
session = {"game": args.game, "instances": instances, "lagSync": not args.no_lag_sync, "p2p": args.p2p}
session = {"game": args.game, "instances": instances, "lagSync": not args.no_lag_sync, "p2p": args.p2p, "speed": args.speed}
with open(session_file(args), "w") as f:
json.dump(session, f, indent=1)
print("Started %d instances (%s), session in %s" % (args.n, "P2P" if args.p2p else "relay", args.dir))
@@ -257,7 +289,7 @@ def cmd_press(args):
session = load_session(args)
for t in range(args.times):
if t > 0:
time.sleep(args.every)
emulated_sleep(session, args.every)
for inst in selected(session, args.i):
dbg = Debugger(inst["port"])
# The answer only comes once the press is over, so don't wait for it.
@@ -284,7 +316,7 @@ def cmd_type(args):
dbg = Debugger(inst["port"])
dbg.send("input.buttons.press", button=button, duration=3)
dbg.close()
time.sleep(0.15)
emulated_sleep(session, 0.15)
row, col = 0, 0
for ch in args.text:
@@ -317,7 +349,7 @@ def cmd_stick(args):
dbg = Debugger(inst["port"])
dbg.request("input.analog.send", x=args.x, y=args.y)
dbg.close()
time.sleep(args.seconds)
emulated_sleep(session, args.seconds)
for inst in insts:
dbg = Debugger(inst["port"])
dbg.request("input.analog.send", x=0, y=0)
@@ -325,7 +357,7 @@ def cmd_stick(args):
def cmd_wait(args):
time.sleep(args.seconds)
emulated_sleep(load_session(args), args.seconds)
def grab_png(inst, attempts=3):
@@ -415,24 +447,26 @@ def cmd_until(args):
sys.exit("until needs a reference image or --pixel")
session = load_session(args)
waiting = list(selected(session, args.i))
end = time.time() + args.timeout
last_press = 0.0
# --timeout, --every and --poll are emulated seconds, like wait's.
start = emulated_seconds(session)
last_press = None
while waiting:
if time.time() > end:
now = emulated_seconds(session)
if now - start > args.timeout:
for inst in waiting:
path = os.path.join(args.dir, "until_failed_%d.png" % inst["n"])
with open(path, "wb") as f:
f.write(grab_png(inst))
sys.exit("until: instances %s never matched; their screens are in until_failed_N.png" % ",".join(str(i["n"]) for i in waiting))
if args.press and time.time() - last_press >= args.every:
if args.press and (last_press is None or now - last_press >= args.every):
for inst in waiting:
dbg = Debugger(inst["port"])
dbg.send("input.buttons.press", button=args.press, duration=6)
dbg.close()
last_press = time.time()
last_press = now
waiting = [inst for inst in waiting if not screen_matches(args, Image.open(io.BytesIO(grab_png(inst))))]
if waiting:
time.sleep(args.poll)
time.sleep(wall_seconds(session, args.poll))
def cmd_crop(args):
@@ -504,6 +538,7 @@ def build_parser():
p.add_argument("--p2p", action="store_true", help="Talk directly instead of through the relay (needs a loopback address per instance)")
p.add_argument("--names", help="Comma-separated nicknames for the instances (default: PPSSPP 1, PPSSPP 2, ...)")
p.add_argument("--no-lag-sync", action="store_true", help="Pace once per frame, like the app's default")
p.add_argument("--speed", type=int, default=100, help="Run at this percentage of PSP speed (scripts count emulated time)")
p.add_argument("--state", help="Savestate to load at boot in every instance")
p.add_argument("--memstick", default=DEFAULT_MEMSTICK, help="Memory stick to take the firmware and savedata from")
p.add_argument("--graphics", default="vulkan")