#!/bin/sh
# ps3-game-mode on <guard pid> [kick] | off (root; ps3-game-guard runs it through sudo): game mode's
# system side. Nothing is stopped: network, SSH, sound, Bluetooth (the DualShock 3), logging, time
# sync and apt keep running.
#   on   1. a watcher (ps3-game-mode-watch.service) that undoes all of it once the guard's pid is
#           gone, whatever happened to the guard (its ExecStopPost: `thaw`)
#        2. units no game needs (FREEZE_UNITS, the running ones) frozen and their memory reclaimed
#        3. the other system services' anonymous memory pushed to swap (RECLAIM_SYSTEM=1); they
#           keep running and fault back in what they use, a few ms the first time
#        4. GAME_SYSCTLS set (none by default; restored after): kernel settings to measure
#           for games, e.g. "vm.swappiness=100 vm.watermark_scale_factor=100"
#        5. the page cache dropped (DROP_CACHES=1: unmapped pages only, so running programs keep
#           their code): the CMA area the GART grows into is then free, not page cache to
#           migrate first, and the game's loading doesn't wait on reclaim
#        6. memory compacted (COMPACT=1): the GART and the VRAM swap work in 1 MB chunks
#        7. a kicker (ps3-kick, unit ps3-game-kick; PS3_GAME_KICK=on|off|force, default on): a
#           cross-CPU wakeup to PPE thread 1 every 10 ms, a stopgap for the lost-decrementer
#           stalls of idle threads (docs/STALLS.md). Skipped when the kernel has the fix
#           (/sys/module/kernel/parameters/ps3_idle_prep = Y; force runs it anyway). The guard
#           passes the game's PS3_GAME_KICK as a third argument, which beats the file.
#   off  stops the watcher, which thaws everything (and thaws once more itself)
# Settings: /etc/default/ps3-game-mode (shell; not shipped, these are the defaults).
FREEZE_UNITS="udisks2.service polkit.service blueman-mechanism.service"
RECLAIM_SYSTEM=1
GAME_SYSCTLS=
DROP_CACHES=1
COMPACT=1
KICK=on
KICK_MS=10
[ -r /etc/default/ps3-game-mode ] && . /etc/default/ps3-game-mode
SELF=/usr/lib/ps3/sbin/ps3-game-mode
KICK_BIN=/usr/lib/ps3/sbin/ps3-kick
# the two paths can be overridden for the host tests only: never when sudo started this (SUDO_UID is set)
if [ -z "${SUDO_UID:-}" ]; then
	SELF=${PS3_GAME_MODE_SELF:-$SELF}
	KICK_BIN=${PS3_KICK_BIN:-$KICK_BIN}
fi
R=/run/ps3-game-mode
SYS=/sys/fs/cgroup/system.slice

# as in ps3-game-guard: anonymous pages first (swap only), then file pages
reclaim() {
	cur=$(cat "$1/memory.current" 2>/dev/null) || return 0
	timeout 30 sh -c 'echo "$1 swappiness=200" > "$2/memory.reclaim"' - "$cur" "$1" 2>/dev/null
	cur=$(cat "$1/memory.current" 2>/dev/null) || return 0
	timeout 30 sh -c 'echo "$1" > "$2/memory.reclaim"' - "$cur" "$1" 2>/dev/null
}

reclaim_anon() {	# anonymous memory only, for running services
	anon=$(awk '$1 == "anon" { print $2 }' "$1/memory.stat" 2>/dev/null)
	[ -n "$anon" ] && timeout 30 sh -c 'echo "$1 swappiness=200" > "$2/memory.reclaim"' - "$anon" "$1" 2>/dev/null
	return 0
}

sysctls_on() {	# GAME_SYSCTLS; the values before are saved first, once (a second on keeps them)
	[ -n "$GAME_SYSCTLS" ] && [ ! -e $R/sysctl ] || return 0
	: > $R/sysctl.new
	for kv in $GAME_SYSCTLS; do
		k=${kv%%=*} v=${kv#*=}
		case $k/$v in vm.*/*[!0-9]*|vm.*/|vm.*=*) ;; vm.*) old=$(sysctl -n "$k") || continue
			case $old in ''|*[!0-9]*) ;; *) echo "$k=$old" >> $R/sysctl.new; sysctl -q -w "$k=$v"; continue ;; esac ;;
		esac
		echo "ps3-game-mode: $kv: only vm.* settings with one number" >&2
	done
	mv $R/sysctl.new $R/sysctl
}

kick_on() {	# kick_on PID [on|off|force]
	case ${2:-} in on|off|force) KICK=$2 ;; esac
	case $KICK in
	off) echo "ps3-game-mode: kicker off (PS3_GAME_KICK=off)" >&2; return 0 ;;
	force) ;;
	*) if [ "$(cat /sys/module/kernel/parameters/ps3_idle_prep 2>/dev/null)" = Y ]; then
		echo "ps3-game-mode: no kicker: the kernel has the idle fix" >&2; return 0
	   fi ;;
	esac
	[ -x "$KICK_BIN" ] || { echo "ps3-game-mode: no $KICK_BIN" >&2; return 0; }
	systemctl stop ps3-game-kick.service 2>/dev/null
	systemd-run --unit=ps3-game-kick --collect -q "$KICK_BIN" -i "$KICK_MS" -p "$1" &&
		echo "ps3-game-mode: kicker started (every $KICK_MS ms)" >&2 ||
		echo "ps3-game-mode: kicker didn't start" >&2
	return 0
}

thaw() {	# only what on froze (a unit someone else froze stays so)
	if systemctl is-active -q ps3-game-kick.service 2>/dev/null; then
		systemctl stop ps3-game-kick.service 2>/dev/null
		echo "ps3-game-mode: kicker stopped" >&2
	fi
	for u in $(cat $R/frozen 2>/dev/null); do systemctl thaw "$u" 2>/dev/null; done
	rm -f $R/frozen
	if [ -e $R/sysctl ]; then
		for kv in $(cat $R/sysctl); do sysctl -q -w "$kv"; done
		rm -f $R/sysctl
	fi
}

on() {
	pid=$1 # $2: PS3_GAME_KICK of the game
	case $pid in ''|*[!0-9]*) echo "ps3-game-mode on: a pid" >&2; exit 2 ;; esac
	[ -d "/proc/$pid" ] || exit 1
	if [ -n "${SUDO_UID:-}" ] && [ "$(stat -c %u "/proc/$pid")" != "$SUDO_UID" ]; then
		echo "ps3-game-mode on: pid $pid isn't yours" >&2; exit 1
	fi
	mkdir -p $R
	systemctl stop ps3-game-mode-watch.service 2>/dev/null	# an earlier game mode's: thaws it
	systemd-run --unit=ps3-game-mode-watch --collect -q -p "ExecStopPost=$SELF thaw" \
		$SELF watch "$pid" || echo "ps3-game-mode: no watcher" >&2
	for u in $FREEZE_UNITS; do
		systemctl is-active -q "$u" || continue
		systemctl freeze "$u" 2>/dev/null || continue
		echo "$u" >> $R/frozen
		reclaim "$SYS/$u"
	done
	[ "$RECLAIM_SYSTEM" = 1 ] && reclaim_anon $SYS
	sysctls_on
	kick_on "$pid" "${2:-}"
	[ "$DROP_CACHES" = 1 ] && echo 1 > /proc/sys/vm/drop_caches
	[ "$COMPACT" = 1 ] && echo 1 > /proc/sys/vm/compact_memory
	true
}

case ${1:-} in
on) on "${2:-}" "${3:-}" ;;
off) systemctl stop ps3-game-mode-watch.service 2>/dev/null; thaw ;;
thaw) thaw ;;
watch)	case ${2:-} in ''|*[!0-9]*) exit 2 ;; esac
	while [ -d "/proc/$2" ]; do sleep 2; done ;;
*) echo "usage: ps3-game-mode on <pid> [on|off|force]|off" >&2; exit 2 ;;
esac
