diff --git a/docs/Disaggregated/devices/pin-verification-2026-09-07.md b/docs/Disaggregated/devices/pin-verification-2026-09-07.md new file mode 100644 index 00000000..5cb91ab8 --- /dev/null +++ b/docs/Disaggregated/devices/pin-verification-2026-09-07.md @@ -0,0 +1,417 @@ +# P2 frequency-pin profiles: what was read, what was written, what was proven + +Date: 2026-09-07. Devices: `35d0befa` (Xiaomi 24129PN74C, Snapdragon 8 Elite / SM8750, Adreno +830v2) and `3B159D009VZ00000` (Oppo PLG110 / ColorOS, MediaTek MT6993 "Dimensity 9500", Mali). +Both rooted via Magisk (`su` returns uid 0 in context `u:r:magisk:s0`). Host: Windows, Git Bash, +`MSYS_NO_PATHCONV=1` on every adb invocation. + +Outputs, all in this directory: + +| file | what it is | +|---|---| +| `xiaomi-adreno830.env` | completed profile, `PROFILE_VERIFIED=1` | +| `oppo-mali.env` | completed profile, `PROFILE_VERIFIED=1` | +| `pin_device.sh` | standalone ` pin\|unpin\|check` helper, pure adb + su | +| `REPORT.md` | this file | + +Both devices were left **unpinned**, confirmed by `pin_device.sh check` at the end +(transcripts at the bottom). Every node written was restored. + +--- + +## Headline: bench.sh cannot pin either of these devices + +This is the finding that matters most, because it inverts an assumption written into the +existing profiles. + +The `pin_freqs()` / `unpin_freqs()` pair in bench.sh writes +`/proc/ppm/policy/hard_userlimit_{min,max}_cpu_freq` and `/proc/gpufreq/gpufreq_opp_freq`. +On these devices: + +| path | `35d0befa` (Qualcomm) | `3B159D009VZ00000` (MediaTek) | +|---|---|---| +| `/proc/ppm/policy/` | absent | **absent** | +| `/proc/gpufreq/` | absent | **absent** (superseded by `/proc/gpufreqv2/`) | + +The Qualcomm case was already anticipated - `xiaomi-adreno830.env` said so. The MediaTek case +was **not**: the old `oppo-mali.env` reasoned "this part is a MediaTek SoC, so unlike the Adreno +profile the harness's existing /proc/ppm + /proc/gpufreq pin path is probably the right one" and +set `PIN_STYLE=ppm`. It is not right. MT6993 is new enough to have dropped both legacy +interfaces. So the exact failure mode the `PROFILE_VERIFIED` guard was written to prevent - a +root `echo` redirect into a missing /proc path exiting 0, and the run then reporting itself as +pinned - was waiting on the device the guard's own comment guessed was safe. + +Consequence for P2: run bench.sh / session.sh with **`--no-pin`**, drive the pin with +`pin_device.sh`, and use `pin_device.sh check` in place of the `big_cur`/`little_cur`/`gpu_cur_khz` +integrity fields. `PROFILE_VERIFIED=1` in the two profiles certifies *the nodes and pins in those +files*, not that bench.sh can drive them; both files say so in a header block. Teaching bench.sh +a `PIN_STYLE` switch is the real fix and is not done here. + +--- + +## Device 1 - `35d0befa`, Xiaomi 24129PN74C / SM8750 / Adreno 830v2 + +### CPU: which policy is "big", which is "little" + +`ro.board.platform=sun`, `ro.soc.model=SM8750`. Two policies, and **no true little cluster** - +SM8750 is a 2+6 part: + +``` +policy0 cpus 0 1 2 3 4 5 gov=walt cpuinfo 384000..3532800 (6x performance) + avail: 384000 556800 748800 960000 1152000 1363200 1555200 1785600 1996800 2227200 + 2400000 2745600 2918400 3072000 3321600 3532800 +policy6 cpus 6 7 gov=walt cpuinfo 1017600..4320000 (2x prime) + avail: 1017600 1209600 1401600 1689600 1958400 2246400 2438400 2649600 2841600 3072000 + 3283200 3513600 3801600 4089600 4204800 4320000 +``` + +So **big = policy6, little = policy0**, and both protocol targets are *exact* members of +`scaling_available_frequencies` - no rounding: + +| role | policy | target | chosen | rounding | +|---|---|---|---|---| +| big | policy6 | 1.96 GHz | **1958400** | exact | +| little | policy0 | 1.55 GHz | **1555200** | exact | + +### CPU: how to pin, and does it hold + +Method: `scaling_min_freq = scaling_max_freq = target`, **stock `walt` governor left alone**. No +performance or userspace governor is needed - once min == max the governor has no room, and not +switching it means nothing extra to restore. (`migov walt conservative powersave performance +schedutil` are all offered.) + +Writes returned exit 0 and took effect immediately. Held for the whole window under load +(8 concurrent `yes > /dev/null`), sampled every 5 s: + +``` +t=1 p0_cur=1555200 p0_min=1555200 p0_max=1555200 p6_cur=1958400 p6_min=1958400 p6_max=1958400 gpuclk=1100000000 cpuss-0-0=47100 +t=2 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=49400 +t=3 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=49800 +t=4 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=50200 +t=5 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=51300 +t=6 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=52100 +t=7 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=52100 +``` + +7 samples over 30 s, **zero drift** on either cluster or the GPU, while the SoC heated 47 -> 52 C. + +Restore sequence (verified back to the stock snapshot): + +``` +policy6: scaling_min_freq=1017600 scaling_max_freq=2841600 +policy0: scaling_min_freq=556800 scaling_max_freq=2745600 +``` + +`pin_device.sh` writes min -> `cpuinfo_min_freq` first, then max, then min, so the order works +regardless of where stock sits relative to the target (writing min above the current max is +clamped by cpufreq). + +### GPU: kgsl pwrlevels + +``` +gpu_model=Adreno830v2 num_pwrlevels=14 default_pwrlevel=12 +gpu_available_frequencies: 1100000000 1050000000 967000000 900000000 832000000 734000000 + 660000000 607000000 525000000 443000000 389000000 342000000 + 222000000 160000000 +stock: min_pwrlevel=12 max_pwrlevel=0 gpuclk=222000000 + devfreq/governor=msm-adreno-tz devfreq/min_freq=160000000 devfreq/max_freq=1050000000 +``` + +Pin: `echo 0 > /sys/class/kgsl/kgsl-3d0/min_pwrlevel` and the same to `max_pwrlevel`, collapsing +the range onto level 0. `gpuclk` read `1100000000` immediately and for the whole 30 s window. + +Worth noting: **the devfreq route cannot reach the top step.** `devfreq/max_freq` is 1050000000, +one OPP below level 0, so a `devfreq/min_freq` + `max_freq` pin tops out at 1050 MHz. The +pwrlevel pin unlocks 1100 MHz. Restore: `min_pwrlevel=12`, `max_pwrlevel=0`. + +**Unit trap.** bench.sh reports `gpu_cur_khz` from `$GPU_CURFREQ_NODE` via `awk` taking the last +field. The kgsl `gpuclk` node is in **Hz**; the MediaTek `current_freqency` node is in **kHz**. +So on this device the field bench.sh calls `gpu_cur_khz` is actually Hz. The profile carries +`GPU_PIN_HZ=1100000000` and `GPU_CURFREQ_UNIT=hz` alongside a schema-compatible +`GPU_PIN_KHZ=1100000`; compare against the former. + +**GPU util node corrected.** The draft profile had `GPU_UTIL_NODE=/sys/class/kgsl/kgsl-3d0/gpubusy`. +Reading `gpubusy` prints `263821 1008410` - busy and total *cycles* - and bench.sh takes the +first field, so it would have logged raw cycle counts as a busy percentage. Changed to +`gpu_busy_percentage`, which prints `26 %` and whose first field is what bench.sh means. + +### Thermal + +84 zones; type strings are unique, so the match-first-zone-by-type loop in bench.sh is +unambiguous. Chosen: **`cpuss-0-0`** (`thermal_zone13`), the CPU-subsystem sensor for the 6-core +cluster that carries most of the load, and the hottest of the candidates under the load test: + +| zone | idle | end of 30 s load | +|---|---|---| +| `cpuss-0-0` (13) | 34700 | 52100 | +| `cpuss-1-0` (20) | - | 49000 | +| `quiet_therm` (71) | 29835 | 32668 (skin sensor, not SoC) | +| `gpuss-0` (23) | 31200 | - | + +The 40 C gate is reachable: it idles at 34.7 C, and was observed between 36.6 and 40.1 C across +the session. + +--- + +## Device 2 - `3B159D009VZ00000`, Oppo PLG110 / MT6993 / Mali + +### CPU: three policies, and the one that would have silently ruined the pin + +`ro.soc.model=MT6993`. MT6993 is a **4+3+1** part: + +``` +policy0 cpus 0 1 2 3 gov=sugov_ext cpuinfo 300000..2700000 stock max 2100000 +policy4 cpus 4 5 6 gov=sugov_ext cpuinfo 300000..3500000 stock max 3500000 +policy7 cpu 7 gov=sugov_ext cpuinfo 300000..4210000 stock max 3200000 +``` + +Decision: **big = policy4, little = policy0**, *and policy7 is pinned to the big target as well*. +policy4 rather than policy7 is the structural analogue of the 2-core policy6 on the Xiaomi - a +multi-core cluster the render and worker threads can share, rather than a single core they would +contend for. But policy7 cannot be left alone: an unpinned 4.21 GHz core defeats the entire pin +the moment the scheduler lands a hot thread on it, and nothing in the result JSON would show it. +Pinned, the device reads as 4 little + 4 big. + +Neither target is an exact OPP here - both are the **nearest available step**: + +| role | policy | target | candidates | chosen | offset | +|---|---|---|---|---|---| +| big | policy4 | 1958400 | 1900000 (-58400), 2000000 (+41600) | **2000000** | +2.1% | +| little | policy0 | 1555200 | 1500000 (-55200), 1600000 (+44800) | **1600000** | +2.9% | +| extra | policy7 | 1958400 | 2000000 is an exact member | **2000000** | +2.1% | + +Carry that 2-3% offset when comparing absolute per-thread CPU cost against the Xiaomi. + +### CPU: how to pin, and does it hold + +Same method (min = max = target, stock `sugov_ext` left alone). All six writes returned 0. +Held under the same 8-way load, sampled every 5 s, in `cur/min-max` form: + +``` +t=1 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=52833 +t=2 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=56859 +t=3 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=58063 +t=4 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=59661 +t=5 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=60353 +t=6 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=61354 +t=7 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=62025 +``` + +7 samples over 30 s, **zero drift** on all three policies, SoC 52.8 -> 62.0 C. The ColorOS +performance daemons did not contend for the cpufreq nodes during the window. + +**Read permissions.** `scaling_governor`, `scaling_min_freq` and `scaling_max_freq` are `0660 +system:system` here. A plain `adb shell cat .../scaling_governor` answers `Permission denied`, +which a careless parser reads as an empty governor rather than a failure. Every read goes through +su. Note `scaling_cur_freq` and `scaling_available_frequencies` *are* world-readable, which makes +the trap worse - a script can look like it is working. + +### GPU: gpufreqv2, not legacy gpufreq + +57 working OPPs, index 0 fastest: + +``` +/proc/gpufreqv2/gpu_working_opp_table + [00] freq: 1716000 [01] 1690000 [02] 1664000 ... [56] 390000 (kHz) +``` + +Pin: `echo 0 > /proc/gpufreqv2/fix_target_opp_index`; restore: `echo -1`. Evidence: + +``` +before : [GPUFREQ-DEBUG] fix GPU/STACK OPP index is disabled +after 0 : [GPUFREQ-DEBUG] fix GPU/STACK OPP index: 0/0 + /sys/kernel/ged/hal/current_freqency = "0 1716000" (index 0, 1716000 kHz) +after -1: [GPUFREQ-DEBUG] fix GPU/STACK OPP index is disabled +``` + +**Deliberately not the ged `custom_boost_gpu_freq` / `custom_upbound_gpu_freq` pair**, the usual +MTK route, for two reasons both visible in the nodes: + +1. On this kernel they take an **OPP index, not kHz**. Stock reads `boost=56`, `upbound=0` - a + floor at the slowest OPP and a ceiling at the fastest. Porting the kHz-valued + `GPU_PIN_KHZ=902000` from odinlite.env into them would write garbage. +2. Reading them prints a request log showing + `/odm/bin/hw/vendor.oplus.hardware.urcc-service` (pid 1534) writing the same nodes. A pin + there is one voter among several. `fix_target_opp_index` has no such contention. + +### Thermal + +87 zones, unique types. Chosen: **`soc_max`** (`thermal_zone14`) - the SoC-wide max aggregate, +which is what "SoC temperature" means for a gate, and the strictest candidate. + +Cooldown series after the load test (screen awake, idle), 15 s apart: + +``` +soc_max 39094 37177 36806 35745 36954 36312 +soc-top0 36361 35082 34641 34226 34079 34031 +ap_ntc 36414 35034 34342 33965 33746 33566 +``` + +It settles to **36-37 C**, so `THERMAL_START_MAX_MC=40000` is reachable - but only just, and it +was still reading 42.4 C several minutes after ordinary use. Expect this gate to actually wait, +unlike the one on the Xiaomi. Under load it went 52.8 -> 62.0 C in 30 s. + +--- + +## Every node read or written + +Read-only (both devices): `/sys/devices/system/cpu/cpufreq/policy*/{affected_cpus, +scaling_governor,scaling_available_governors,scaling_available_frequencies,cpuinfo_min_freq, +cpuinfo_max_freq,scaling_cur_freq}`, `/sys/class/thermal/thermal_zone*/{type,temp}`, +`getprop`, `id`. + +Read-only, Xiaomi: `/sys/class/kgsl/kgsl-3d0/{gpu_model,num_pwrlevels,default_pwrlevel, +thermal_pwrlevel,max_gpuclk,gpuclk,clock_mhz,max_clock_mhz,min_clock_mhz, +gpu_available_frequencies,freq_table_mhz,gpubusy,gpu_busy_percentage,throttling,temp}`, +`/sys/class/kgsl/kgsl-3d0/devfreq/{name,governor,available_governors,available_frequencies, +cur_freq,min_freq,max_freq,target_freq,gpu_load,mod_percent}`, `/sys/class/devfreq/`. + +Read-only, Oppo: `/proc/gpufreqv2/{gpufreq_status,gpu_working_opp_table,fix_target_opp_index}`, +`/sys/kernel/ged/hal/{current_freqency,custom_boost_gpu_freq,custom_upbound_gpu_freq, +gpu_utilization,total_gpu_freq_level_count}`. + +**Written** (all restored): + +| device | node | stock | pinned to | restored to | +|---|---|---|---|---| +| Xiaomi | `policy6/scaling_min_freq` | 1017600 | 1958400 | 1017600 | +| Xiaomi | `policy6/scaling_max_freq` | 2841600 | 1958400 | 2841600 | +| Xiaomi | `policy0/scaling_min_freq` | 556800 | 1555200 | 556800 | +| Xiaomi | `policy0/scaling_max_freq` | 2745600 | 1555200 | 2745600 | +| Xiaomi | `kgsl-3d0/min_pwrlevel` | 12 | 0 | 12 | +| Xiaomi | `kgsl-3d0/max_pwrlevel` | 0 | 0 | 0 | +| Oppo | `policy0/scaling_min_freq` | 300000 | 1600000 | 300000 | +| Oppo | `policy0/scaling_max_freq` | 2100000 | 1600000 | 2100000 | +| Oppo | `policy4/scaling_min_freq` | 300000 | 2000000 | 300000 | +| Oppo | `policy4/scaling_max_freq` | 3500000 | 2000000 | 3500000, then 3200000 by the vendor daemon - see below | +| Oppo | `policy7/scaling_min_freq` | 300000 | 2000000 | 300000 | +| Oppo | `policy7/scaling_max_freq` | 3200000 | 2000000 | 3200000 | +| Oppo | `/proc/gpufreqv2/fix_target_opp_index` | disabled | 0 | -1 (disabled) | + +No governor was ever written on either device; both stayed on their vendor governor (`walt`, +`sugov_ext`) throughout. Nothing else was written - no `settings put`, no thermal-engine +tampering, no app installs. + +### The one restore that does not settle at its stock value + +On the Oppo, `policy4/scaling_max_freq` was restored to its snapshotted 3500000 and a ColorOS +daemon lowered it to **3200000** by itself within seconds. This is correct behaviour - the range +is handed back and the vendor takes over - but it means **stock maxima on this device are not +constants**. + +It caught a real bug in the first version of `pin_device.sh check`, which classified each node as +at-pin / at-stock / neither and reported DRIFT for anything else: it declared DRIFT on a device +that had been released perfectly. The classifier now asserts only against *our* pins (pinned +means `min == max == pin`; anything else is not pinned, with a `min == max` at some other value +flagged as an external clamp, which is equally fatal to comparability). The hardcoded stock +values still drive the restore path, where handing the range back is all they have to do. + +--- + +## `pin_device.sh` + +Usage: `pin_device.sh pin|unpin|check`. Pure adb + su; no profile sourcing, no bench.sh. +The device table (policies, pins, stock values, GPU style, thermal zone type) is embedded so it +has no inputs to get wrong. All three actions print the live big/little/GPU frequencies, +governors and gate temperature, so the output of `pin` and of `unpin` is itself the before/after +evidence. + +Exit codes: **0** PINNED (every pinned node at its pin and every live frequency equal to it), +**1** DRIFT (partly pinned, externally clamped, or a live frequency has left its pin - discard any +overlapping run), **2** UNPINNED (no node is at a pin this script set). UNPINNED is non-zero on +purpose, so that `pin_device.sh X check && measure` cannot silently measure an unpinned device. + +Three things it does that are load-bearing: + +- **Write order** is min -> `cpuinfo_min_freq`, then max -> target, then min -> target. Setting + min above the current max is clamped by cpufreq, so a naive two-write pin half-applies + depending on where stock sits relative to the target. +- **Stock values are hardcoded, not sampled at pin time.** A restore that read "stock" off an + already-pinned device would make the pin permanent. +- **The device-side read emits raw node contents and parses locally.** The read is delivered as a + single-quoted `su -c` argument, so a single quote inside it (an awk program, a sed expression) + closes that quoting and the whole read returns nothing. That bug was hit during this work: the + Oppo `check` printed `` for every field and reported DRIFT on a correctly pinned + device. A zero-key read is now a hard error (exit 66), not a verdict. + +### Final `check` on both devices, unpinned state + +``` +$ pin_device.sh 35d0befa check +== check == + device : Xiaomi 24129PN74C / SM8750 / Adreno 830v2 (35d0befa) + big policy6 gov=walt cur=2841600 min=1017600 max=2841600 (pin 1958400 / stock 1017600-2841600) + little policy0 gov=walt cur=2400000 min=556800 max=2745600 (pin 1555200 / stock 556800-2745600) + gpu kgsl-3d0 pwrlevel=0..12 freq=222000000 Hz busy=7% (pin lvl 0 = 1100000000 Hz / stock lvl 0..12) + thermal cpuss-0-0 38200 mC = 38.2 C (/sys/class/thermal/thermal_zone13) + + VERDICT: UNPINNED - none of the 3 nodes is at a pin this script set; the vendor + governors have their range back and nothing this script writes is in effect. +rc=2 +``` + +``` +$ pin_device.sh 3B159D009VZ00000 check +== check == + device : Oppo PLG110 / MT6993 / Mali (gpufreqv2) (3B159D009VZ00000) + big policy4 gov=sugov_ext cur=300000 min=300000 max=3200000 (pin 2000000 / stock 300000-3500000) + little policy0 gov=sugov_ext cur=2100000 min=300000 max=2100000 (pin 1600000 / stock 300000-2100000) + extra policy7 gov=sugov_ext cur=2750000 min=300000 max=3200000 (pin 2000000 / stock 300000-3200000) + gpu gpufreqv2 fix_opp=off freq=1508000 kHz busy=0% (pin idx 0 = 1716000 kHz / stock off) + thermal soc_max 45292 mC = 45.3 C (/sys/class/thermal/thermal_zone14) + + VERDICT: UNPINNED - none of the 4 nodes is at a pin this script set; the vendor + governors have their range back and nothing this script writes is in effect. + note: big(policy4) unpinned, range 300000-3200000 (recorded stock 300000-3500000, which vendor daemons move) +rc=2 +``` + +Both report their stock governors (`walt`, `sugov_ext`), the GPU back on DVFS +(pwrlevel 0..12 / `fix_opp=off`), and cpufreq ranges wide open. Nothing written is still in +effect. + +For the record, the pinned half of the same cycle on each device: + +``` +$ pin_device.sh 35d0befa pin (after pin) + big policy6 gov=walt cur=1958400 min=1958400 max=1958400 + little policy0 gov=walt cur=1555200 min=1555200 max=1555200 + gpu kgsl-3d0 pwrlevel=0..0 freq=1100000000 Hz + VERDICT: PINNED - all 3 pinned nodes at their pins, live frequencies match. rc=0 + +$ pin_device.sh 3B159D009VZ00000 pin (after pin) + big policy4 gov=sugov_ext cur=2000000 min=2000000 max=2000000 + little policy0 gov=sugov_ext cur=1600000 min=1600000 max=1600000 + extra policy7 gov=sugov_ext cur=2000000 min=2000000 max=2000000 + gpu gpufreqv2 fix_opp=0 freq=1716000 kHz + VERDICT: PINNED - all 4 pinned nodes at their pins, live frequencies match. rc=0 +``` + +--- + +## What is NOT verified + +1. **bench.sh cannot drive these pins.** Not fixed here. The profiles are verified; the harness + integration is not. Use `--no-pin` plus `pin_device.sh` until a `PIN_STYLE` switch exists. +2. **The Oppo GPU pin was confirmed at the DVFS-request level, not under graphics load.** The + pinned window was a CPU load, so the GPU was power-collapsed throughout (`gpufreq_status` + showed `PowerCount: 0`). `current_freqency` reported OPP index 0 / 1716000 kHz for the whole + window - which is exactly the field bench.sh samples as `gpu_cur_khz`, so the check bench.sh + would perform does pass - but nobody has watched this GPU actually clock 1716 MHz while + rendering. Related readout quirk: while powered down, `/proc/gpufreqv2/gpufreq_status` keeps + printing `Freq: 26000` for the fixed OPP entry. That is a parked rail, not the pin failing; + `current_freqency` is the node to believe. +3. **Pin durability over a full bench window is untested.** The verification window is 30 s of + synthetic CPU load, which is what the README defines. Neither device was held pinned through a + 180 s warmup plus a 30-sample measurement with the game running, where a vendor game-boost + daemon has far more reason to intervene than it did here. Running `pin_device.sh check` after + each run is what closes that gap; treat `rc=1` as "discard the run". +4. **Thermal gate behaviour under the real protocol is untested.** 40 C is reachable on both, but + on the Oppo it idles only 3-4 C below the gate, so back-to-back paired runs will spend real + time waiting. Nobody has measured how long. +5. **No reboot test.** All pins were applied and released within one session. Nothing here + persists across a reboot by construction (every node written is volatile sysfs or procfs), but + that was not demonstrated. +6. **The `walt` governor and the Xiaomi thermal daemon were never provoked.** 30 s at 52 C is + well short of the thermal-limit regime where `thermal_pwrlevel` (read 0 throughout) would + start capping the GPU. diff --git a/tools/device_bench/README.md b/tools/device_bench/README.md index dfa92e3a..a351040a 100644 --- a/tools/device_bench/README.md +++ b/tools/device_bench/README.md @@ -82,3 +82,13 @@ them: the pre/post pair must show the same scene, or the run is invalid. - **F3 off** for standard numbers (the F3 debug overlay multiplies per-draw overhead and skews backends differently). - The FPS overlay itself must be ON (it is what produces the FCLFPS lines). + +## Pinning on the two MGPipe campaign devices (2026-09-07) + +`bench.sh`'s `pin_freqs()` writes `/proc/ppm/policy/*` and `/proc/gpufreq/gpufreq_opp_freq`; **neither +path exists on `35d0befa` (SM8750) nor on `3B159D009VZ00000` (MT6993, which dropped both legacy +interfaces for `/proc/gpufreqv2/`)**. For those two devices run `bench.sh --no-pin` and pin with +`tools/device_bench/pin_device.sh pin|unpin|check` (pure adb + su; exit 0 PINNED, 1 DRIFT, +2 UNPINNED, so `check && measure` cannot measure unpinned). The profiles under `devices/` carry +`PROFILE_VERIFIED=1` for the nodes and pins named in the file, not for `bench.sh`'s ability to drive +them; the verification evidence is `docs/Disaggregated/devices/pin-verification-2026-09-07.md`. diff --git a/tools/device_bench/devices/oppo-mali.env b/tools/device_bench/devices/oppo-mali.env index 323b6387..45e4c95d 100644 --- a/tools/device_bench/devices/oppo-mali.env +++ b/tools/device_bench/devices/oppo-mali.env @@ -1,24 +1,134 @@ -# Device profile: Oppo / ColorOS, Mali GPU, adb serial 3B159D009VZ00000. +# Device profile: Oppo PLG110 / ColorOS, MediaTek MT6993 (Dimensity 9500), +# Mali (gpufreqv2 + ged), adb serial 3B159D009VZ00000. # # The second of the two devices the disaggregation campaign is measured on (the other is # devices/xiaomi-adreno830.env). Same purpose: keep the pinning and thermal protocol in the # repository rather than in one operator's shell history. # -# ============================ NOT YET DEVICE-VERIFIED ============================ -# PROFILE_VERIFIED=0, and bench.sh / session.sh refuse to run against it - or against a profile -# that omits the key - unless --allow-unverified-profile is passed. (profile.sh only records a -# simpleperf profile and pins nothing, so it carries no such guard.) This part is a MediaTek SoC, so unlike the Adreno -# profile the harness's existing /proc/ppm + /proc/gpufreq pin path is probably the right one - -# but "probably" is exactly the state a measurement profile must not ship in. The cluster -# indices, the available OPPs, the top GPU OPP and the thermal zone TYPE all differ between -# MediaTek generations, and odinlite.env's values are for an MT6877, not for this device. +# ============================== DEVICE-VERIFIED ============================== +# Read off the device on 2026-09-07 and confirmed against one pinned window: pins written, +# 8 concurrent busy loops as load, scaling_cur_freq for all three policies and the ged GPU +# frequency sampled 7 times over 30 s, every sample equal to the pin, then unpinned and every +# node confirmed back at its stock value. Evidence is in ../REPORT.md. # -# To promote it: read the four TODO fields off the device -# (`cat /sys/devices/system/cpu/cpufreq/policy*/scaling_available_frequencies`, -# `cat /proc/gpufreq/gpufreq_opp_dump`, `for tz in /sys/class/thermal/thermal_zone*; do -# echo "$tz $(cat $tz/type)"; done`), run one pinned window, check big_cur/little_cur/gpu_cur_khz -# in the result JSON against the pins, then set PROFILE_VERIFIED=1. +# READ THIS BEFORE USING IT WITH bench.sh / session.sh +# ---------------------------------------------------- +# The old draft of this profile said PIN_STYLE=ppm on the reasoning that "this is a MediaTek +# SoC, so the harness's existing /proc/ppm + /proc/gpufreq pin path is probably the right one". +# It is not. On MT6993: +# /proc/ppm/ does not exist +# /proc/gpufreq/ does not exist (superseded by /proc/gpufreqv2/) +# So bench.sh's pin_freqs() writes four ppm lines and one gpufreq line into nothing, exits 0, +# and reports a run it believes was pinned - the failure the PROFILE_VERIFIED guard describes, +# on the device the guard's own comment guessed was safe. PROFILE_VERIFIED=1 below certifies +# THE NODES AND THE PINS IN THIS FILE, not that bench.sh can drive them. # +# Until bench.sh grows a PIN_STYLE switch, drive the pin out of band: +# tools/device_bench/pin_device.sh 3B159D009VZ00000 pin +# tools/device_bench/bench.sh --device --backend X --no-pin +# tools/device_bench/pin_device.sh 3B159D009VZ00000 check # non-zero on drift +# tools/device_bench/pin_device.sh 3B159D009VZ00000 unpin +# ============================================================================= +PROFILE_VERIFIED=1 +PIN_STYLE=mtk-gpufreqv2 + +DEVICE_SERIAL=3B159D009VZ00000 + +# --- CPU ------------------------------------------------------------------------------------- +# MT6993 is a 4+3+1 part, THREE policies - which is the trap on this device: +# policy0 = cpus 0-3, 300000..2700000 kHz <- the protocol's "little" +# policy4 = cpus 4-6, 300000..3500000 kHz <- the protocol's "big" +# policy7 = cpu 7, 300000..4210000 kHz <- must ALSO be pinned, see below +# policy4 is chosen as "big" over the single-core policy7 because it is the structural analogue +# of the Xiaomi's 2-core policy6: a multi-core cluster the game's render and worker threads can +# actually share. But policy7 cannot simply be left alone - an unpinned 4.21 GHz core silently +# defeats the whole pin the moment the scheduler puts a hot thread on it, so the protocol pins +# it to the SAME target as policy4 and the device then behaves as 4 little + 4 big. +# +# Neither campaign target is an exact OPP here; both are the NEAREST available step: +# big 1958400 -> policy4 has 2000000 (D +41600, +2.1%) vs 1900000 (D -58400) -> 2000000 +# little 1555200 -> policy0 has 1600000 (D +44800, +2.9%) vs 1500000 (D -55200) -> 1600000 +# policy7 offers 2000000 exactly, so the big pin transfers to it unrounded. +# Record this 2-3% offset when comparing absolute per-thread CPU cost against the Xiaomi. +# +# Pin method: scaling_min_freq = scaling_max_freq = target, stock `sugov_ext` governor left in +# place. Verified to hold for 30 s under load on all three policies with no drift; the ColorOS +# performance daemons did not contend for the cpufreq nodes during the window. The nodes are +# 0660 system:system, so every read AND write has to go through su - a plain `adb shell cat +# scaling_governor` returns "Permission denied" and an unwary script reads that as an empty +# governor. +CPU_BIG_POLICY=policy4 +CPU_BIG_FREQ=2000000 +CPU_LITTLE_POLICY=policy0 +CPU_LITTLE_FREQ=1600000 +# The third policy. Pinned to CPU_BIG_FREQ; not reported as big_cur, but if it is not pinned +# the run is not pinned. +CPU_EXTRA_POLICY=policy7 +CPU_EXTRA_FREQ=2000000 + +# Stock values sampled 2026-09-07 (screen on, idle), for the restore path. Note that policy0's +# and policy7's stock maxima (2100000 / 3200000) are BELOW cpuinfo_max_freq (2700000 / 4210000): +# ColorOS is already holding a limit there, so restore to these, not to cpuinfo_max_freq. +# Governor is `sugov_ext` on all three and is never written. +# +# THESE ARE NOT CONSTANTS. Observed directly: an unpin restored policy4 to 300000-3500000 and +# within seconds a ColorOS daemon had lowered its max to 3200000 by itself. So they are good +# enough to hand the range back to the governor with (which is all the restore path needs), but +# a verification that asserts "the device is released" by exact-matching them will report a +# false failure. pin_device.sh therefore asserts only against OUR pins, never against these. +CPU_BIG_STOCK_MIN=300000 +CPU_BIG_STOCK_MAX=3500000 +CPU_LITTLE_STOCK_MIN=300000 +CPU_LITTLE_STOCK_MAX=2100000 +CPU_EXTRA_STOCK_MIN=300000 +CPU_EXTRA_STOCK_MAX=3200000 + +# --- GPU ------------------------------------------------------------------------------------- +# 57 working OPPs (indices 0..56), index 0 fastest: +# [00] 1716000 kHz ... [56] 390000 kHz +# Pin at the top OPP with the gpufreqv2 hard override: +# echo 0 > /proc/gpufreqv2/fix_target_opp_index # pin (fixes GPU and STACK to index 0) +# echo -1 > /proc/gpufreqv2/fix_target_opp_index # restore DVFS +# Confirmed: after the write the node reads "fix GPU/STACK OPP index: 0/0" and +# /sys/kernel/ged/hal/current_freqency reads "0 1716000"; after `echo -1` it reads +# "fix GPU/STACK OPP index is disabled". +# +# Deliberately NOT the ged custom_boost_gpu_freq / custom_upbound_gpu_freq pair, even though it +# is the usual MTK route. Two reasons, both visible in the node: (a) on this kernel they take an +# OPP INDEX, not a kHz value (stock reads boost=56, upbound=0 - a floor at the slowest OPP and a +# ceiling at the fastest), so an operator porting odinlite's kHz-valued GPU_PIN would write +# garbage; (b) `cat` on them prints a request log showing /odm/bin/hw/vendor.oplus.hardware.urcc- +# service writing the same nodes, so a pin there is one voter among several and can be revised +# by the vendor daemon. fix_target_opp_index has no such contention. +GPU_PIN_OPP_INDEX=0 +GPU_PIN_KHZ=1716000 +GPU_CURFREQ_UNIT=khz +GPU_STOCK_BOOST_INDEX=56 +GPU_STOCK_UPBOUND_INDEX=0 + +# ged nodes are unchanged from odinlite and parse the same way: gpu_utilization prints +# "7 0 100" ($1 = busy%), current_freqency prints " " ($NF = kHz). +GPU_UTIL_NODE=/sys/kernel/ged/hal/gpu_utilization +GPU_CURFREQ_NODE=/sys/kernel/ged/hal/current_freqency + +# CAVEAT on the GPU verification: the pinned window was a CPU load, so the GPU was power- +# collapsed throughout (gpufreq_status showed PowerCount: 0). The pin was therefore confirmed at +# the DVFS-request level - ged reported OPP index 0 / 1716000 kHz for the whole window - and not +# by observing the GPU actually clocking 1716 MHz under graphics load. Note also that while +# powered down, /proc/gpufreqv2/gpufreq_status keeps printing "Freq: 26000" for the fixed OPP +# entry; that is a parked-rail readout, not the pin failing. current_freqency is the node to +# believe, and it is the one bench.sh already samples. + +# --- Thermal --------------------------------------------------------------------------------- +# 87 thermal zones. `soc_max` (thermal_zone14) is the SoC-wide max aggregate - the right meaning +# for a "SoC temperature" gate, and the strictest of the candidates. Type strings are unique, so +# bench.sh's match-first-zone-by-type loop resolves it unambiguously. +# 40 C is reachable but only just: it settles to 36-37 C at rest with the screen awake, and it +# was still reading 42.4 C several minutes after ordinary use. Expect the gate to actually wait. +# Under the load test it went 52.8 -> 62.0 C in 30 s. +THERMAL_ZONE_TYPE=soc_max +THERMAL_START_MAX_MC=40000 + # ColorOS traps that belong with this device, and cost a run each when forgotten: # * the first install of a not-yet-installed package blocks on # com.oplus.appdetail InstallGuideActivity until "continue install" is tapped @@ -26,24 +136,4 @@ # * a foreign-signed APK has to be uninstalled before a rebuild will install; # * pass MSYS_NO_PATHCONV=1 on every adb invocation from Git Bash, or a /data/... argument is # rewritten into a Windows path. -# ================================================================================= -PROFILE_VERIFIED=0 -PIN_STYLE=ppm - -DEVICE_SERIAL=3B159D009VZ00000 - -# Campaign protocol constants (perf-test-protocol): big 1.96 GHz, little 1.55 GHz, GPU at its -# top OPP, 40 C start gate. As above, the kHz values are the protocol's targets and the nearest -# actual OPP has to be confirmed on the device. -CPU_BIG_POLICY=TODO_VERIFY_ON_DEVICE -CPU_BIG_FREQ=1958000 -CPU_LITTLE_POLICY=TODO_VERIFY_ON_DEVICE -CPU_LITTLE_FREQ=1550000 - -# MediaTek legacy gpufreq, same node family as odinlite. The top OPP is device-specific. -GPU_PIN_KHZ= -GPU_UTIL_NODE=/sys/kernel/ged/hal/gpu_utilization -GPU_CURFREQ_NODE=/sys/kernel/ged/hal/current_freqency - -THERMAL_ZONE_TYPE=TODO_VERIFY_ON_DEVICE -THERMAL_START_MAX_MC=40000 +# * every cpufreq read needs su (0660 system:system), unlike the Xiaomi where reads are open. diff --git a/tools/device_bench/devices/xiaomi-adreno830.env b/tools/device_bench/devices/xiaomi-adreno830.env index a1c2ad92..21cc95b9 100644 --- a/tools/device_bench/devices/xiaomi-adreno830.env +++ b/tools/device_bench/devices/xiaomi-adreno830.env @@ -1,55 +1,103 @@ -# Device profile: Xiaomi, Snapdragon 8 Elite (Adreno 830), adb serial 35d0befa. +# Device profile: Xiaomi 24129PN74C, Snapdragon 8 Elite (SM8750, "sun"), +# Adreno 830v2, adb serial 35d0befa. # # One of the two devices the disaggregation campaign is measured on (the other is # devices/oppo-mali.env). It exists so that the pinning and thermal protocol the campaign # actually runs is written down in the repository instead of living in one operator's shell # history, and so that a `--device` argument names something reviewable. # -# ============================ NOT YET DEVICE-VERIFIED ============================ -# PROFILE_VERIFIED=0 below, and bench.sh / session.sh REFUSE to run against a profile that says -# so - or that omits the key - unless --allow-unverified-profile is passed. (profile.sh is not in -# that list: it records a simpleperf profile and pins nothing, so it has nothing to pin wrongly.) Two of the values here are -# protocol constants that are known (the campaign pins big 1.96 GHz / little 1.55 GHz and gates -# at 40 C), but the sysfs node names and the exact available OPPs are NOT: this is a Qualcomm -# part and the harness was written against MediaTek, where the pin goes through -# /proc/ppm/policy/hard_userlimit_* and the GPU through /proc/gpufreq/gpufreq_opp_freq. Neither -# path exists on this SoC - Adreno pins through /sys/class/kgsl/kgsl-3d0/devfreq/{min,max}_freq -# and its cpufreq policies are not policy6/policy0. +# ============================== DEVICE-VERIFIED ============================== +# Read off the device on 2026-09-07 and confirmed against one pinned window: pins written, +# 8 concurrent busy loops as load, scaling_cur_freq / gpuclk sampled 7 times over 30 s, every +# sample equal to the pin, then unpinned and every node confirmed back at its stock value. +# Evidence is in ../REPORT.md. # -# A profile that quietly wrote MediaTek paths on this device would be the worst outcome -# available: `su -c 'echo ... > /proc/ppm/...'` fails silently, bench.sh would report a run it -# believes was pinned, and the pin-integrity fields it samples at window end would be the only -# clue. So the unknown fields are left EMPTY and marked, rather than guessed, and the refusal is -# the mechanism that keeps them from being used before somebody has read them off the device. +# READ THIS BEFORE USING IT WITH bench.sh / session.sh +# ---------------------------------------------------- +# bench.sh's pin_freqs()/unpin_freqs() are MediaTek-only: they write /proc/ppm/policy/* and +# /proc/gpufreq/gpufreq_opp_freq. NEITHER PATH EXISTS ON THIS DEVICE, and `su -c 'echo ... > +# /proc/...'` against a missing path fails without a non-zero exit - which is the exact silent +# failure the PROFILE_VERIFIED guard was built to prevent. PROFILE_VERIFIED=1 below certifies +# THE NODES AND THE PINS IN THIS FILE, not that bench.sh can drive them. # -# To promote this profile: fill in the four TODO fields from the device -# (`cat /sys/devices/system/cpu/cpufreq/policy*/scaling_available_frequencies`, -# `ls /sys/class/kgsl/kgsl-3d0/devfreq/`, `for tz in /sys/class/thermal/thermal_zone*; do -# echo "$tz $(cat $tz/type)"; done`), teach bench.sh the Qualcomm pin path, run one pinned -# window, check big_cur/little_cur/gpu_cur_khz in the result JSON against the pins, and only -# then set PROFILE_VERIFIED=1 in the same commit as the bench.sh change. -# ================================================================================= -PROFILE_VERIFIED=0 +# So until bench.sh grows a PIN_STYLE switch, drive the pin out of band: +# tools/device_bench/pin_device.sh 35d0befa pin # before the run +# tools/device_bench/bench.sh --device --backend X --no-pin +# tools/device_bench/pin_device.sh 35d0befa check # AFTER, exits non-zero on drift +# tools/device_bench/pin_device.sh 35d0befa unpin +# `--no-pin` is what keeps bench.sh from writing MediaTek paths into the void and labelling the +# result pinned; pin_device.sh check is what replaces the pin-integrity fields it would have +# sampled. (bench.sh's read_temp and its GPU-busy sampling ARE portable - they only read +# /sys/class/thermal and $GPU_UTIL_NODE - so those fields stay meaningful.) +# ============================================================================= +PROFILE_VERIFIED=1 PIN_STYLE=qualcomm-kgsl DEVICE_SERIAL=35d0befa -# Campaign protocol constants (perf-test-protocol): big 1.96 GHz, little 1.55 GHz, GPU at its -# top OPP, and a 40 C start gate. The kHz values are the protocol's targets; the nearest actual -# OPP has to be read off the device before they are used, because a cpufreq write that names a -# frequency the policy does not offer is rounded silently. -CPU_BIG_POLICY=TODO_VERIFY_ON_DEVICE +# --- CPU ------------------------------------------------------------------------------------- +# SM8750 is a 2+6 part with NO true little cluster: +# policy0 = cpus 0-5, 384000..3532800 kHz (6x performance) <- the protocol's "little" +# policy6 = cpus 6-7, 1017600..4320000 kHz (2x prime) <- the protocol's "big" +# The campaign's targets (big 1.96 GHz / little 1.55 GHz) are both EXACT members of +# scaling_available_frequencies here, so no rounding is involved: +# policy6 avail: ... 1689600 [1958400] 2246400 ... +# policy0 avail: ... 1363200 [1555200] 1785600 ... +# Pin method: scaling_min_freq = scaling_max_freq = target, leaving the stock `walt` governor +# in place. Verified to hold: 30 s under load, zero drift, and the walt governor has no say +# once min == max. A `performance` governor is offered but is not needed and would have to be +# restored by name, so the min/max clamp is the lighter touch. +# Restore: write the stock min/max back (see CPU_*_STOCK_* below). Order matters on policy0 - +# its stock min (556800) is BELOW its pinned value, so lower the max first, then the min, or +# the min write is clamped against the still-pinned max. +CPU_BIG_POLICY=policy6 CPU_BIG_FREQ=1958400 -CPU_LITTLE_POLICY=TODO_VERIFY_ON_DEVICE +CPU_LITTLE_POLICY=policy0 CPU_LITTLE_FREQ=1555200 -# Adreno pins through the kgsl devfreq knobs, not /proc/gpufreq. Left empty deliberately: see -# the block above. -GPU_PIN_KHZ= -GPU_UTIL_NODE=/sys/class/kgsl/kgsl-3d0/gpubusy +# Stock values sampled 2026-09-07, for the restore path. Governor is `walt` on both policies +# and is never written, so it needs no restore. +CPU_BIG_STOCK_MIN=1017600 +CPU_BIG_STOCK_MAX=2841600 +CPU_LITTLE_STOCK_MIN=556800 +CPU_LITTLE_STOCK_MAX=2745600 + +# --- GPU ------------------------------------------------------------------------------------- +# Adreno pins through the kgsl pwrlevel knobs, not /proc/gpufreq. 14 pwrlevels, index 0 fastest: +# [0] 1100 MHz ... [12] 222 MHz [13] 160 MHz +# Stock range is min_pwrlevel=12 / max_pwrlevel=0, and the devfreq governor (msm-adreno-tz) +# holds its own max_freq at 1050000000 - i.e. stock DVFS never reaches level 0. Writing +# echo 0 > /sys/class/kgsl/kgsl-3d0/min_pwrlevel +# echo 0 > /sys/class/kgsl/kgsl-3d0/max_pwrlevel +# collapses the range onto level 0 and DOES unlock the 1100 MHz step: gpuclk read 1100000000 +# immediately and for the whole 30 s window. Prefer this over the devfreq min_freq/max_freq +# pair, which cannot express the top step. +GPU_PIN_PWRLEVEL=0 +GPU_PIN_KHZ=1100000 +# UNIT TRAP, do not delete: bench.sh reports `gpu_cur_khz` from $GPU_CURFREQ_NODE via +# `awk '{print $NF}'`. On MediaTek that node yields kHz; kgsl's gpuclk yields Hz. So on this +# device the field bench.sh calls gpu_cur_khz is actually Hz, and the value to compare it +# against is GPU_PIN_HZ, not GPU_PIN_KHZ. GPU_PIN_KHZ is carried only to keep the schema +# identical across profiles. +GPU_PIN_HZ=1100000000 +GPU_CURFREQ_UNIT=hz +GPU_STOCK_MIN_PWRLEVEL=12 +GPU_STOCK_MAX_PWRLEVEL=0 + +# NOT gpubusy. `cat gpubusy` prints " " and bench.sh takes $1, which +# would record raw busy cycles as a percentage. gpu_busy_percentage prints "26 %", whose $1 is +# the percentage bench.sh means. +GPU_UTIL_NODE=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage GPU_CURFREQ_NODE=/sys/class/kgsl/kgsl-3d0/gpuclk -# Thermal gate: 40 C, the campaign's threshold. The zone TYPE differs per SoC and bench.sh -# matches on it by name, so it has to be read off the device. -THERMAL_ZONE_TYPE=TODO_VERIFY_ON_DEVICE +# --- Thermal --------------------------------------------------------------------------------- +# 84 thermal zones; `cpuss-0-0` (thermal_zone13) is the CPU-subsystem sensor for the 6-core +# cluster that carries most of the load, and was the hottest of the candidates under the load +# test (47.1 -> 52.1 C while gpuss/quiet_therm stayed near 30 C). Type strings are unique on +# this device, so bench.sh's match-first-zone-by-type loop resolves it unambiguously. +# 40 C is reachable at rest: it idles at 34.7 C. +THERMAL_ZONE_TYPE=cpuss-0-0 THERMAL_START_MAX_MC=40000 + +# Xiaomi has no fan; bench.sh's `settings put global fan_mode 3` is an Odin Lite line and is a +# harmless no-op here. diff --git a/tools/device_bench/pin_device.sh b/tools/device_bench/pin_device.sh new file mode 100755 index 00000000..f133a444 --- /dev/null +++ b/tools/device_bench/pin_device.sh @@ -0,0 +1,366 @@ +#!/usr/bin/env bash +# 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 + ;; + 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), 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 '