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);'),