repo: merge the brightness path (backlight-path)

BrightnessControl writes brightness-nits only to a backlight service that carries force-brightness-control; AdrenoBacklight now publishes it (SnapAdreno 9cc1e92), so CoreBrightness's slider and keys reach the panel instead of dimming in software (next1b: RequestsNits 0). The floor drops from 10% to 2% (level 41 of 2047) and nits map proportionally onto levels.
This commit is contained in:
Acelogic committed 2026-10-24 19:50:41 -04:00
commit fbed1cad82
5 files changed
+198 -72

No files matched your search

+46 -31
View File
@@ -68,14 +68,25 @@
#define DPCD_EDP_PWMGEN_BITS_MASK 0x1Fu
/* ---- Policy ------------------------------------------------------------------------------------------------------ */
/* The lowest level the monitor writes, in thousandths of full scale (at least level 1). An OLED's DPCD level need
* not be linear in luminance: until a hardware run measures the curve, 10% keeps the panel visibly lit even if the
* TCON applies a 2.2 gamma (about 0.6% of peak). The guest cannot lower it; the top is the panel's own maximum. */
#define EDP_BL_FLOOR_PERMILLE 100u
/* The lowest level the monitor writes, in thousandths of full scale (at least level 1); the guest cannot go below it
* and the top is the panel's own maximum. What is known (run next1b and the A16 DSDT, AGENTS.md labels):
* - OBSERVED: 11-bit levels 0..2047; the firmware/Windows left level 716 (35% of full scale).
* - VERIFIED: the DSDT panel table for 0x834C/0x422C names it "Generic OLED Panel HDR" and gives Windows' nits-based
* brightness BrightnessRangeLevel0 "5000 500000 1000 2" and BrightnessRangeLowOffset 10000 (INFERRED: millinits,
* so 5..500 nits with a 10-nit low offset).
* - Nothing measures the panel's luminance per level. If it is linear in luminance (INFERRED: the table describes
* the panel to Windows only by a nits range; edp_bl_nits_to_level uses this model), the previous 10% floor
* (level 205) was ~50 nits, ten times Windows' minimum, so the bottom of the macOS slider could not be dim.
* 2% is level 41 on this panel: ~10 nits under the linear model, the table's low offset and twice Windows' minimum,
* so the bottom of the slider is dim but readable. If the TCON instead followed a 2.2 power law it would be ~0.02% of
* peak (very dark, never off); the hardware run must look at the bottom of the slider before this is relied on, and
* the kext's boot-arg adreno_backlight_min_nits raises the darkest request without rebuilding the monitor. */
#define EDP_BL_FLOOR_PERMILLE 20u
#define EDP_BL_MAX_FAILURES 3u /* consecutive failed SETs before the monitor stops for the boot */
#define EDP_BL_AUX_BUDGET_DIV 200u /* one transaction's completion budget: hz / 200 = 5 ms */
/* The kext's nominal range for CoreBrightness, from the panel table's millinit range (INFERRED); boot-args may
* narrow it within [EDP_BL_NITS_LIMIT_MIN, EDP_BL_NITS_LIMIT_MAX]. */
/* The kext's nominal range for CoreBrightness, from the panel table's millinit range (INFERRED). MAX is the luminance
* the model assigns to the top level; MIN is the darkest request, raised to the floor's luminance when below it.
* Boot-args adreno_backlight_min_nits / _max_nits may set either within [EDP_BL_NITS_LIMIT_MIN, EDP_BL_NITS_LIMIT_MAX]. */
#define EDP_BL_NITS_MIN_DEFAULT 5u
#define EDP_BL_NITS_MAX_DEFAULT 500u
#define EDP_BL_NITS_LIMIT_MIN 1u
@@ -184,39 +195,43 @@ static inline uint32_t edp_bl_decode(const uint8_t in[2], int lsb)
return lsb ? (uint32_t)in[0] << 8 | in[1] : in[0];
}
/* Nits (16.16 fixed point, as CoreBrightness's "brightness-nits") to a level: linear from [min_nits, max_nits] onto
* [floor, max], clamped at both ends. The panel's luminance curve is not known (INFERRED linear); the floor holds
* whatever is asked. */
static inline uint32_t edp_bl_nits_to_level(uint64_t nits_q16, uint32_t min_nits, uint32_t max_nits,
uint32_t floor, uint32_t max)
/* The luminance model the kext maps CoreBrightness's requests with: luminance proportional to the level, the top
* level being max_nits (INFERRED, see EDP_BL_FLOOR_PERMILLE). Nits are 16.16 fixed point throughout, the unit of
* BrightnessControl's "brightness-nits". A request maps to the nearest level, clamped into [floor, max]: the bottom
* of the slider lands on (or near) the floor and the top on the panel's maximum. Callers keep max_nits within
* EDP_BL_NITS_LIMIT_MAX and max within 16 bits, so no product below exceeds 2^42. */
static inline uint32_t edp_bl_nits_to_level(uint64_t nits_q16, uint32_t max_nits, uint32_t floor, uint32_t max)
{
uint64_t lo = (uint64_t)min_nits << 16, hi = (uint64_t)max_nits << 16;
if (hi <= lo || max <= floor) return floor;
if (nits_q16 <= lo) return floor;
if (nits_q16 >= hi) return max;
return floor + (uint32_t)(((nits_q16 - lo) * (max - floor) + (hi - lo) / 2) / (hi - lo));
uint64_t full = (uint64_t)max_nits << 16;
if (!full || max <= floor) return floor;
if (nits_q16 >= full) return max;
return edp_bl_clamp((nits_q16 * max + full / 2) / full, floor, max);
}
static inline uint64_t edp_bl_level_to_nits(uint32_t level, uint32_t min_nits, uint32_t max_nits,
uint32_t floor, uint32_t max)
/* A level's luminance under the same model (levels above max count as max). */
static inline uint64_t edp_bl_level_to_nits(uint32_t level, uint32_t max_nits, uint32_t max)
{
uint64_t lo = (uint64_t)min_nits << 16, hi = (uint64_t)max_nits << 16;
if (hi <= lo || max <= floor || level <= floor) return lo;
if (level >= max) return hi;
return lo + ((uint64_t)(level - floor) * (hi - lo) + (max - floor) / 2) / (max - floor);
if (!max) return 0;
if (level > max) level = max;
return ((uint64_t)level * ((uint64_t)max_nits << 16) + max / 2) / max;
}
/* IODisplayParameters "brightness" (0..EDP_BL_SLIDER_MAX) as nits, linear between the nominal ends. */
static inline uint64_t edp_bl_slider_to_nits(uint64_t slider, uint32_t min_nits, uint32_t max_nits)
/* The darkest luminance the kext offers CoreBrightness (BrightnessMilliNits "min"): the configured minimum, raised to
* the floor's luminance, since nothing below the floor can be shown. */
static inline uint64_t edp_bl_min_nits_q16(uint32_t min_nits, uint32_t max_nits, uint32_t floor, uint32_t max)
{
uint64_t at_floor = edp_bl_level_to_nits(floor, max_nits, max), configured = (uint64_t)min_nits << 16;
return configured > at_floor ? configured : at_floor;
}
/* IODisplayParameters "brightness" (0..EDP_BL_SLIDER_MAX) as nits, linear between lo and hi (16.16), and back. */
static inline uint64_t edp_bl_slider_to_nits(uint64_t slider, uint64_t lo_q16, uint64_t hi_q16)
{
uint64_t lo = (uint64_t)min_nits << 16, hi = (uint64_t)max_nits << 16;
if (slider > EDP_BL_SLIDER_MAX) slider = EDP_BL_SLIDER_MAX;
return hi <= lo ? lo : lo + ((hi - lo) * slider + EDP_BL_SLIDER_MAX / 2) / EDP_BL_SLIDER_MAX;
return hi_q16 <= lo_q16 ? lo_q16 : lo_q16 + ((hi_q16 - lo_q16) * slider + EDP_BL_SLIDER_MAX / 2) / EDP_BL_SLIDER_MAX;
}
static inline uint32_t edp_bl_nits_to_slider(uint64_t nits_q16, uint32_t min_nits, uint32_t max_nits)
static inline uint32_t edp_bl_nits_to_slider(uint64_t nits_q16, uint64_t lo_q16, uint64_t hi_q16)
{
uint64_t lo = (uint64_t)min_nits << 16, hi = (uint64_t)max_nits << 16;
if (hi <= lo || nits_q16 <= lo) return 0;
if (nits_q16 >= hi) return EDP_BL_SLIDER_MAX;
return (uint32_t)(((nits_q16 - lo) * EDP_BL_SLIDER_MAX + (hi - lo) / 2) / (hi - lo));
if (hi_q16 <= lo_q16 || nits_q16 <= lo_q16) return 0;
if (nits_q16 >= hi_q16) return EDP_BL_SLIDER_MAX;
return (uint32_t)(((nits_q16 - lo_q16) * EDP_BL_SLIDER_MAX + (hi_q16 - lo_q16) / 2) / (hi_q16 - lo_q16));
}
/* ---- Allow-lists: every MMIO access and every DPCD transaction the monitor makes passes one of these ----------- */
+10 -3
View File
@@ -198,12 +198,19 @@ self-test, for A/B rendering checks only.
panel's brightness. The monitor gets `SNAP_EDP_BACKLIGHT`: its
`ADRENO_GUEST_BACKLIGHT` call writes the panel's DPCD backlight level over the
eDP controller's AUX channel, touching only the AUX transport registers and
DPCD `0x721`-`0x723`, clamping the level to between 10% of full scale and the
DPCD `0x721`-`0x723`, clamping the level to between 2% of full scale and the
panel's maximum, and stopping for the boot on any failure with the brightness
left as it was (`compat/edp-backlight.h`, event `EV_EDP_BACKLIGHT`). The guest
gets the AuxKC pinned as `INPUT_AUXKC_DESKTOP_BACKLIGHT`, whose AdrenoX2 has the
`AdrenoBacklight` service CoreBrightness drives, and the boot-arg
`adreno_backlight=1`. The pinned collection, `next-20261024`, is built with
`AdrenoBacklight` service, and the boot-arg `adreno_backlight=1`. CoreBrightness
hands its nits to WindowServer, whose BrightnessControl writes them to the
service as `brightness-nits` because the service carries
`force-brightness-control` (the path and its evidence are in the kext's
`src/AdrenoBacklight.h`). The service maps nits to a level proportionally, the
top level being `adreno_backlight_max_nits` (default 500) and the darkest
request `adreno_backlight_min_nits` (default 5, raised to the 2% floor's ~10
nits); either boot-arg moves the curve without rebuilding the monitor. The
collection is built with
`KEXT_PROFILE=desktop-next I2C_PROFILE=desktop-fnkeys tools/build-auxkc-desktop.sh
NAME` for a monitor without `--gpu-coherent` (`desktop-next` is
`desktop-nocoherent` plus the service); a `--gpu-coherent` run needs a
+64 -27
View File
@@ -187,6 +187,61 @@ static uint32_t level_of(const uint64_t r[4]) { return (uint32_t)r[3]; }
static uint32_t state_of(const uint64_t r[4]) { return edp_bl_reply_state(r[1]); }
static uint32_t panel_level(void) { return (uint32_t)M.dpcd[0x722] << 8 | M.dpcd[0x723]; }
/* The kext's nits -> level curve (luminance proportional to level, the top level = max_nits), on the A16's 11-bit
* panel with the default 5..500 nits: the bottom of CoreBrightness's range is the floor, the top is the panel's
* maximum, and everything between is proportional, so the slider's lower half is genuinely dim. */
#define Q(n) ((uint64_t)(n) << 16)
static void test_curve(void)
{
const uint32_t max = 2047, floor = edp_bl_floor_level(max), maxn = EDP_BL_NITS_MAX_DEFAULT;
CHECK(floor == 41 && maxn == 500);
uint32_t prev = 0;
for (uint64_t q = 0; q <= Q(600); q += 1u << 12) { /* monotonic, inside [floor, max] */
uint32_t l = edp_bl_nits_to_level(q, maxn, floor, max);
CHECK(l >= floor && l <= max && l >= prev);
prev = l;
}
CHECK(edp_bl_nits_to_level(0, maxn, floor, max) == floor);
CHECK(edp_bl_nits_to_level(Q(5), maxn, floor, max) == floor); /* Windows' minimum is below the floor */
CHECK(edp_bl_nits_to_level(Q(10), maxn, floor, max) == floor); /* the table's low offset is the floor */
CHECK(edp_bl_nits_to_level(Q(11), maxn, floor, max) == 45);
CHECK(edp_bl_nits_to_level(Q(250), maxn, floor, max) == 1024); /* half the luminance, half the levels */
CHECK(edp_bl_nits_to_level(Q(140), maxn, floor, max) == 573); /* CoreBrightness's default, 28% */
CHECK(edp_bl_nits_to_level(Q(500), maxn, floor, max) == max);
CHECK(edp_bl_nits_to_level(Q(500) - 1, maxn, floor, max) == max);
CHECK(edp_bl_nits_to_level(Q(1000), maxn, floor, max) == max);
CHECK(edp_bl_nits_to_level(UINT64_MAX, maxn, floor, max) == max);
CHECK(edp_bl_nits_to_level(Q(100), 0, floor, max) == floor); /* degenerate: the floor */
CHECK(edp_bl_nits_to_level(Q(100), maxn, max, max) == max && edp_bl_nits_to_level(Q(100), maxn, 9, 9) == 9);
/* The configured top moves the scale: with 400 nits as full scale, 200 nits is half. */
CHECK(edp_bl_nits_to_level(Q(200), 400, floor, max) == 1024 && edp_bl_nits_to_level(Q(400), 400, floor, max) == max);
/* Level -> nits is the same line; every level from the floor up survives the round trip. */
CHECK(edp_bl_level_to_nits(0, maxn, max) == 0 && edp_bl_level_to_nits(max, maxn, max) == Q(500) &&
edp_bl_level_to_nits(max + 100, maxn, max) == Q(500) && edp_bl_level_to_nits(5, maxn, 0) == 0);
CHECK(edp_bl_level_to_nits(716, maxn, max) >> 16 == 174); /* where Windows left the panel */
for (uint32_t l = floor; l <= max; l++) {
uint64_t n = edp_bl_level_to_nits(l, maxn, max), exact = (uint64_t)l * Q(maxn);
CHECK(n * max + max >= exact && n * max <= exact + max); /* within half a q16 step of the line */
CHECK(edp_bl_nits_to_level(n, maxn, floor, max) == l);
}
/* The darkest offer: the configured minimum, never below what the floor shows. */
uint64_t lo = edp_bl_min_nits_q16(EDP_BL_NITS_MIN_DEFAULT, maxn, floor, max);
CHECK(lo == edp_bl_level_to_nits(floor, maxn, max) && lo >> 16 == 10 && edp_bl_nits_to_level(lo, maxn, floor, max) == floor);
CHECK(edp_bl_min_nits_q16(20, maxn, floor, max) == Q(20) && edp_bl_nits_to_level(Q(20), maxn, floor, max) == 82);
/* The slider scale between the published ends. */
CHECK(edp_bl_slider_to_nits(0, lo, Q(500)) == lo && edp_bl_slider_to_nits(65536, lo, Q(500)) == Q(500) &&
edp_bl_slider_to_nits(1u << 20, lo, Q(500)) == Q(500) && edp_bl_slider_to_nits(77, Q(9), Q(9)) == Q(9));
CHECK(edp_bl_nits_to_slider(lo, lo, Q(500)) == 0 && edp_bl_nits_to_slider(Q(500), lo, Q(500)) == 65536 &&
edp_bl_nits_to_slider(Q(3), lo, Q(500)) == 0 && edp_bl_nits_to_slider(Q(9), Q(9), Q(9)) == 0);
for (uint32_t v = 0; v <= 65536; v += 512) {
uint32_t back = edp_bl_nits_to_slider(edp_bl_slider_to_nits(v, lo, Q(500)), lo, Q(500));
CHECK(back + 1 >= v && back <= v + 1);
}
/* The bottom of the slider is the floor, its top the panel's maximum. */
CHECK(edp_bl_nits_to_level(edp_bl_slider_to_nits(0, lo, Q(500)), maxn, floor, max) == floor);
CHECK(edp_bl_nits_to_level(edp_bl_slider_to_nits(65536, lo, Q(500)), maxn, floor, max) == max);
}
static void test_helpers(void)
{
uint32_t bits = 0;
@@ -204,37 +259,19 @@ static void test_helpers(void)
CHECK(edp_bl_bits(10, 10, 10, 0, &bits)); /* above 8 without the LSB register */
CHECK(!edp_bl_bits(8, 8, 8, 0, &bits) && bits == 8);
CHECK(edp_bl_max_level(11) == 2047 && edp_bl_max_level(8) == 255 && edp_bl_max_level(16) == 65535);
CHECK(edp_bl_floor_level(2047) == 205 && edp_bl_floor_level(255) == 26 && edp_bl_floor_level(1) == 1);
/* The floor is 2% of full scale, rounded up (EDP_BL_FLOOR_PERMILLE): level 41 of the A16's 2047. */
CHECK(edp_bl_floor_level(2047) == 41 && edp_bl_floor_level(255) == 6 && edp_bl_floor_level(1) == 1);
for (uint32_t n = 1; n <= 16; n++) {
uint32_t max = edp_bl_max_level(n), floor = edp_bl_floor_level(max);
CHECK(floor >= 1 && floor <= max);
CHECK((uint64_t)floor * 1000 >= (uint64_t)max * EDP_BL_FLOOR_PERMILLE);
CHECK(floor == 1 || (uint64_t)(floor - 1) * 1000 < (uint64_t)max * 20); /* the first level at 2%, no more */
}
CHECK(edp_bl_clamp(0, 205, 2047) == 205 && edp_bl_clamp(5000, 205, 2047) == 2047 &&
edp_bl_clamp(1000, 205, 2047) == 1000 && edp_bl_clamp(UINT64_MAX, 205, 2047) == 2047);
CHECK(edp_bl_clamp(0, 41, 2047) == 41 && edp_bl_clamp(5000, 41, 2047) == 2047 &&
edp_bl_clamp(1000, 41, 2047) == 1000 && edp_bl_clamp(UINT64_MAX, 41, 2047) == 2047);
CHECK(edp_bl_encode(0x5A3, 1, b) == 2 && b[0] == 0x05 && b[1] == 0xA3 && edp_bl_decode(b, 1) == 0x5A3);
CHECK(edp_bl_encode(0xC8, 0, b) == 1 && b[0] == 0xC8 && edp_bl_decode(b, 0) == 0xC8);
/* nits -> level: ends map onto floor and max, monotonic, round trip within one level */
uint32_t prev = 0;
for (uint64_t q = 0; q <= (600ull << 16); q += 1u << 12) {
uint32_t l = edp_bl_nits_to_level(q, 5, 500, 205, 2047);
CHECK(l >= 205 && l <= 2047 && l >= prev);
prev = l;
}
CHECK(edp_bl_nits_to_level(5ull << 16, 5, 500, 205, 2047) == 205);
CHECK(edp_bl_nits_to_level(500ull << 16, 5, 500, 205, 2047) == 2047);
CHECK(edp_bl_nits_to_level(0, 5, 500, 205, 2047) == 205);
CHECK(edp_bl_nits_to_level(UINT64_MAX >> 20, 5, 500, 205, 2047) == 2047);
CHECK(edp_bl_nits_to_level(100ull << 16, 500, 5, 205, 2047) == 205); /* inverted range: the floor */
for (uint32_t l = 205; l <= 2047; l += 7)
CHECK(edp_bl_nits_to_level(edp_bl_level_to_nits(l, 5, 500, 205, 2047), 5, 500, 205, 2047) == l);
CHECK(edp_bl_slider_to_nits(0, 5, 500) == 5ull << 16 && edp_bl_slider_to_nits(65536, 5, 500) == 500ull << 16 &&
edp_bl_slider_to_nits(1u << 20, 5, 500) == 500ull << 16);
CHECK(edp_bl_nits_to_slider(5ull << 16, 5, 500) == 0 && edp_bl_nits_to_slider(500ull << 16, 5, 500) == 65536);
for (uint32_t v = 0; v <= 65536; v += 512) {
uint32_t back = edp_bl_nits_to_slider(edp_bl_slider_to_nits(v, 5, 500), 5, 500);
CHECK(back + 1 >= v && back <= v + 1);
}
test_curve();
uint8_t caps[4] = {0x03, 0x07, 0x06, 0x10};
uint64_t x1 = edp_bl_reply_x1(EDP_BL_STATE_MODE, EDP_BL_DETAIL_BITS, caps, 0x12);
CHECK(edp_bl_reply_abi(x1) == EDP_BL_ABI && edp_bl_reply_state(x1) == EDP_BL_STATE_MODE &&
@@ -260,14 +297,14 @@ static void test_probe_and_set(void)
M.status = ST_DONE; /* a status the firmware left behind */
CHECK(call(EDP_BL_QUERY, 0, 0, r) == 0);
CHECK(edp_bl_reply_abi(r[1]) == EDP_BL_ABI && state_of(r) == EDP_BL_STATE_READY);
CHECK((uint32_t)r[2] == 205 && r[2] >> 32 == 2047 && level_of(r) == 768 && (r[3] >> 32 & 0xff) == 11 &&
CHECK((uint32_t)r[2] == 41 && r[2] >> 32 == 2047 && level_of(r) == 768 && (r[3] >> 32 & 0xff) == 11 &&
(r[3] >> 40 & 1) == 1);
CHECK(M.dpcd_writes == 0 && M.gos == 3 && M.violations == 0);
CHECK(call(EDP_BL_SET, 1500, 0, r) == 0 && level_of(r) == 1500 && panel_level() == 1500 && !(r[3] >> 41 & 1));
CHECK(M.last_write_addr == 0x722 && M.last_write_len == 2 && M.last_write[0] == 0x05 && M.last_write[1] == 0xDC);
unsigned gos = M.gos;
CHECK(call(EDP_BL_SET, 1500, 0, r) == 0 && M.gos == gos); /* unchanged: no traffic */
CHECK(call(EDP_BL_SET, 0, 0, r) == 0 && panel_level() == 205 && (r[3] >> 41 & 1)); /* never black */
CHECK(call(EDP_BL_SET, 0, 0, r) == 0 && panel_level() == 41 && (r[3] >> 41 & 1)); /* never black */
CHECK(call(EDP_BL_SET, 70000, 0, r) == 0 && panel_level() == 2047 && (r[3] >> 41 & 1)); /* never past max */
CHECK(M.dpcd[0x721] == 0x12 && M.violations == 0);
}
@@ -280,7 +317,7 @@ static void test_byte_panel(void)
M.dpcd[0x722] = 0x80; M.dpcd[0x723] = 0x55;
M.dpcd[0x724] = 8; M.dpcd[0x725] = 8; M.dpcd[0x726] = 8;
CHECK(call(EDP_BL_QUERY, 0, 0, r) == 0 && state_of(r) == EDP_BL_STATE_READY && level_of(r) == 0x80 &&
r[2] >> 32 == 255 && (uint32_t)r[2] == 26);
r[2] >> 32 == 255 && (uint32_t)r[2] == 6);
CHECK(call(EDP_BL_SET, 200, 0, r) == 0 && M.dpcd[0x722] == 200 && M.dpcd[0x723] == 0x55 && M.last_write_len == 1);
CHECK(M.violations == 0);
}
+77 -10
View File
@@ -8,13 +8,17 @@ opt-in wiring, and (on request) the default-build identity of the monitor and th
python3 tools/test-edp-backlight.py --kext-reference DIR # + kext: default sections == DIR's
Model: tools/test-edp-backlight.c runs compat/edp-backlight.{h,c} against a model of the eDP controller's AUX FIFO and a
DPCD panel and must PASS (ASan/UBSan). Each MUTATIONS entry weakens one safety property in a copy of the engine; the
same program must FAIL on every one.
DPCD panel, and checks the kext's nits -> level curve, and must PASS (ASan/UBSan). Each MUTATIONS entry weakens one
safety property or bends the curve in a copy of the engine; the same program must FAIL on every one.
Wiring: probes/xnuboot.c defaults SNAP_EDP_BACKLIGHT to 0 and every monitor use sits under it; compat/edp-backlight.c is
linked only through probes/run.py OPTIONAL; the call number and event number are unique; the kext's backlight code sits
under ADRENO_BACKLIGHT; installed-fresh appends the define last, adds the boot-args, and refuses until the AuxKC is
pinned.
under ADRENO_BACKLIGHT and publishes what BrightnessControl needs to write to it (force-brightness-control), mapping
every request through the one curve; installed-fresh appends the define last, adds the boot-args, and refuses until the
AuxKC is pinned.
UserspaceContract (a 26A428 host with ipsw only, else SKIP): the dyld cache's BrightnessControl and IOMobileFramebuffer
still carry the names that path relies on (gpu/adreno-kext/kext/src/AdrenoBacklight.h).
--build: xnuboot with each --qualification's flags, define absent, =0 and =1: absent == =0 == the qualification's
monitor_sha256; =1 differs and fits the code budget (probes/run.py OUT_AT - CODE_AT), its headroom printed.
@@ -45,7 +49,15 @@ _ap.add_argument('--out')
ARGS, REST = _ap.parse_known_args() if __name__ == '__main__' else (_ap.parse_args([]), [])
ENGINE = ('compat/edp-backlight.h', 'compat/edp-backlight.c', 'compat/dpu-intf.h')
MUTATIONS = (
('no floor', 'compat/edp-backlight.h', '#define EDP_BL_FLOOR_PERMILLE 100u', '#define EDP_BL_FLOOR_PERMILLE 0u'),
('no floor', 'compat/edp-backlight.h', '#define EDP_BL_FLOOR_PERMILLE 20u', '#define EDP_BL_FLOOR_PERMILLE 0u'),
('bright floor', 'compat/edp-backlight.h', '#define EDP_BL_FLOOR_PERMILLE 20u',
'#define EDP_BL_FLOOR_PERMILLE 100u'),
('affine curve', 'compat/edp-backlight.h', 'return edp_bl_clamp((nits_q16 * max + full / 2) / full, floor, max);',
'return floor + (uint32_t)((nits_q16 * (max - floor) + full / 2) / full);'),
('top short of max', 'compat/edp-backlight.h', 'if (nits_q16 >= full) return max;',
'if (nits_q16 >= full) return max - 1;'),
('minimum below the floor', 'compat/edp-backlight.h', 'return configured > at_floor ? configured : at_floor;',
'return configured + 0 * at_floor;'),
('no clamp', 'compat/edp-backlight.c', 'uint32_t level = edp_bl_clamp(request, s->floor, s->max);',
'uint32_t level = (uint32_t)request;'),
('mask bits dropped', 'compat/edp-backlight.c', ' << 1 | (status & EDP_INTR_MASK_BITS));', ' << 1);'),
@@ -162,6 +174,21 @@ class Wiring(unittest.TestCase):
'ADRENO_BACKLIGHT')
self.assertEqual(bad, [])
def test_kext_brightness_path(self):
# The service carries what WindowServer's BrightnessControl needs to write brightness-nits to it
# (gpu/adreno-kext/kext/src/AdrenoBacklight.h, steps 2 and 3), and every request goes through the curve.
src = (ROOT / 'gpu/adreno-kext/kext/src/AdrenoDisplay/backlight.cpp').read_text()
force = re.search(r'kForceBrightnessControl\[(\d+)\] = \{([^}]*)\}', src)
self.assertIsNotNone(force, 'force-brightness-control bytes not found')
self.assertNotEqual(int(force.group(2).split(',')[0], 0), 0, 'its first byte must be nonzero')
for needle in ('setProperty("force-brightness-control", (void *)kForceBrightnessControl,',
'setProperty("backlight-control", kOSBooleanTrue)', 'd->getObject("brightness-nits")',
'putDict(params, "BrightnessMilliNits", nits)', 'setProperty("IODisplayParameters", params)',
'edp_bl_nits_to_level(nits_q16 < fMinQ16 ? fMinQ16 : nits_q16, fMaxNits, fFloor, fMax)',
'fMinQ16 = edp_bl_min_nits_q16(fMinNits, fMaxNits, fFloor, fMax);'):
self.assertIn(needle, src)
self.assertEqual(src.count('edp_bl_nits_to_level('), 1, 'one mapping for every source')
def test_cycle(self):
from cycle_lib import installed_fresh as f
chain = dict(variant='fast', input=True, input_touchpad=True, input_ptp=True, present_dartpool=True,
@@ -202,6 +229,41 @@ class Wiring(unittest.TestCase):
self.assertTrue(body.rstrip().endswith('flags.append(EDP_BACKLIGHT_DEFINE)'), 'the define is not appended last')
DSC = Path('/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e')
IPSW = shutil.which('ipsw') or '/opt/homebrew/bin/ipsw'
# The names the guest's 26A428 userspace must still use for AdrenoBacklight.h's path, per image: BrightnessControl's
# match key, the keys it reads and writes, and the BrightnessControl entry points IOMobileFramebuffer resolves.
USERSPACE_NAMES = {
'BrightnessControl': ('IOPropertyMatch', 'backlight-control', 'force-brightness-control', 'brightness-nits',
'IODisplayParameters', 'BrightnessMilliNits', 'LmaxProduct'),
'IOMobileFramebuffer': ('BCBrtControlNewMonitorForAllDisplaysWithHandler', 'BCBrtControlGetCapabilities',
'BCBrtControlSetNits'),
}
def host_build():
try:
return subprocess.run(['sw_vers', '-buildVersion'], capture_output=True, text=True).stdout.strip()
except OSError:
return ''
@unittest.skipUnless(DSC.exists() and Path(IPSW).exists() and host_build() == '26A428',
'needs a 26A428 host (the guest\'s userspace), its dyld cache and ipsw')
class UserspaceContract(unittest.TestCase):
"""The host runs the guest's userspace build; its dyld cache must carry every name the path relies on. A
missing name means the 26A428 analysis in AdrenoBacklight.h no longer holds for this build."""
def test_names_present(self):
names = sorted({n for v in USERSPACE_NAMES.values() for n in v})
out = subprocess.run([IPSW, 'dsc', 'str', str(DSC), '--no-color', *names], capture_output=True, text=True,
timeout=600)
self.assertEqual(out.returncode, 0, out.stderr[-1000:])
found = set(re.findall(r'^0x[0-9a-f]+: "([^"]*)"\s+image=(\S+)$', out.stdout, re.M))
missing = [(image, n) for image, ns in USERSPACE_NAMES.items() for n in ns if (n, image) not in found]
self.assertEqual(missing, [])
@unittest.skipUnless(ARGS.build, 'pass --build to compile the monitor (minutes)')
class Build(unittest.TestCase):
def test_monitor(self):
@@ -231,7 +293,8 @@ class Kext(unittest.TestCase):
out = Path(ARGS.out) / 'kext' if ARGS.out else Path(tempfile.mkdtemp(prefix='edp-backlight-kext-'))
env = dict(os.environ, ZERO_AR_DATE='1')
kext = ROOT / 'gpu/adreno-kext/kext/build.sh'
for profile, dest in (('', out / 'default'), ('desktop-backlight', out / 'desktop-backlight')):
profiles = ('desktop-backlight', 'desktop-next')
for profile, dest in (('', out / 'default'), *((p, out / p) for p in profiles)):
e = dict(env, OUT=str(dest), **({'PROFILE': profile} if profile else {}))
b = subprocess.run(['sh', str(kext)], env=e, capture_output=True, text=True)
self.assertEqual(b.returncode, 0, b.stdout[-2000:] + b.stderr[-2000:])
@@ -239,10 +302,14 @@ class Kext(unittest.TestCase):
c = subprocess.run([sys.executable, str(ROOT / 'tools/macho-sections.py'), str(ref),
str(out / 'default/AdrenoX2.kext/Contents/MacOS/AdrenoX2')], capture_output=True, text=True)
self.assertRegex(c.stdout, r'(\d+) of \1 sections identical', c.stdout)
nm = subprocess.run(['nm', '-m', str(out / 'desktop-backlight/AdrenoX2.kext/Contents/MacOS/AdrenoX2')],
capture_output=True, text=True).stdout
self.assertIn('__ZN13AdrenoDisplay20set_brightness_gatedEi', nm)
self.assertIn('__ZN15AdrenoBacklight10gMetaClassE', nm)
default = (out / 'default/AdrenoX2.kext/Contents/MacOS/AdrenoX2').read_bytes()
self.assertNotIn(b'force-brightness-control', default)
for profile in profiles:
binary = out / profile / 'AdrenoX2.kext/Contents/MacOS/AdrenoX2'
nm = subprocess.run(['nm', '-m', str(binary)], capture_output=True, text=True).stdout
self.assertIn('__ZN13AdrenoDisplay20set_brightness_gatedEi', nm, profile)
self.assertIn('__ZN15AdrenoBacklight10gMetaClassE', nm, profile)
self.assertIn(b'force-brightness-control\0', binary.read_bytes(), profile)
if __name__ == '__main__':