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.
This commit is contained in:
Acelogic committed 2026-10-24 19:47:06 -04:00
1 parent 8351bc0674
commit e273ff6475
1 file changed
+66 -7
+66 -7
View File
@@ -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__':