Three of the four a540 GPMU limiter "gaps" are vendor parity; only the throttle bit and the stale power level are real
scope: soc:msm8998 · severity: finding · confidence: proven · subsystem: gpu
The question — mainline’s a540_lm_setup() reads like a bring-up stub: it
sets AGC_LM_CONFIG_BCL_DISABLED and AGC_LM_CONFIG_THROTTLE_DISABLE under
comments that say “isn’t enabled for A540” and “for now”, pins the GPMU to
power level 0 “until we get clock scaling”, and never touches the per-part
leakage fuse that a530_lm_setup() writes. Taimen’s DT carries
qcom,lm-limit = <6000>, qcom,base-leakage-coefficient = <34>,
qcom,gpu-efuse-leakage = <0x00070130 24> and qcom,max-power = <5448>, so it
looks like four values the vendor uses and we throw away. Which of those does
the vendor actually do differently?
The answer — one and a half of the four. The vendor’s own
a540_lm_init() is much closer to ours than the comments suggest, because
a540 does not carry the ADRENO_LM feature at all. adreno-gpulist.h:243
and :265 — both a540 entries, patchid 0 and ANY_ID:
.features = ADRENO_PREEMPTION | ADRENO_64BIT | ADRENO_CONTENT_PROTECTION | ADRENO_GPMU | ADRENO_SPTP_PC,No ADRENO_LM, and no .lm_major/.lm_minor (the a530 entries above it have
all three). lm_on() is ADRENO_FEATURE(ADRENO_LM) && test_bit(ADRENO_LM_CTRL),
so on this GPU it is false, and in a540_lm_init():
| what | vendor | mainline | verdict |
|---|---|---|---|
AGC_BCL_DISABLED |
always set (adreno_a5xx.c:1522) |
always set | parity |
ENABLE_GPMU_ADAPTIVE | ISENSE_ENABLE |
behind lm_on() → never on a540 |
never | parity |
per-part leakage fuse → GPMU_BASE_LEAKAGE |
a530_lm_init() only; a540_lm_init() does not write it |
not written | parity |
GPMU_PWR_THRESHOLD |
PWR_THRESHOLD_VALID | lm_limit(), lm_limit() = DT qcom,lm-limit = 6000 |
hardcoded 0x80000000 | 6000 |
parity by value |
AGC_THROTTLE_DISABLE |
set only if (!test_bit(ADRENO_THROTTLING_CTRL)), and adreno.c:100-102 has that bit set by default → vendor does not set it |
set unconditionally | REAL GAP |
GPMU_GPMU_VOLTAGE |
0x80000000 | active_pwrlevel, then re-written on every level change |
0x80000000 | 0, once, never again |
REAL GAP |
| voltage table payload | _write_voltage_table() writes max_power, levels, then (mV, MHz) for every pwrlevel |
max_power, 1, one (mV, MHz) pair at fast_rate |
REAL GAP (same shape as above) |
AGC_LEVEL_CONFIG |
~(GENMASK(LM_DCVS_LIMIT,0) | GENMASK(16+LM_DCVS_LIMIT,16)) with LM_DCVS_LIMIT 1 = ~0x30003 |
LEVEL_CONFIG ~(0x303) |
differs — mainline’s second field is at bit 8, vendor’s at bit 16 |
GPMU_VOLTAGE_INTR_EN_MASK |
written at the end of a540_lm_init() |
written in a5xx_gpmu_init() instead |
parity, different place |
The level gap has a second half that is not in a5xx_power.c at all. Vendor
a5xx_pwrlevel_change_settings() (adreno_a5xx.c:1604-1640) runs on every
power-level transition, and for a540 it is gated on nothing but ADRENO_GPMU:
if (ADRENO_FEATURE(adreno_dev, ADRENO_GPMU)) { if (adreno_is_a540(adreno_dev)) on = ADRENO_GPMU;}...gpmu_set_level(adreno_dev, (0x80000010 | postlevel)); /* pre */gpmu_set_level(adreno_dev, (0x80000000 | postlevel)); /* post */and gpmu_set_level() polls bit 31 until the GPMU acknowledges, 100 tries.
Mainline a5xx implements no gpu_set_freq callback at all (msm_gpu.h:81 — only
a6xx does), so there is no place this could happen today: since devfreq scaling
landed, the GPU changes frequency behind a GPMU that still believes it is at
level 0 and at fast_rate volts.
What this rules out — three quarters of the “GPU on-die limiter is
disabled” gap as previously written up. Do not go looking for the leakage fuse
read, do not enable BCL, and do not enable the adaptive/ISENSE bits: the
vendor does none of those on this GPU, and the DT properties that suggest
otherwise (base-leakage-coefficient, gpu-efuse-leakage) are read by code
paths a540 never reaches. What is left is worth doing, and is two changes, not
four: clear AGC_THROTTLE_DISABLE, and give a5xx a gpu_set_freq that writes
the real level to GPMU_GPMU_VOLTAGE with the vendor’s pre/post pair and ack
poll — which also makes writing the full voltage table meaningful.
How it was established — read, not measured: both trees are source. The
decisive line is the absence of ADRENO_LM from the a540 gpulist entries,
because everything else follows from lm_on() being false. It would be
overturned by a wahoo-specific override setting ADRENO_LM or clearing
ADRENO_THROTTLING_CTRL somewhere outside adreno.c’s initialiser. grep pwrctrl_flag over the vendor tree finds only the default and its readers, and
the two kgsl knobs are also settable from userspace — but a case-insensitive
grep for kgsl|throttl|adreno|devfreq over the whole pulled /vendor/etc
(blobs/work/vendor-etc/) finds no write to either: powerhint.json touches
only kgsl-3d0/devfreq/{min,max}_freq, and init.taimen.rc mentions kgsl
nowhere at all.
