#!/usr/bin/env bash # MobileGL - tools/device_bench/pin_device.sh # Copyright (c) 2025-2026 MobileGL-Dev # Licensed under the GNU Lesser General Public License v3.0: # https://www.gnu.org/licenses/gpl-3.0.txt # https://www.gnu.org/licenses/lgpl-3.0.txt # SPDX-License-Identifier: LGPL-3.0-only # End of Source File Header # pin_device.sh pin|unpin|check # # Frequency pinning for the two disaggregation-campaign devices, standalone: pure adb + su, no # sourcing of a device profile, no dependency on bench.sh. It exists because bench.sh's # pin_freqs()/unpin_freqs() are MediaTek-legacy-only (/proc/ppm + /proc/gpufreq) and NEITHER of # these two devices has those paths - on both of them bench.sh's pin writes into nothing and # exits 0, which is precisely the silent "the run looks pinned but is not" failure the # PROFILE_VERIFIED guard was built to catch. So: pin here, run bench.sh with --no-pin, and use # `check` in place of the pin-integrity fields bench.sh would otherwise have sampled. # # ./pin_device.sh 35d0befa pin # ./bench.sh --device devices/xiaomi-adreno830.env --backend magma --no-pin # ./pin_device.sh 35d0befa check || echo "PIN DRIFTED - discard this run" # ./pin_device.sh 35d0befa unpin # # All three actions print the live big/little/GPU frequencies, the governors and the gate # temperature, so the output of `pin` and of `unpin` is itself the before/after evidence. # # check exit codes (deliberately three-valued: "no pin at all" and "a pin that slipped" are # different facts, and collapsing them to one non-zero would hide which one happened): # 0 PINNED - every pinned node is at its pin AND every live frequency equals it # 1 DRIFT - the device is partly pinned, or a live frequency has left its pin. The # dangerous state: a run overlapping this is not comparable. Discard it. # 2 UNPINNED- no node is at a pin this script set; the vendor governors have their range # back. That is the correct state to leave a device in, and it is still non-zero # so that `pin_device.sh X check && measure` cannot silently measure unpinned. # Note this is asserted against OUR pins, not against the stock range: ColorOS # moves policy4's max on its own within seconds of a release, so an exact-stock # comparison reported DRIFT on a correctly unpinned device. # # Values were read off each device on 2026-09-07 and confirmed against a pinned window; see # ../REPORT.md and the matching *.env profiles. Stock values are hardcoded rather than sampled # at pin time on purpose - a restore that reads "stock" from an already-pinned device would # make the pin permanent, which is how a device silently stays clamped across a reboot-less # week of runs. set -u -o pipefail # Git Bash: stop MSYS rewriting /sys/... and /proc/... arguments into C:/Program Files/... export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' SERIAL=${1:-} ACTION=${2:-} case "$ACTION" in pin|unpin|check) ;; *) echo "usage: $0 pin|unpin|check" >&2; exit 64 ;; esac CPUFREQ=/sys/devices/system/cpu/cpufreq # --------------------------------------------------------------------------------------------- # Device table. Everything device-specific lives here; the actions below are generic. # POLICIES - "label:policy:pinned_khz:stock_min:stock_max" per cpufreq policy to pin. # Every policy that can run a hot thread must be listed, not just the two # the protocol reports: on MT6993 an unpinned policy7 at 4.21 GHz defeats # the entire pin the moment the scheduler lands a thread on it. # THERMAL_TYPE - matched against /sys/class/thermal/thermal_zone*/type by name, never by # zone number: numbering is not stable across boots. # --------------------------------------------------------------------------------------------- case "$SERIAL" in 35d0befa) # Xiaomi 24129PN74C, Snapdragon 8 Elite (SM8750), Adreno 830v2 DEV_NAME="Xiaomi 24129PN74C / SM8750 / Adreno 830v2" GPU_STYLE=kgsl POLICIES="big:policy6:1958400:1017600:2841600 little:policy0:1555200:556800:2745600" THERMAL_TYPE=cpuss-0-0 KGSL=/sys/class/kgsl/kgsl-3d0 GPU_PIN_LEVEL=0 # pwrlevel 0 = 1100 MHz, above the stock devfreq ceiling of 1050 GPU_STOCK_MIN_LEVEL=12 GPU_STOCK_MAX_LEVEL=0 GPU_PINNED_FREQ=1100000000 # gpuclk is in Hz on kgsl, NOT kHz GPU_FREQ_UNIT=Hz ;; 2f7cbe2e) # Redmi M332BF, same SoC as 35d0befa but a DIFFERENT board vendor lock DEV_NAME="Redmi M332BF / SM8750 / Adreno 830v2" GPU_STYLE=kgsl # Read off THIS unit on 2026-09-22, then confirmed against a pinned window (pin written, # every node read back equal to the pin, unpinned, every node back at the stock values # recorded below). Same SM8750 as 35d0befa and the same policy ids, BUT THE STOCK RANGES # DIFFER (policy6 max is 3072000 here vs 2841600 there) - which is exactly why copying the # sibling row would have been wrong even though the two parts are the same silicon. # Every write below needs root; the verify action re-reads all of them. POLICIES="big:policy6:1958400:1017600:3072000 little:policy0:1555200:556800:2745600" THERMAL_TYPE=cpuss-0-0 # thermal_zone13 on this boot; matched by type, never by number KGSL=/sys/class/kgsl/kgsl-3d0 # THE ONE PLACE THIS DEVICE DIFFERS FROM 35d0befa'S ROW, AND IT IS MEASURED, NOT ASSUMED. # Collapsing the kgsl pwrlevel range onto 0 does NOT reach 1100 MHz here: gpuclk reads # 1050000000 and stays there across repeated samples, because this unit ships # thermal_pwrlevel=1 permanently (it latches - writing 0 returns success and the node reads # back 1) and max_gpuclk is read-only at 1050000000. devfreq's own max_freq also clamps to # 1050000000. So the pin is "the fastest frequency this board will actually run", not "the # top OPP in the table" - recorded as 1050 MHz so `check` compares against the truth. # Consequence for the campaign, stated where the number is: absolute times from this device # are NOT comparable with the 35d0befa rows taken while it could still reach 1100. GPU_PIN_LEVEL=0 # pwrlevel 0 = the 1050 MHz ceiling this board enforces GPU_STOCK_MIN_LEVEL=12 GPU_STOCK_MAX_LEVEL=0 GPU_PINNED_FREQ=1050000000 # gpuclk is in Hz on kgsl, NOT kHz GPU_FREQ_UNIT=Hz ;; 3B159D009VZ00000) # Oppo PLG110 / ColorOS, MediaTek MT6993 (Dimensity 9500), Mali DEV_NAME="Oppo PLG110 / MT6993 / Mali (gpufreqv2)" GPU_STYLE=gpufreqv2 # 2000000 / 1600000 are the nearest available OPPs to the protocol's 1958400 / 1555200 # (+2.1% / +2.9%); neither target is an exact step on this part. POLICIES="big:policy4:2000000:300000:3500000 little:policy0:1600000:300000:2100000 extra:policy7:2000000:300000:3200000" THERMAL_TYPE=soc_max GPU_FIX_NODE=/proc/gpufreqv2/fix_target_opp_index GPU_PIN_LEVEL=0 # OPP index 0 = 1716000 kHz, the top working OPP GPU_PINNED_FREQ=1716000 GPU_FREQ_UNIT=kHz ;; *) echo "$0: unknown serial '$SERIAL'." >&2 echo "Known: 35d0befa (Xiaomi/Adreno830), 2f7cbe2e (Redmi/Adreno830), 3B159D009VZ00000 (Oppo/Mali)." >&2 echo "Refusing to guess: the pin path differs per SoC and a wrong one fails silently." >&2 exit 64 ;; esac A="adb -s $SERIAL" # Quote the whole su invocation for the DEVICE shell, or the redirect runs unprivileged. su_() { $A shell "su -c '$*'" 2>&1 | tr -d '\r'; } $A get-state >/dev/null 2>&1 || { echo "$0: device $SERIAL is not connected" >&2; exit 65; } [ "$(su_ 'id -u')" = "0" ] || { echo "$0: no root on $SERIAL (su failed)" >&2; exit 65; } # --- read live state in ONE device round trip ------------------------------------------------- # Every read goes through su: on the Oppo the cpufreq nodes are 0660 system:system and a plain # `adb shell cat scaling_governor` answers "Permission denied", which a careless parser reads # as an empty governor rather than as a failure. read_state() { local script="" spec label pol for spec in $POLICIES; do IFS=: read -r label pol _ _ _ <<<"$spec" script="$script echo \"${label}_gov=\$(cat $CPUFREQ/$pol/scaling_governor)\";" script="$script echo \"${label}_min=\$(cat $CPUFREQ/$pol/scaling_min_freq)\";" script="$script echo \"${label}_max=\$(cat $CPUFREQ/$pol/scaling_max_freq)\";" script="$script echo \"${label}_cur=\$(cat $CPUFREQ/$pol/scaling_cur_freq)\";" done script="$script for tz in /sys/class/thermal/thermal_zone*; do" script="$script if [ \"\$(cat \$tz/type 2>/dev/null)\" = \"$THERMAL_TYPE\" ]; then" script="$script echo \"temp_mc=\$(cat \$tz/temp)\"; echo \"temp_zone=\$tz\"; break; fi; done;" # Emit the GPU nodes RAW and pick them apart locally. The device-side script is delivered as # `su -c '