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 #define DPCD_EDP_PWMGEN_BITS_MASK 0x1Fu
/* ---- Policy ------------------------------------------------------------------------------------------------------ */ /* ---- Policy ------------------------------------------------------------------------------------------------------ */
/* The lowest level the monitor writes, in thousandths of full scale (at least level 1). An OLED's DPCD level need /* The lowest level the monitor writes, in thousandths of full scale (at least level 1); the guest cannot go below it
* not be linear in luminance: until a hardware run measures the curve, 10% keeps the panel visibly lit even if the * and the top is the panel's own maximum. What is known (run next1b and the A16 DSDT, AGENTS.md labels):
* TCON applies a 2.2 gamma (about 0.6% of peak). The guest cannot lower it; the top is the panel's own maximum. */ * - OBSERVED: 11-bit levels 0..2047; the firmware/Windows left level 716 (35% of full scale).
#define EDP_BL_FLOOR_PERMILLE 100u * - 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_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 */ #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 /* The kext's nominal range for CoreBrightness, from the panel table's millinit range (INFERRED). MAX is the luminance
* narrow it within [EDP_BL_NITS_LIMIT_MIN, EDP_BL_NITS_LIMIT_MAX]. */ * 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_MIN_DEFAULT 5u
#define EDP_BL_NITS_MAX_DEFAULT 500u #define EDP_BL_NITS_MAX_DEFAULT 500u
#define EDP_BL_NITS_LIMIT_MIN 1u #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]; 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 /* The luminance model the kext maps CoreBrightness's requests with: luminance proportional to the level, the top
* [floor, max], clamped at both ends. The panel's luminance curve is not known (INFERRED linear); the floor holds * level being max_nits (INFERRED, see EDP_BL_FLOOR_PERMILLE). Nits are 16.16 fixed point throughout, the unit of
* whatever is asked. */ * BrightnessControl's "brightness-nits". A request maps to the nearest level, clamped into [floor, max]: the bottom
static inline uint32_t edp_bl_nits_to_level(uint64_t nits_q16, uint32_t min_nits, uint32_t max_nits, * of the slider lands on (or near) the floor and the top on the panel's maximum. Callers keep max_nits within
uint32_t floor, uint32_t max) * 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; uint64_t full = (uint64_t)max_nits << 16;
if (hi <= lo || max <= floor) return floor; if (!full || max <= floor) return floor;
if (nits_q16 <= lo) return floor; if (nits_q16 >= full) return max;
if (nits_q16 >= hi) return max; return edp_bl_clamp((nits_q16 * max + full / 2) / full, floor, max);
return floor + (uint32_t)(((nits_q16 - lo) * (max - floor) + (hi - lo) / 2) / (hi - lo));
} }
static inline uint64_t edp_bl_level_to_nits(uint32_t level, uint32_t min_nits, uint32_t max_nits, /* A level's luminance under the same model (levels above max count as max). */
uint32_t floor, uint32_t 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 (!max) return 0;
if (hi <= lo || max <= floor || level <= floor) return lo; if (level > max) level = max;
if (level >= max) return hi; return ((uint64_t)level * ((uint64_t)max_nits << 16) + max / 2) / max;
return lo + ((uint64_t)(level - floor) * (hi - lo) + (max - floor) / 2) / (max - floor);
} }
/* IODisplayParameters "brightness" (0..EDP_BL_SLIDER_MAX) as nits, linear between the nominal ends. */ /* The darkest luminance the kext offers CoreBrightness (BrightnessMilliNits "min"): the configured minimum, raised to
static inline uint64_t edp_bl_slider_to_nits(uint64_t slider, uint32_t min_nits, uint32_t max_nits) * 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; 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_q16 <= lo_q16 || nits_q16 <= lo_q16) return 0;
if (hi <= lo || nits_q16 <= lo) return 0; if (nits_q16 >= hi_q16) return EDP_BL_SLIDER_MAX;
if (nits_q16 >= hi) 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));
return (uint32_t)(((nits_q16 - lo) * EDP_BL_SLIDER_MAX + (hi - lo) / 2) / (hi - lo));
} }
/* ---- Allow-lists: every MMIO access and every DPCD transaction the monitor makes passes one of these ----------- */ /* ---- 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 panel's brightness. The monitor gets `SNAP_EDP_BACKLIGHT`: its
`ADRENO_GUEST_BACKLIGHT` call writes the panel's DPCD backlight level over the `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 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 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 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 gets the AuxKC pinned as `INPUT_AUXKC_DESKTOP_BACKLIGHT`, whose AdrenoX2 has the
`AdrenoBacklight` service CoreBrightness drives, and the boot-arg `AdrenoBacklight` service, and the boot-arg `adreno_backlight=1`. CoreBrightness
`adreno_backlight=1`. The pinned collection, `next-20261024`, is built with 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 `KEXT_PROFILE=desktop-next I2C_PROFILE=desktop-fnkeys tools/build-auxkc-desktop.sh
NAME` for a monitor without `--gpu-coherent` (`desktop-next` is NAME` for a monitor without `--gpu-coherent` (`desktop-next` is
`desktop-nocoherent` plus the service); a `--gpu-coherent` run needs a `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 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]; } 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) static void test_helpers(void)
{ {
uint32_t bits = 0; 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(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_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_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++) { for (uint32_t n = 1; n <= 16; n++) {
uint32_t max = edp_bl_max_level(n), floor = edp_bl_floor_level(max); uint32_t max = edp_bl_max_level(n), floor = edp_bl_floor_level(max);
CHECK(floor >= 1 && floor <= max); CHECK(floor >= 1 && floor <= max);
CHECK((uint64_t)floor * 1000 >= (uint64_t)max * EDP_BL_FLOOR_PERMILLE); 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 && CHECK(edp_bl_clamp(0, 41, 2047) == 41 && edp_bl_clamp(5000, 41, 2047) == 2047 &&
edp_bl_clamp(1000, 205, 2047) == 1000 && edp_bl_clamp(UINT64_MAX, 205, 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(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); 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 */ test_curve();
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);
}
uint8_t caps[4] = {0x03, 0x07, 0x06, 0x10}; 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); 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 && 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 */ M.status = ST_DONE; /* a status the firmware left behind */
CHECK(call(EDP_BL_QUERY, 0, 0, r) == 0); 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(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); (r[3] >> 40 & 1) == 1);
CHECK(M.dpcd_writes == 0 && M.gos == 3 && M.violations == 0); 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(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); 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; unsigned gos = M.gos;
CHECK(call(EDP_BL_SET, 1500, 0, r) == 0 && M.gos == gos); /* unchanged: no traffic */ 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(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); 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[0x722] = 0x80; M.dpcd[0x723] = 0x55;
M.dpcd[0x724] = 8; M.dpcd[0x725] = 8; M.dpcd[0x726] = 8; 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 && 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(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); 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 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 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 DPCD panel, and checks the kext's nits -> level curve, and must PASS (ASan/UBSan). Each MUTATIONS entry weakens one
same program must FAIL on every 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 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 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 under ADRENO_BACKLIGHT and publishes what BrightnessControl needs to write to it (force-brightness-control), mapping
pinned. 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 --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. 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([]), []) 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') ENGINE = ('compat/edp-backlight.h', 'compat/edp-backlight.c', 'compat/dpu-intf.h')
MUTATIONS = ( 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);', ('no clamp', 'compat/edp-backlight.c', 'uint32_t level = edp_bl_clamp(request, s->floor, s->max);',
'uint32_t level = (uint32_t)request;'), 'uint32_t level = (uint32_t)request;'),
('mask bits dropped', 'compat/edp-backlight.c', ' << 1 | (status & EDP_INTR_MASK_BITS));', ' << 1);'), ('mask bits dropped', 'compat/edp-backlight.c', ' << 1 | (status & EDP_INTR_MASK_BITS));', ' << 1);'),
@@ -162,6 +174,21 @@ class Wiring(unittest.TestCase):
'ADRENO_BACKLIGHT') 'ADRENO_BACKLIGHT')
self.assertEqual(bad, []) 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): def test_cycle(self):
from cycle_lib import installed_fresh as f from cycle_lib import installed_fresh as f
chain = dict(variant='fast', input=True, input_touchpad=True, input_ptp=True, present_dartpool=True, 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') 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)') @unittest.skipUnless(ARGS.build, 'pass --build to compile the monitor (minutes)')
class Build(unittest.TestCase): class Build(unittest.TestCase):
def test_monitor(self): 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-')) out = Path(ARGS.out) / 'kext' if ARGS.out else Path(tempfile.mkdtemp(prefix='edp-backlight-kext-'))
env = dict(os.environ, ZERO_AR_DATE='1') env = dict(os.environ, ZERO_AR_DATE='1')
kext = ROOT / 'gpu/adreno-kext/kext/build.sh' 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 {})) e = dict(env, OUT=str(dest), **({'PROFILE': profile} if profile else {}))
b = subprocess.run(['sh', str(kext)], env=e, capture_output=True, text=True) b = subprocess.run(['sh', str(kext)], env=e, capture_output=True, text=True)
self.assertEqual(b.returncode, 0, b.stdout[-2000:] + b.stderr[-2000:]) 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), 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) 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) 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')], default = (out / 'default/AdrenoX2.kext/Contents/MacOS/AdrenoX2').read_bytes()
capture_output=True, text=True).stdout self.assertNotIn(b'force-brightness-control', default)
self.assertIn('__ZN13AdrenoDisplay20set_brightness_gatedEi', nm) for profile in profiles:
self.assertIn('__ZN15AdrenoBacklight10gMetaClassE', nm) 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__': if __name__ == '__main__':