From b8e2e042f2f5bb4333518dec7f51c83b49067d44 Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:46:34 -0400 Subject: [PATCH 1/4] compat: lower the eDP backlight floor and map nits linearly The monitor's floor was 10% of full scale (level 205 of 2047) and the kext's helpers mapped [min_nits, max_nits] affinely onto [floor, max]. If the panel's DPCD level is linear in luminance, the darkest macOS request was ~50 nits: ten times the 5-nit minimum the A16's panel table gives Windows (BrightnessRangeLevel0 5000..500000 millinits, BrightnessRangeLowOffset 10000). EDP_BL_FLOOR_PERMILLE is now 20: level 41, ~10 nits under the linear model (the table's low offset), very dark but never off if the TCON applied a 2.2 power law instead. Nothing has measured the panel's curve; the header says so and the hardware run must check the bottom of the slider. The helpers now model luminance proportional to level (edp_bl_nits_to_level/edp_bl_level_to_nits with the top level at max_nits) and edp_bl_min_nits_q16 raises the darkest request to the floor's luminance; the slider helpers take 16.16 bounds. The SNAP_EDP_BACKLIGHT=1 monitor built with run next1b's flags differs from next1b's qualified one in one byte (same size, 200 bytes of code budget left); without the define the monitor is byte-identical. The model test checks the floor and the curve, and four new mutations (no floor change back to 10%, an affine curve, a top short of max, a minimum below the floor) must fail it. The SnapAdreno kext with ADRENO_BACKLIGHT uses the new signatures; it builds once the submodule pointer moves (next commit). The default kext does not include this header. --- compat/edp-backlight.h | 77 ++++++++++++++++++------------- tools/test-edp-backlight.c | 91 ++++++++++++++++++++++++++----------- tools/test-edp-backlight.py | 14 ++++-- 3 files changed, 121 insertions(+), 61 deletions(-) diff --git a/compat/edp-backlight.h b/compat/edp-backlight.h index 55f8ff6..89a6aec 100644 --- a/compat/edp-backlight.h +++ b/compat/edp-backlight.h @@ -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 ----------- */ diff --git a/tools/test-edp-backlight.c b/tools/test-edp-backlight.c index 70fc220..45930ab 100644 --- a/tools/test-edp-backlight.c +++ b/tools/test-edp-backlight.c @@ -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); } diff --git a/tools/test-edp-backlight.py b/tools/test-edp-backlight.py index 165fdbe..11c2bb7 100644 --- a/tools/test-edp-backlight.py +++ b/tools/test-edp-backlight.py @@ -8,8 +8,8 @@ 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 @@ -45,7 +45,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);'), From 8351bc06742d735592979a73d037ec1b3d6c6d36 Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:46:45 -0400 Subject: [PATCH 2/4] repo: point SnapAdreno at its backlight-path branch SnapAdreno 9cc1e92 (branch backlight-path, not pushed): AdrenoBacklight maps nits proportionally onto levels with the compat helpers of the previous commit, publishes force-brightness-control so WindowServer's BrightnessControl writes brightness-nits to it, and publishes its DPCD700 diagnostic as 64 bits. The default kext is section for section the base commit's. --- gpu/adreno-kext/kext | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpu/adreno-kext/kext b/gpu/adreno-kext/kext index b5bfcb4..9cc1e92 160000 --- a/gpu/adreno-kext/kext +++ b/gpu/adreno-kext/kext @@ -1 +1 @@ -Subproject commit b5bfcb409267751bb62adfc3bca8b4b65fe66ebb +Subproject commit 9cc1e92ba9ea440c8f422697179f2cde4099b9eb From e273ff6475193c8e71089df975239aefbaa7b7b8 Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:47:06 -0400 Subject: [PATCH 3/4] tests: check AdrenoBacklight's BrightnessControl path test-edp-backlight.py gains three checks for the path SnapAdreno's AdrenoBacklight.h describes: - Wiring: the service publishes force-brightness-control as data with a nonzero first byte, backlight-control and IODisplayParameters, takes brightness-nits, and maps every request through the one curve with the configured minimum. - UserspaceContract: on a 26A428 host (the guest's userspace) with ipsw, the dyld cache's BrightnessControl and IOMobileFramebuffer still carry the names the path relies on; SKIP elsewhere. - Kext (--kext-reference): desktop-next builds too, both backlight profiles carry force-brightness-control, the default kext does not. --- tools/test-edp-backlight.py | 73 +++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/tools/test-edp-backlight.py b/tools/test-edp-backlight.py index 11c2bb7..f02034a 100644 --- a/tools/test-edp-backlight.py +++ b/tools/test-edp-backlight.py @@ -13,8 +13,12 @@ safety property or bends the curve in a copy of the engine; the same program mus 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. @@ -170,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, @@ -208,6 +227,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): @@ -237,7 +291,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:]) @@ -245,10 +300,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__': From 15336488b9ace85192b9048729243808e83b4e3c Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:47:13 -0400 Subject: [PATCH 4/4] docs: describe how --edp-backlight brightness reaches the panel RUNNING.md now gives the 2% floor, how CoreBrightness's nits reach AdrenoBacklight (WindowServer's BrightnessControl, enabled by the service's force-brightness-control) and the two boot-args that move the nits-to-level curve. It no longer names the pinned collection's folder, which the next pin will change. --- docs/RUNNING.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/RUNNING.md b/docs/RUNNING.md index b633383..30dde96 100644 --- a/docs/RUNNING.md +++ b/docs/RUNNING.md @@ -191,12 +191,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