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:
commit
fbed1cad82
5 files changed
+198
-72
No files matched your search
+46
-31
@@ -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
@@ -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
|
||||||
|
|||||||
+1
-1
Submodule gpu/adreno-kext/kext updated: b5bfcb4092...9cc1e92ba9.
+64
-27
@@ -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
@@ -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__':
|
||||||
|
|||||||
Reference in new issue
Block a user