#!/usr/bin/env bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # verify-psp-input.sh - Input gate for the PSP: the PSP's own controls reach # JoeyLib, pressed through PPSSPPHeadless's input script and read back from # what the examples draw. # # The PSP has no keyboard or mouse and one set of controls: SDL3's PSP # joystick, which the SDL3 backend opens as gamepad 0 (JL_JOYSTICK_0: the # d-pad, cross as button 0, circle as button 1), with the analog nub as the # mouse pointer and L and R as its buttons, Start as Return and Select as # Escape, and Square opening a keyboard drawn over the screen # (src/hosted/psp/pspKeyboard.c) whose d-pad moves and cross presses are # worked out from its own source by tests/gate/pspKeyboardPath.c. Four # runs under PPSSPPHeadless (scripts/lib/pspGate.sh), each from its folder # on a private memory stick with every memory access checked as a PSP # checks it, each driven by an --input script on PPSSPP's emulated clock # (installer/patches/ppsspp): # # JOY (examples/joy) each d-pad direction, two diagonals, each button # and both together, the nub left and up (the stick must rest: the # nub is the mouse's), with a release after every press so a # latched line shows up; then the keyboard opened, # the d-pad and cross pressed (the stick must rest: they are the # keyboard's), the keyboard closed and the d-pad pressed again (the # stick's once more). After each, a snapshot of the PSP's screen, # whose stage tests/gate/joyScreen.c reads both sticks off (stick # B must stay unplugged). Then Select: JOY must end by itself, as # it does on Escape. # KEYS (examples/keys) Start: the Return key's cell lights, and goes out # again on release, and Start has typed one Return, read off KEYS's # row of the characters it received (tests/gate/keysRow.c). Then # Select: KEYS must end by itself. # KEYS again for the mouse, read off KEYS's mouse row (stage row 199, # tests/gate/dosRow.c mouse-at): present, in the middle at first; L, # R, and both; the nub a little off center (inside the dead zone: # no move), then full tilt right, up, and down-left into the # stage's edges and corner, half tilt right for a second (a speed # between, short of the edge), and left again; then the keyboard # opened and moved to the top with L (no click) and the nub tilted # (no move), closed, and the nub moving the pointer again. Then # Select. # KEYS again through the keyboard: Square shows it at the bottom of the # screen and L moves it to the top; F5 held lights KEYS's F5 cell # and let go puts it out; the RetroNet string typed on it (Shift # lights the Shift cell for the colon); Square takes it away, and # KEYS must have received the string, nothing more. Then Select. # # Usage: scripts/verify-psp-input.sh # Requires: PPSSPPHeadless; build/psp/bin/JOY and # build/psp/bin/KEYS (make psp). set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" source "$repo/scripts/lib/pspGate.sh" pspNeed label=verify-psp-input joyDir=$repo/build/psp/bin/JOY keysDir=$repo/build/psp/bin/KEYS for dir in "$joyDir" "$keysDir"; do [ -f "$dir/EBOOT.PBP" ] || { echo "$label: build $(basename "$dir") first (make psp)" >&2; exit 2; } done # Real seconds a run may take: the script is emulated time, which passes # much faster than real time while the program waits. runSeconds=60 # Emulated milliseconds: the first step (the program is up by then), and # from each press to its snapshot and on to the next press. startMs=1500 holdMs=400 stepMs=600 # The input script's steps run on slices of emulated time this long. sliceMs=100 # KEYS's Return cell (examples/keys: column 2, row 4 of 28x26 cells on a # 32x30 pitch from (2, 2)), at its center, on the PSP's screen, where the # backend centers the 320x200 stage at 1:1 in 480x272. screenSize=480x272 stageX=80 stageY=36 returnX=$((stageX + 2 + 2 * 32 + 14)) returnY=$((stageY + 2 + 4 * 30 + 13)) returnLit="0 255 0" returnUnlit="51 51 51" # KEYS's row of every character it received (stage row 198), which must # hold the one Return that Start typed. typedRow=$((stageY + 198)) typedWant=$'\r' rest=10000 unplugged=00000 # A stick at rest under the keyboard, which covers the patch that says # whether it is there (joyScreen reads the stage where the backend puts # it, so it needs no patch to find it). covered=?0000 # name, input script action, stick A's state (joyScreen's characters: # connected, X, Y, button 0, button 1), and stick B's (unplugged unless # given). joySteps=( "rest|buttons none|$rest" "left|buttons left|1-000" "release|buttons none|$rest" "right|buttons right|1+000" "release|buttons none|$rest" "up|buttons up|10-00" "release|buttons none|$rest" "down|buttons down|10+00" "release|buttons none|$rest" "up-left|buttons up left|1--00" "release|buttons none|$rest" "down-right|buttons down right|1++00" "release|buttons none|$rest" "cross|buttons cross|10010" "release|buttons none|$rest" "circle|buttons circle|10001" "release|buttons none|$rest" "cross+circle|buttons cross circle|10011" "release|buttons none|$rest" "nub left|analog -1 0|$rest" "nub centered|analog 0 0|$rest" "nub up|analog 0 1|$rest" "nub centered|analog 0 0|$rest" "keyboard open|buttons square|$covered|$covered" "release|buttons none|$covered|$covered" "keyboard left|buttons left|$covered|$covered" "release|buttons none|$covered|$covered" "keyboard cross|buttons cross|$covered|$covered" "release|buttons none|$covered|$covered" "keyboard closed|buttons square|$rest" "release|buttons none|$rest" "left again|buttons left|1-000" "release|buttons none|$rest" ) # The mouse: name, input script action, emulated milliseconds until its # snapshot (the next action comes a slice later), and KEYS's mouse row then # ("FLAGS X Y": flags 1000 present, 0100 left, 0010 right; an X of LO-HI is # a range), or - where the keyboard covers the row. Full tilt moves the # pointer 320 stage pixels a second, so a second or more reaches any edge; # half tilt some 130 a second. mouseSteps=( "start|buttons none|300|1000 160 100" "L|buttons ltrigger|300|1100 160 100" "R|buttons rtrigger|300|1010 160 100" "L+R|buttons ltrigger rtrigger|300|1110 160 100" "release|buttons none|300|1000 160 100" "dead zone|analog 0.1 -0.1|1000|1000 160 100" "right|analog 1 0|1500|1000 319 100" "up|analog 0 1|1000|1000 319 0" "down-left|analog -1 -1|2000|1000 0 199" "half right|analog 0.5 0|1000|1000 90-170 199" "left|analog -1 0|1000|1000 0 199" "centered|analog 0 0|300|1000 0 199" "keyboard open|buttons square|300|-" "keyboard L|buttons ltrigger|300|1000 0 199" "keyboard nub|analog 1 -1|500|1000 0 199" "keyboard nub centered|analog 0 0|300|1000 0 199" "keyboard closed|buttons square|300|1000 0 199" "nub again|analog 1 0|1500|1000 319 199" ) # The keyboard over KEYS: its border's color (COLOR_BOARD, which the PSP's # 16-bit textures keep) at the screen's bottom (where Square opens it) and # top (where L puts it), black where it is not; how # long a d-pad press and a cross press last (short of the keyboard's repeat) # and how long after each the next comes; KEYS's F5 cell (column 4, row 5) # and its left Shift cell (column 7, row 3). kbdSource=$repo/src/hosted/psp/pspKeyboard.c kbdTarget="192.168.1.10:6510" kbdBorder="33 33 33" kbdNone="0 0 0" kbdX=240 kbdBottomY=270 kbdTopY=1 kbdPressMs=40 kbdStepMs=100 kbdHoldMs=300 f5X=$((stageX + 2 + 4 * 32 + 14)) f5Y=$((stageY + 2 + 5 * 30 + 13)) shiftX=$((stageX + 2 + 7 * 32 + 14)) shiftY=$((stageY + 2 + 3 * 30 + 13)) work=$(mktemp -d -t joeylib-psp-input.XXXXXX) kbdShots=$work/kbd/shots fails="" cleanup() { pspStop rm -rf "$work" } trap cleanup EXIT # fail WHAT -- note a failed check; the gate goes on to report the rest. fail() { echo "$label: FAIL - $1" fails="${fails:+$fails; }$1" } # runScript DIR NAME -- run the program folder DIR from $work/NAME with # $work/NAME/input.txt; fails unless it ended by itself, cleanly. runScript() { local run="$work/$2" local folder folder=$(pspStage "$run" "$1") pspRun "$run" "$folder" "$runSeconds" --input="$run/input.txt" pspWait pspStrictCheck "$run" "$1" || fail "$2: a bad memory access" if grep -q '^TIMEOUT' "$run/ppsspp.log"; then fail "$2: did not end on Select (Escape)" fi } # JOY: each step's action, then its snapshot holdMs later; Select last. mkdir -p "$work/joy/shots" at=$startMs i=0 for step in "${joySteps[@]}"; do IFS='|' read -r name action want wantB <<< "$step" printf '%d %s\n%d snapshot %s/joy/shots/%02d.png\n' "$at" "$action" "$((at + holdMs))" "$work" "$i" >> "$work/joy/input.txt" at=$((at + stepMs)) i=$((i + 1)) done printf '%d buttons select\n' "$at" >> "$work/joy/input.txt" runScript "$joyDir" joy i=0 for step in "${joySteps[@]}"; do IFS='|' read -r name action want wantB <<< "$step" wantB=${wantB:-$unplugged} png=$(printf '%s/joy/shots/%02d.png' "$work" "$i") i=$((i + 1)) if [ ! -f "$png" ]; then fail "JOY $name: no snapshot" continue fi got=$("$(joeyTool joyScreen)" "$png" --scale 1 --at "$stageX,$stageY") echo "$label: JOY $name: $got" [ "${got% scale=*}" = "A=$want B=$wantB" ] || fail "JOY $name: read ${got% scale=*}, want A=$want B=$wantB" done # KEYS: Start held, then let go, then Select. mkdir -p "$work/keys/shots" { printf '%d buttons start\n' "$startMs" printf '%d snapshot %s/keys/shots/start.png\n' "$((startMs + holdMs))" "$work" printf '%d buttons none\n' "$((startMs + stepMs))" printf '%d snapshot %s/keys/shots/release.png\n' "$((startMs + stepMs + holdMs))" "$work" printf '%d buttons select\n' "$((startMs + 2 * stepMs))" } > "$work/keys/input.txt" runScript "$keysDir" keys for shot in "start|$returnLit" "release|$returnUnlit"; do IFS='|' read -r name want <<< "$shot" png=$work/keys/shots/$name.png if [ ! -f "$png" ]; then fail "KEYS $name: no snapshot" continue fi got=$("$(joeyTool pngProbe)" pixel "$png" "$returnX" "$returnY") echo "$label: KEYS $name: Return cell $got" [ "$got" = "$want" ] || fail "KEYS $name: the Return cell is $got, want $want" typed=$("$(joeyTool keysRow)" "$png" "$typedWant" --size "$screenSize" --at "$stageX,$typedRow" --scale 1) || fail "KEYS $name: Start did not type one Return" echo "$label: KEYS $name: ${typed//$'\n'/; }" done # mouseMatch GOT WANT -- KEYS's mouse row GOT ("FLAGS X Y") is WANT, whose X # may be a range LO-HI. mouseMatch() { local flags local x local y local wantFlags local wantX local wantY read -r flags x y <<< "$1" read -r wantFlags wantX wantY <<< "$2" if [[ $wantX == *-* ]]; then (( x >= ${wantX%-*} && x <= ${wantX#*-} )) || return 1 elif [ "$x" != "$wantX" ]; then return 1 fi [ "$flags $y" = "$wantFlags $wantY" ] } # KEYS for the mouse: each step's action, then its snapshot; Select last. mkdir -p "$work/mouse/shots" at=$startMs i=0 for step in "${mouseSteps[@]}"; do IFS='|' read -r name action ms want <<< "$step" printf '%d %s\n%d snapshot %s/mouse/shots/%02d.png\n' "$at" "$action" "$((at + ms))" "$work" "$i" >> "$work/mouse/input.txt" at=$((at + ms + sliceMs)) i=$((i + 1)) done printf '%d buttons select\n' "$at" >> "$work/mouse/input.txt" runScript "$keysDir" mouse i=0 for step in "${mouseSteps[@]}"; do IFS='|' read -r name action ms want <<< "$step" png=$(printf '%s/mouse/shots/%02d.png' "$work" "$i") i=$((i + 1)) if [ "$want" = - ]; then continue fi if [ ! -f "$png" ]; then fail "MOUSE $name: no snapshot" continue fi if ! got=$("$(joeyTool dosRow)" mouse-at "$png" "$stageX,$stageY"); then fail "MOUSE $name: no mouse row on the screen" continue fi echo "$label: MOUSE $name: $got" mouseMatch "$got" "$want" || fail "MOUSE $name: read $got, want $want" done # KEYS through the keyboard: open, to the top, F5 held and let go, the # string typed, closed. mkdir -p "$kbdShots" at=$startMs kbd() { printf '%d %s\n' "$1" "$2" >> "$work/kbd/input.txt" } kbd "$at" "buttons square" kbd "$((at + kbdPressMs))" "buttons none" kbd "$((at + kbdHoldMs))" "snapshot $kbdShots/open.png" at=$((at + 2 * kbdHoldMs)) kbd "$at" "buttons ltrigger" kbd "$((at + kbdPressMs))" "buttons none" kbd "$((at + kbdHoldMs))" "snapshot $kbdShots/top.png" at=$((at + 2 * kbdHoldMs)) while read -r step what; do if [ "$step" = move ]; then kbd "$at" "buttons $what" kbd "$((at + kbdPressMs))" "buttons none" at=$((at + kbdStepMs)) continue fi kbd "$at" "buttons cross" case "$what" in F5) kbd "$((at + kbdHoldMs))" "snapshot $kbdShots/f5Held.png" kbd "$((at + 2 * kbdHoldMs))" "buttons none" kbd "$((at + 3 * kbdHoldMs))" "snapshot $kbdShots/f5Up.png" at=$((at + 4 * kbdHoldMs)) ;; SHIFT) kbd "$((at + kbdPressMs))" "buttons none" kbd "$((at + kbdHoldMs))" "snapshot $kbdShots/shift.png" at=$((at + 2 * kbdHoldMs)) ;; *) kbd "$((at + kbdPressMs))" "buttons none" at=$((at + kbdStepMs)) ;; esac done < <("$(joeyTool pspKeyboardPath)" "$kbdSource" key:F5 "type:$kbdTarget") kbd "$at" "buttons square" kbd "$((at + kbdPressMs))" "buttons none" kbd "$((at + kbdHoldMs))" "snapshot $kbdShots/closed.png" kbd "$((at + 2 * kbdHoldMs))" "buttons select" runScript "$keysDir" kbd # pixelIs SHOT X Y WANT WHAT -- the snapshot's pixel at X,Y must be WANT. pixelIs() { local got if [ ! -f "$kbdShots/$1.png" ]; then fail "keyboard $1: no snapshot" return fi got=$("$(joeyTool pngProbe)" pixel "$kbdShots/$1.png" "$2" "$3") echo "$label: keyboard $1: $5 $got" [ "$got" = "$4" ] || fail "keyboard $1: $5 is $got, want $4" } pixelIs open "$kbdX" "$kbdBottomY" "$kbdBorder" "the bottom of the screen" pixelIs top "$kbdX" "$kbdTopY" "$kbdBorder" "the top of the screen" pixelIs top "$kbdX" "$kbdBottomY" "$kbdNone" "the bottom of the screen" pixelIs f5Held "$f5X" "$f5Y" "$returnLit" "KEYS's F5 cell" pixelIs f5Up "$f5X" "$f5Y" "$returnUnlit" "KEYS's F5 cell" pixelIs shift "$shiftX" "$shiftY" "$returnLit" "KEYS's Shift cell" pixelIs closed "$kbdX" "$kbdTopY" "$kbdNone" "the top of the screen" if [ -f "$kbdShots/closed.png" ]; then typed=$("$(joeyTool keysRow)" "$kbdShots/closed.png" "$kbdTarget" --size "$screenSize" --at "$stageX,$typedRow" --scale 1) || fail "keyboard: KEYS did not receive \"$kbdTarget\"" echo "$label: keyboard typed: ${typed//$'\n'/; }" fi if [ -n "$fails" ]; then echo "$label: FAIL - $fails" >&2 exit 1 fi echo "$label: PASS (JOY: ${#joySteps[@]} d-pad, button, and nub states read back from the screen, the nub leaving the stick alone, stick B unplugged, resting while the keyboard is open; Start held KEYS's Return and typed one; MOUSE: ${#mouseSteps[@]} nub and L/R states, the dead zone, the edges, a half-tilt speed, and the pointer still under the keyboard; the keyboard shown, moved, F5 held and \"$kbdTarget\" typed on it; Select ended all four)"