tools: add the System Settings icon suites

test-adreno-settings-icons.py: 261 cases (52 Settings extension icons,
34 type icons at 20 pt and 4 at 64 pt, 67 app icons, 87 named images
of the extensions' asset catalogs, 3 MonitorPanel previews, 14 SF
Symbols) through iconservicesagent's generation entry, the client's CPU
redraw and a CARenderer, on this Mac's GPU answering like the plug-in
(the reference, with pixels) and on the plug-in device. Per case: a
valid reference image, the same image size, nothing omitted or failed,
the same CoreImage kernels as the reference, and every package admitted
by the kext validator. --expect-before requires the unchanged plug-in
to fail. --cs-samplers N stands in for a kext publishing
AdrenoIBComputeSamplers, --cs-samplers-validator replays through this
tree's validator built with ADRENO_IB_CS_SAMPLERS and opted in,
--allow-kext-gated reports the kernels waiting for the property.
Replays run in parallel (--jobs), --cases selects by name.

test-adreno-settings-ci-encoding.py decodes the CS T#, sampler, TSIZE
and SP_CS_CONFIG state of one CoreImage kernel per sampled format (13)
and of a two-input srcOver, with and without the property, checks a
replaceRegion/getBytes round trip of each format against this Mac's
GPU, and requires the production validator to refuse the 2-unit sampler
load that the opted-in validator admits.

Ported from agents/settings-ui (the scratch tree's --kext-tree and
--validator-tree proposal replaced by --cs-samplers-validator, replays
parallelised). Results on this Mac (26A428) in the handoff: the
unchanged plug-in fails 24 of 2873 icon checks and 27 of 126 encoding
checks; ADRENO_SETTINGS_ICONS passes 4713/4714 and 351.
This commit is contained in:
Acelogic committed 2026-10-24 03:19:47 -04:00
1 parent b9c12cb158
commit 40556de8cd
2 files changed
+656

No files matched your search

+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""The CoreImage compute shapes of the System Settings pane icons, decoded from the plug-in's packages (no GPU, no A16).
ADRENO_SETTINGS_ICONS (gpu/adreno-kext/plugin/AdrenoMetal/AdrenoDrawCommands/Compute.m): iconservicesagent's CoreImage
kernels sample RGBA8Unorm_sRGB (icns device and display images), RG8Unorm / RG16Float (grey+alpha document icons) and
composite several inputs in one kernel (folder, documents, app icons with effects). The harness (gpu/adreno-kext/plugin/harness/settings-ci-encoding.m) runs one
CoreImage kernel per sampled format and a two-input srcOver, twice: as the deployed kext answers (no
AdrenoIBComputeSamplers) and standing in for a kext that publishes AdrenoIBComputeSamplers = 8. Checks:
- every single-input kernel is encoded, nothing omitted; its package's CS T# for the source texture has the format's
FMT6, swap, own channel mapping and sRGB decode (Mesa fd6_format_table; A8 as the fragment sampler encodes it),
linear tiling, the texture's size and a 64-byte-multiple pitch; the single shape keeps one sampler unit, a T# table of
T#0 (null) + T#1 and SP_CS_CONFIG NTEX 1 / NSAMP 1 / NUAV 2;
- the two-input kernel: without the property it is omitted, naming AdrenoIBComputeSamplers, and no package carries a
CS sampler load of more than one unit; with it, it is encoded with a 3-entry T# table (null, the two inputs, each
relocated READ to its own texture with its own format), a 2-unit sampler table of clamping samplers, SP_CS_TSIZE 3
and SP_CS_CONFIG NTEX 3 / NSAMP 2;
- a replaceRegion/getBytes round trip of each format returns the written bytes on the plug-in device and on this
Mac's GPU (the CPU layout of the textures CoreImage uploads icons into);
- every package replays through the production validator, except the two-input package, which that validator must
refuse (the reason for the gate) and, with --cs-samplers-validator, this tree's validator built with
ADRENO_IB_CS_SAMPLERS and opted in (a kext booted with adreno_cs_samplers=1) must admit.
--expect-before: the unchanged plug-in must fail at least one of these.
python3 tools/test-adreno-settings-ci-encoding.py --bundle BUNDLE --output NEW-DIR [--expect-before]
[--cs-samplers-validator]
"""
import argparse
import importlib.util
import json
import os
from pathlib import Path
import struct
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[1]
HARNESS = ROOT / 'gpu/adreno-kext/plugin/harness/settings-ci-encoding.m'
def load(name, path):
spec = importlib.util.spec_from_file_location(name, ROOT / path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
T = load('targets', 'tools/test-adreno-targets.py')
P, hv = T.P, T.header_value
SB6_CS_TEX, SB6_CS_SHADER = hv('A8XX_E_SB6_CS_TEX'), hv('A8XX_E_SB6_CS_SHADER')
ST6_SHADER, ST6_CONSTANTS, ST6_UAV = hv('A8XX_E_ST6_SHADER'), hv('A8XX_E_ST6_CONSTANTS'), hv('A8XX_E_ST6_UAV')
CP_LOAD_STATE6_FRAG = P.R.CP_LOAD_STATE6_FRAG
REG_TSIZE, REG_CONFIG = hv('A8XX_REG_SP_CS_TSIZE'), hv('A8XX_REG_SP_CS_CONFIG')
# Metal format -> (TEX format, swap, the format's own channel mapping, sRGB decode, bytes per pixel)
EXPECT = {71: ('FMT6_8_8_8_8_UNORM', 'WZYX', 'XYZW', 1, 4), 81: ('FMT6_8_8_8_8_UNORM', 'WXYZ', 'XYZW', 1, 4),
80: ('FMT6_8_8_8_8_UNORM', 'WXYZ', 'XYZW', 0, 4), 30: ('FMT6_8_8_UNORM', 'WZYX', 'XY01', 0, 2),
65: ('FMT6_16_16_FLOAT', 'WZYX', 'XY01', 0, 4), 10: ('FMT6_8_UNORM', 'WZYX', 'X001', 0, 1),
1: ('FMT6_8_UNORM', 'WZYX', '000X', 0, 1), 25: ('FMT6_16_FLOAT', 'WZYX', 'X001', 0, 2),
20: ('FMT6_16_UNORM', 'WZYX', 'X001', 0, 2), 60: ('FMT6_16_16_UNORM', 'WZYX', 'XY01', 0, 4),
110: ('FMT6_16_16_16_16_UNORM', 'WZYX', 'XYZW', 0, 8), 94: ('FMT6_10_10_10_2_UNORM', 'WXYZ', 'XYZW', 0, 4),
90: ('FMT6_10_10_10_2_UNORM', 'WZYX', 'XYZW', 0, 4)}
SWZ = {'X': 'X', 'Y': 'Y', 'Z': 'Z', 'W': 'W', '0': 'ZERO', '1': 'ONE'}
GATE = '(AdrenoIBComputeSamplers)'
checks, failures = 0, []
def check(ok, what):
global checks
checks += 1
if not ok:
failures.append(what)
return ok
def u32(pkg, at):
return struct.unpack_from('<I', pkg['package'], at)[0]
def cs_state(pkg):
"""The CS texture/sampler/UAV loads of one package: {(block, type): (units, table byte offset)}, the T# entries'
resource relocations {table offset + 64k: reloc}, and the last SP_CS_TSIZE / SP_CS_CONFIG values."""
words = P.ib_words(pkg)
regs, packets = P.walk_ib(words)
by_lo = {r['lo']: r for r in pkg['relocs']}
loads = {}
for op, start, body in packets:
if op != CP_LOAD_STATE6_FRAG or len(body) < 3:
continue
w0 = body[0]
typ, block, units = (w0 >> 14) & 3, (w0 >> 18) & 0xf, w0 >> 22
if block not in (SB6_CS_TEX, SB6_CS_SHADER):
continue
src = by_lo.get(4 * (start + 1))
loads[(block, typ)] = (units, src['offset'] if src and src['kind'] == 2 else None)
values = {off: words[at] for at, off in regs}
return loads, by_lo, values.get(REG_TSIZE), values.get(REG_CONFIG)
def name_of(table, value):
try:
return T.enum(table, value)
except KeyError:
return f'{table}#{value}'
def tdesc(pkg, at):
d = [u32(pkg, at + 4 * k) for k in range(16)]
f = lambda w, desc, name: T.dfield(d[w], desc, name)
return dict(fmt=name_of('a6xx_format', f(3, 'A8XX_TEX_MEMOBJ_3', 'FMT')),
swap=name_of('a3xx_color_swap', f(3, 'A8XX_TEX_MEMOBJ_3', 'SWAP')),
swiz=[f(3, 'A8XX_TEX_MEMOBJ_3', f'SWIZ_{c}') for c in 'XYZW'],
srgb=f(4, 'A8XX_TEX_MEMOBJ_4', 'SRGB'), tile=f(4, 'A8XX_TEX_MEMOBJ_4', 'TILE_MODE'),
width=f(2, 'A8XX_TEX_MEMOBJ_2', 'WIDTH'), height=f(2, 'A8XX_TEX_MEMOBJ_2', 'HEIGHT'),
pitch=f(6, 'A8XX_TEX_MEMOBJ_6', 'TEX_LINE_OFFSET') // 8, words=d)
def packages(out, case):
pk = []
for c in case['commits']:
f = out / c['dir'] / 'commands.bin'
if f.is_file():
pk += [(c['dir'], P.parse_package(cmd)) for k, cmd in P.kernel_commands(f.read_bytes()) if k == 0x1002]
return pk
def check_texture(name, pkg, by_lo, table, slot, source):
exp = EXPECT.get(source['format'])
if not check(exp is not None, f'{name}: no expectation for format {source["format"]}'):
return
at = table + 64 * slot
rel = by_lo.get(at)
check(rel is not None and rel['kind'] == 1 and rel['resource_id'] == source['resource'] and rel['access'] == 1,
f'{name}: T#{slot} is not relocated READ to the source texture {source["resource"]}: {rel}')
d = tdesc(pkg, at)
want_swiz = [T.SWIZ[SWZ[c]] for c in exp[2]]
check((d['fmt'], d['swap'], d['swiz'], d['srgb'], d['tile']) == (exp[0], exp[1], want_swiz, exp[3], 0),
f'{name}: T#{slot} {d["fmt"]} {d["swap"]} swiz {d["swiz"]} srgb {d["srgb"]} tile {d["tile"]}; '
f'expected {exp[0]} {exp[1]} {want_swiz} srgb {exp[3]} linear')
check((d['width'], d['height']) == (source['width'], source['height']) and d['pitch'] % 64 == 0 and
d['pitch'] >= source['width'] * exp[4], f'{name}: T#{slot} size {d["width"]}x{d["height"]} pitch {d["pitch"]}')
def verify(out, report, gated, replays, proposal):
multi_refused_by_main = []
for case in report['cases']:
name = case['name']
multi = name == 'composite'
omissions = [o for c in case['commits'] for o in ((c.get('work') or {}).get('omissions') or [])]
# A strict build raises where a lenient one omits: the gated two-input kernel is then an exception naming it.
strict_gate = multi and gated and case.get('status') == 'exception' and GATE in (case.get('exception') or '')
check(case.get('status') == 'committed' or strict_gate, f'{name}: {case.get("status")} {case.get("exception")}')
if multi and gated:
check(strict_gate or (len(omissions) == 1 and GATE in (omissions[0].get('details') or {}).get('reason', '')),
f'{name}: without AdrenoIBComputeSamplers the two-input kernel must be omitted naming it: '
f'{[(o.get("details") or {}).get("reason") for o in omissions]}')
else:
check(not omissions, f'{name}: omitted: {[(o.get("details") or {}).get("reason") for o in omissions][:2]}')
if not multi:
rt = case.get('roundtrip') or {}
check(rt.get('adreno') and rt.get('native'), f'{name}: replaceRegion/getBytes round trip {rt}')
pks = packages(out, case)
sampler_units = []
for d, pkg in pks:
loads, by_lo, tsize, config = cs_state(pkg)
if (SB6_CS_TEX, ST6_SHADER) in loads:
sampler_units.append(loads[(SB6_CS_TEX, ST6_SHADER)][0])
if (SB6_CS_TEX, ST6_CONSTANTS) not in loads:
continue
units, table = loads[(SB6_CS_TEX, ST6_CONSTANTS)]
samplers = loads.get((SB6_CS_TEX, ST6_SHADER), (0, None))[0]
ntex = (config >> hv('A8XX_REG_SP_CS_CONFIG__NTEX__SHIFT')) & 0xff if config is not None else None
nsamp = (config >> hv('A8XX_REG_SP_CS_CONFIG__NSAMP__SHIFT')) & 0x1f if config is not None else None
nuav = (config >> hv('A8XX_REG_SP_CS_CONFIG__NUAV__SHIFT')) & 0x7f if config is not None else None
check(table is not None, f'{name}: CS T# load without a package table')
if table is None:
continue
if not multi:
check((units, samplers, tsize, ntex, nsamp, nuav) == (2, 1, 2, 1, 1, 2),
f'{name}: single shape T# {units} samplers {samplers} TSIZE {tsize} NTEX {ntex} NSAMP {nsamp} NUAV {nuav}')
check(not any(tdesc(pkg, table)['words']) and by_lo.get(table) is None, f'{name}: T#0 is not null')
check_texture(name, pkg, by_lo, table, 1, case['sources'][0])
else:
check((units, samplers, tsize, ntex, nsamp, nuav) == (3, 2, 3, 3, 2, 2),
f'{name}: two-input shape T# {units} samplers {samplers} TSIZE {tsize} NTEX {ntex} NSAMP {nsamp} NUAV {nuav}')
check(not any(tdesc(pkg, table)['words']) and by_lo.get(table) is None, f'{name}: T#0 is not null')
kernel = case['kernels'][0]['textures'] if case.get('kernels') else {}
by_res = {s['resource']: s for s in case['sources']}
for slot in (1, 2):
bound = kernel.get(str(slot)) or {}
check(bound.get('resource') in by_res, f'{name}: texture({slot}) is not one of the sources: {bound}')
if bound.get('resource') in by_res:
check_texture(name, pkg, by_lo, table, slot, by_res[bound['resource']])
st = loads[(SB6_CS_TEX, ST6_SHADER)][1]
for k in range(2):
w = [u32(pkg, st + 16 * k + 4 * i) for i in range(4)]
wraps = [T.dfield(w[0], 'A8XX_TEX_SAMP_0', f'WRAP_{a}') for a in 'STR']
check(any(w) and all(name_of('a6xx_tex_clamp', x) in ('A6XX_TEX_CLAMP_TO_EDGE', 'A6XX_TEX_CLAMP_TO_BORDER')
for x in wraps), f'{name}: sampler {k} words {w} wraps {wraps}')
for label, exe in replays:
rd = out / f'replay-{label}' / d
rd.mkdir(parents=True, exist_ok=True)
(rd / 'commands.bin').write_bytes((out / d / 'commands.bin').read_bytes())
try:
T_ds.replay(exe, rd, f'{label}:{d}', window=0x3a00000)
admitted = ''
except Exception as error:
admitted = str(error) or 'refused'
if multi and samplers > 1 and label == 'main':
multi_refused_by_main.append(bool(admitted))
check(bool(admitted), f'{d}: the production validator admitted a {samplers}-unit CS sampler load')
elif multi and samplers > 1 and not proposal:
continue
else:
check(not admitted, f'{d}: {label} kext validator: {admitted[:300]}')
if gated or not multi:
check(all(n <= 1 for n in sampler_units), f'{name}: a CS sampler load of {sampler_units} units without the property')
if multi and not gated:
check(any(n == 2 for n in sampler_units), f'{name}: no 2-unit CS sampler load: {sampler_units}')
return multi_refused_by_main
T_ds = None
def main():
global T_ds
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--bundle', type=Path, required=True)
ap.add_argument('--output', type=Path, required=True)
ap.add_argument('--expect-before', action='store_true')
ap.add_argument('--cs-samplers-validator', action='store_true',
help='also replay through this tree\'s validator built with ADRENO_IB_CS_SAMPLERS and opted in')
args = ap.parse_args()
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
exe = out / 'settings-ci-encoding'
subprocess.run(['xcrun', 'clang', '-arch', 'arm64e', '-fobjc-arc', '-O1', '-Wall', '-Werror', str(HARNESS), '-o', str(exe),
'-framework', 'Foundation', '-framework', 'Metal', '-framework', 'CoreImage', '-framework', 'CoreGraphics',
'-framework', 'IOKit'], check=True)
subprocess.run(['codesign', '-f', '-s', '-', str(exe)], check=True, capture_output=True)
T_ds = load('depth_stencil', 'tools/test-adreno-depth-stencil.py')
replays = []
if not args.expect_before:
replays.append(('main', T_ds.build_replay(out, guest_size=0x4000000, arena=0x3c00000, fixtures=160, capacity=320,
name='kernel-replay-64m', arena_pages=129)))
if args.cs_samplers_validator:
st = load('settings_icons', 'tools/test-adreno-settings-icons.py')
replays.append(('cs-samplers', st.build_replay_at(T_ds, ROOT, out, 'kernel-replay-cs-samplers',
st.CS_SAMPLERS_DEFINES)))
results = {}
for label, units in (('deployed-kext', None), ('cs-samplers-8', 8)):
cap = out / label
env = dict(os.environ)
env.pop('SETTINGS_ICONS_CS_SAMPLERS', None)
if units:
env['SETTINGS_ICONS_CS_SAMPLERS'] = str(units)
r = subprocess.run([str(exe), str(args.bundle.resolve()), str(cap)], capture_output=True, text=True, env=env, timeout=600)
(out / f'{label}.log').write_text(r.stdout + r.stderr)
if not check(r.returncode == 0 and (cap / 'report.json').is_file(), f'{label}: harness exit {r.returncode}'):
continue
report = json.loads((cap / 'report.json').read_text())
use = [x for x in replays if units or x[0] != 'cs-samplers']
refused = verify(cap, report, gated=units is None, replays=use, proposal=args.cs_samplers_validator)
results[label] = dict(cases=len(report['cases']), multi_refused_by_main=refused)
if units and not args.expect_before:
check(refused and all(refused), f'{label}: the production validator must refuse the two-input package (gate)')
result = dict(checks=checks, failures=failures, runs=results, expect_before=args.expect_before,
validators=[label for label, _ in replays])
(out / 'result.json').write_text(json.dumps(result, indent=1))
if args.expect_before:
if failures:
print(f'PASS: the unchanged plug-in fails {len(failures)} of {checks} encoding checks (first: {failures[0][:200]}); '
f'{out / "result.json"}')
return 0
print(f'FAIL: the unchanged plug-in passed every encoding check; {out / "result.json"}')
return 1
if failures:
for f in failures[:40]:
print(f'FAIL {f[:400]}')
print(f'FAIL: {len(failures)} of {checks} encoding checks; {out / "result.json"}')
return 1
runs = ', '.join(f'{k}: {v["cases"]} cases' for k, v in results.items())
print(f'PASS: {checks} encoding checks ({runs}; validators {[label for label, _ in replays]}); {out / "result.json"}')
return 0
if __name__ == '__main__':
sys.exit(main())
+368
View File
@@ -0,0 +1,368 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""System Settings pane icons through the plug-in device, against this Mac's GPU (no GPU submission, no A16).
ADRENO_SETTINGS_ICONS (gpu/adreno-kext/plugin/AdrenoMetal/AdrenoDrawCommands/Compute.m): the pane rows' icons
(AsyncIconImage) come from iconservicesagent's ISGenerationRequest -generateImage on the system default Metal device, are redrawn by the client on the CPU and drawn
by WindowServer's Core Animation. The harness (gpu/adreno-kext/plugin/harness/settings-icons.m) runs those stages for
every Settings extension icon, the device/display/storage/document type icons the panes show, the system apps' icons
(Login Items, Notifications, Privacy lists), named images from the Settings extensions' asset catalogs, MonitorPanel's
display previews and SF Symbols, twice: --host-caps (this Mac's GPU answering like the plug-in device: Apple's
reference, with pixels) and --adreno (the plug-in device as the system default; commits intercepted). Per case:
- reference: this Mac's GPU produces a non-transparent image and draws it (else the case is invalid, and counted);
- the plug-in run produces an image of the same pixel size;
- every generate/display commit encodes work, omits nothing and fails no encoding; no exception, no pipeline error;
- CoreImage dispatches as many kernels on the plug-in device as on this Mac's GPU (same work);
- every commit's kernel commands replay through the production kext validator (and, with --validator-tree, through
that tree's validator sources too).
--expect-before: the unchanged plug-in must fail at least one of these.
Multi-input CoreImage kernels need more than one CS sampler unit, which the validator admits only in a kext built with
ADRENO_IB_CS_SAMPLERS and booted with adreno_cs_samplers=1; an ADRENO_SETTINGS_ICONS plug-in encodes them only when the
kext publishes AdrenoIBComputeSamplers. --cs-samplers N stands in for such a kext (replay those packages with
--cs-samplers-validator, this tree's validator built and opted in that way, and --skip-main-validator);
--allow-kext-gated reports the kernels refused for want of the property without failing on them. --jobs runs that many
validator replays at once (each commit's replay is independent); --cases keeps the cases whose name matches a regex.
python3 tools/test-adreno-settings-icons.py --bundle BUNDLE --output NEW-DIR [--expect-before] [--quick]
[--cases REGEX] [--validator-tree TREE]... [--cs-samplers-validator] [--skip-main-validator]
[--cs-samplers N] [--allow-kext-gated] [--reference HOST-CAPS-RUN-DIR] [--jobs N]
"""
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
import importlib.util
import json
import os
from pathlib import Path
import plistlib
import re
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[1]
HARNESS = ROOT / 'gpu/adreno-kext/plugin/harness'
EXT = Path('/System/Library/ExtensionKit/Extensions')
# Type icons the panes show (About/Storage/Displays/Bluetooth/Network/Time Machine/Privacy rows, documents, folders).
TYPES = ['public.folder', 'public.plain-text', 'public.volume', 'public.display', 'public.disk-image', 'public.item',
'public.image', 'public.movie', 'public.audio', 'com.apple.application-bundle', 'com.apple.network-neighborhood',
'com.apple.storage-external', 'com.apple.generic-time-machine-disk', 'com.apple.macbookpro-16-space-gray',
'com.apple.macbookair-13-2022-midnight', 'com.apple.imac-2021-blue', 'com.apple.macmini', 'com.apple.studio-display',
'com.apple.pro-display-xdr', 'com.apple.cinema-display', 'com.apple.magic-mouse', 'com.apple.airpods-pro',
'com.apple.iphone', 'com.apple.ipad', 'com.apple.application-icon.icloud', 'com.apple.application-icon.icloud-storage',
'com.apple.graphic-icon.bluetooth', 'com.apple.graphic-icon.display', 'com.apple.graphic-icon.usb',
'com.apple.graphic-icon.local-network', 'com.apple.graphic-icon.external-drive', 'com.apple.graphic-icon.privacy',
'com.apple.graphic-icon.trackpad-and-mouse', 'com.apple.graphic-icon.icloud-backup']
LARGE_TYPES = ['com.apple.macbookpro-16-space-gray', 'com.apple.studio-display', 'public.display', 'com.apple.storage-external']
SYMBOLS = [('wifi', None), ('network', None), ('gear', None), ('externaldrive.fill', None), ('lock.fill', None),
('person.crop.circle.fill', 'palette'), ('battery.100percent', 'multicolor'), ('bell.badge.fill', 'multicolor'),
('sun.max.fill', 'multicolor'), ('speaker.wave.3.fill', None), ('display', None), ('keyboard', None),
('hand.raised.fill', None), ('cloud.fill', 'multicolor')]
MONITORPANEL = Path('/System/Library/PrivateFrameworks/MonitorPanel.framework/Versions/A/Resources')
def settings_extensions():
out = []
for info in sorted(EXT.glob('*.appex/Contents/Info.plist')):
try:
d = plistlib.loads(info.read_bytes())
except Exception:
continue
if (d.get('EXAppExtensionAttributes') or {}).get('EXExtensionPointIdentifier') == 'com.apple.Settings.extension.ui':
out.append(info.parents[1])
return out
def asset_cases(exts, per_kind):
"""Named images of the Settings extensions' own asset catalogs, a few per (catalog, encoding)."""
cases = []
for ext in exts:
car = ext / 'Contents/Resources/Assets.car'
if not car.is_file():
continue
r = subprocess.run(['assetutil', '--info', str(car)], capture_output=True, text=True)
try:
rows = json.loads(r.stdout)[1:]
except Exception:
continue
picked = {}
for row in rows:
if row.get('AssetType') not in ('Image', 'PackedImage', 'Icon Image', 'MultiSized Image', 'Vector'):
continue
if '/' in (row.get('Name') or '') or (row.get('Name') or '').startswith('ZZZZPackedAsset'):
continue # a layer of an icon stack or a packed-asset atlas, not a named image
enc = row.get('Encoding') or row.get('AssetType')
names = picked.setdefault(enc, [])
if row.get('Name') and row['Name'] not in names and len(names) < per_kind:
names.append(row['Name'])
for enc, names in sorted(picked.items()):
for n in names:
cases.append(dict(name=f'asset-{ext.stem}-{enc}-{n}', kind='asset', path=str(ext), asset=n, size=32, scale=2))
return cases
def make_cases(quick, only=None):
exts = settings_extensions()
cases = [dict(name=f'ext-{e.stem}', kind='bundle', path=str(e), size=20, scale=2) for e in exts]
cases += [dict(name=f'type-{t}', kind='type', type=t, size=20, scale=2) for t in TYPES]
cases += [dict(name=f'type64-{t}', kind='type', type=t, size=64, scale=2) for t in LARGE_TYPES]
apps = sorted(Path('/System/Applications').glob('*.app')) + sorted(Path('/System/Applications/Utilities').glob('*.app'))
apps += [Path('/Applications/Safari.app'), Path('/System/Library/CoreServices/Finder.app')]
cases += [dict(name=f'app-{a.stem}', kind='bundle', path=str(a), size=20, scale=2) for a in apps if a.exists()]
cases += asset_cases(exts, 1 if quick else 2)
cases += [dict(name=f'file-{p.stem}', kind='file', path=str(p), size=64, scale=2) for p in sorted(MONITORPANEL.glob('*.tiff'))]
for name, mode in SYMBOLS:
c = dict(name=f'symbol-{name}' + (f'-{mode}' if mode else ''), kind='symbol', symbol=name, size=20, scale=2)
if mode:
c[mode] = True
cases.append(c)
if quick:
keep = re.compile(r'ext-(General|Power|Headphone|TimeMachine|Displays)|type-public\.(folder|plain-text|display)$|'
r'type-com\.apple\.macbookpro|type64-com\.apple\.studio|app-(Safari|Calculator|Terminal)|asset-|file-|symbol-(wifi|battery)')
cases = [c for c in cases if keep.search(c['name'])]
if only:
cases = [c for c in cases if re.search(only, c['name'])]
names = [c['name'] for c in cases]
assert len(names) == len(set(names)), 'duplicate case names'
return cases
def load_ds():
spec = importlib.util.spec_from_file_location('depth_stencil', ROOT / 'tools/test-adreno-depth-stencil.py')
ds = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ds)
return ds
# The validator of a kext built with ADRENO_IB_CS_SAMPLERS and booted with adreno_cs_samplers=1 (8 CS sampler units).
CS_SAMPLERS_DEFINES = ['-DADRENO_IB_CS_SAMPLERS=1', '-DIB_REPLAY_CS_SAMPLER_UNITS=8']
def build_replay_at(ds, root, out, name, defines=()):
"""ds.build_replay with the validator sources of a tree (this one or another) and extra defines."""
sources = ['tools/test-adreno-ib-command.c', 'compat/adreno-ib-command.c', 'compat/adreno-ib.c', 'compat/adreno-fill-command.c',
'compat/adreno-fill.c', 'compat/adreno-submit.c', 'compat/adreno-resource.c', 'compat/adreno-resource-list.c',
'compat/adreno.c', 'compat/irq-forward.c', 'shadow/stage2.c', 'shadow/shadow.c', 'shadow/mmu.c']
exe = out / name
flags = ['clang', '-std=gnu11', '-Icompat', '-Ishadow', '-DGUEST_SIZE=0x4000000', '-DIB_ARENA_OFFSET=0x3c00000',
'-DIB_FIXTURES=160', '-DIB_REPLAY_CAPACITY=320', '-DIB_ARENA_PAGES=129u', '-O1', '-g', '-Wall', '-Wextra', '-Werror',
*defines]
r = subprocess.run(flags + sources + ['-o', str(exe)], cwd=root, capture_output=True, text=True, timeout=300)
(out / f'{name}-build.log').write_text(r.stdout + r.stderr)
if r.returncode:
raise SystemExit(f'FAIL: {name} build failed (see {out / (name + "-build.log")})')
return exe
def run_harness(exe, lib, mode, bundle, out, cases_file, cs_samplers=None):
env = dict(os.environ, OS_ACTIVITY_DT_MODE='1')
env.pop('SETTINGS_ICONS_CS_SAMPLERS', None)
if mode == '--adreno':
env['DYLD_INSERT_LIBRARIES'] = str(lib)
if cs_samplers:
env['SETTINGS_ICONS_CS_SAMPLERS'] = str(cs_samplers)
r = subprocess.run([str(exe), mode, str(bundle), str(out), str(cases_file)], capture_output=True, text=True, env=env, timeout=3600)
log = r.stdout + r.stderr
return r.returncode, log
def attribute(log):
"""os_log lines per case (the harness prints '@@case NAME STAGE' before each stage)."""
per, current = {}, None
for line in log.splitlines():
if line.startswith('@@case '):
current = line.split()[1]
continue
if current:
per.setdefault(current, []).append(line)
return per
def compute_counts(commits):
enc = om = 0
for c in commits:
for e in ((c.get('work') or {}).get('encoders') or {}).values():
if e.get('kind') == 'compute':
enc += int(e.get('encoded') or 0)
om += int(e.get('omitted') or 0)
return enc, om
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--bundle', type=Path, required=True)
ap.add_argument('--output', type=Path, required=True)
ap.add_argument('--expect-before', action='store_true')
ap.add_argument('--quick', action='store_true', help='a representative subset (about 40 cases)')
ap.add_argument('--cases', help='only the cases whose name matches this regular expression')
ap.add_argument('--reference', type=Path, help='reuse a --host-caps run (its report.json) instead of running one')
ap.add_argument('--validator-tree', type=Path, action='append', default=[], help='also replay through this tree\'s validator')
ap.add_argument('--cs-samplers-validator', action='store_true',
help='also replay through this tree\'s validator built with ADRENO_IB_CS_SAMPLERS and opted in')
ap.add_argument('--skip-main-validator', action='store_true', help='do not replay through this tree\'s production validator')
ap.add_argument('--jobs', type=int, default=min(8, os.cpu_count() or 1), help='validator replays run at once')
ap.add_argument('--cs-samplers', type=int, help='stand in for a kext publishing AdrenoIBComputeSamplers = N')
ap.add_argument('--allow-kext-gated', action='store_true',
help='kernels refused only for want of AdrenoIBComputeSamplers are reported, not failed')
args = ap.parse_args()
out = args.output.resolve()
out.mkdir(parents=True, exist_ok=False)
build = subprocess.run(['sw_vers', '-buildVersion'], capture_output=True, text=True).stdout.strip()
if build != '26A428':
print(f'FAIL: host build {build}, not 26A428')
return 1
lib, exe = out / 'default-device-interpose.dylib', out / 'settings-icons'
subprocess.run(['xcrun', 'clang', '-dynamiclib', '-arch', 'arm64e', str(HARNESS / 'default-device-interpose.m'), '-o', str(lib),
'-framework', 'Foundation', '-framework', 'Metal'], check=True)
subprocess.run(['xcrun', 'clang', '-arch', 'arm64e', '-fobjc-arc', '-O1', '-Wall', '-Werror', str(HARNESS / 'settings-icons.m'),
'-o', str(exe), '-framework', 'AppKit', '-framework', 'QuartzCore', '-framework', 'Metal', '-framework', 'IOSurface',
'-framework', 'IOKit', '-framework', 'CoreGraphics'], check=True)
for f in (lib, exe):
subprocess.run(['codesign', '-f', '-s', '-', str(f)], check=True, capture_output=True)
cases = make_cases(args.quick, args.cases)
cases_file = out / 'cases.json'
cases_file.write_text(json.dumps(cases, indent=1))
bundle = args.bundle.resolve()
if args.reference:
ref = json.loads((args.reference / 'report.json').read_text())
ref_log = (args.reference.parent / 'host-caps.log').read_text() if (args.reference.parent / 'host-caps.log').is_file() else ''
else:
rc, ref_log = run_harness(exe, lib, '--host-caps', bundle, out / 'host-caps', cases_file)
(out / 'host-caps.log').write_text(ref_log)
if rc or not (out / 'host-caps/report.json').is_file():
print(f'FAIL: host-caps harness exit {rc}; {out / "host-caps.log"}')
return 1
ref = json.loads((out / 'host-caps/report.json').read_text())
rc, log = run_harness(exe, lib, '--adreno', bundle, out / 'adreno', cases_file, args.cs_samplers)
(out / 'adreno.log').write_text(log)
failures, checks, invalid = [], 0, []
def check(ok, what):
nonlocal checks
checks += 1
if not ok:
failures.append(what)
check(rc == 0 and (out / 'adreno/report.json').is_file(), f'adreno harness exit {rc}')
if failures:
print(f'FAIL: {failures[0]}; {out / "adreno.log"}')
return 1
got = json.loads((out / 'adreno/report.json').read_text())
refs = {c['name']: c for c in ref['cases']}
lines = attribute(log)
ds = load_ds()
replays = []
if not args.expect_before and not args.skip_main_validator:
replays.append(('main', ds.build_replay(out, guest_size=0x4000000, arena=0x3c00000, fixtures=160, capacity=320,
name='kernel-replay-64m', arena_pages=129)))
if not args.expect_before:
if args.cs_samplers_validator:
replays.append(('cs-samplers', build_replay_at(ds, ROOT, out, 'kernel-replay-cs-samplers', CS_SAMPLERS_DEFINES)))
for n, tree in enumerate(args.validator_tree):
replays.append((f'tree{n}', build_replay_at(ds, tree.resolve(), out, f'kernel-replay-tree{n}')))
gated, pending = {}, []
summary, omitted_reasons = {}, {}
for c in got['cases']:
name = c['name']
r = refs.get(name) or {}
ref_img = r.get('client') or {}
ref_ok = bool(r.get('client_coverage')) and all(f.get('changed_pixels', 0) > 0 for f in (r.get('display') or {}).get('frames', [{}]))
if not ref_ok:
invalid.append(name)
continue
img = c.get('client') or {}
check(bool(img) and img.get('width') == ref_img.get('width') and img.get('height') == ref_img.get('height'),
f'{name}: image {img.get("width")}x{img.get("height")}, reference {ref_img.get("width")}x{ref_img.get("height")}')
check(not c.get('exception'), f'{name}: exception {c.get("exception")}')
errors = [l for l in lines.get(name, []) if 'pipeline error: ' in l]
check(not errors, f'{name}: pipeline errors {errors[:2]}')
stages = c.get('stages') or {}
row = {}
for stage in ('generate', 'display'):
commits = (stages.get(stage) or {}).get('commits', [])
ref_census = ((r.get('stages') or {}).get(stage) or {}).get('census') or {}
got_census = (stages.get(stage) or {}).get('census') or {}
omissions = [o for x in commits for o in ((x.get('work') or {}).get('omissions') or [])]
if args.allow_kext_gated:
kept = [o for o in omissions if '(AdrenoIBComputeSamplers)' not in ((o.get('details') or {}).get('reason') or '')]
if len(kept) != len(omissions):
gated[name] = sorted({((o.get('details') or {}).get('pipeline') or {}).get('entry', '?') for o in omissions
if o not in kept})
omissions = kept
for o in omissions:
d = o.get('details') or {}
k = (d.get('reason') or '?')[:140]
omitted_reasons.setdefault(k, set()).add(name)
check(not omissions, f'{name}/{stage}: {len(omissions)} omitted: ' + '; '.join(sorted({(o.get("details") or {}).get("reason", "?")[:160] for o in omissions})[:2]))
check(not any((x.get('work') or {}).get('encoding_failed') for x in commits), f'{name}/{stage}: an encoding failed')
if stage == 'generate':
# Same work: CoreImage dispatches the same kernels, as often, on the plug-in device as on this Mac's GPU
# answering like it. (Other compute work, e.g. RenderBox's, may differ with the device and is not compared.)
# Without a GPU a flattened icon stack is transparent and IconServices retries ("Central pixel is still
# transparent even with retry"; os_log drops some of these lines under load), adding Lanczos passes:
# extra kernels are allowed with that line or for an icon-stack flatten (RenderBox draws on this Mac).
want = Counter(k for k in ref_census.get('kernels', []) if k.startswith('ci_'))
have = Counter(k for k in got_census.get('kernels', []) if k.startswith('ci_'))
retried = any('Central pixel is still transparent' in l for l in lines.get(name, [])) or ref_census.get('draws', 0) > 0
check(not (want - have), f'{name}/generate: CoreImage kernels the plug-in did not run: {sorted((want - have).elements())[:3]}')
check(not (have - want) or retried, f'{name}/generate: extra CoreImage kernels without a retry: '
f'{sorted((have - want).elements())[:3]}')
enc, om = compute_counts(commits)
check(enc + om == got_census.get('dispatches', 0), f'{name}/generate: {enc}+{om} compute dispatches in the work '
f'reports, {got_census.get("dispatches")} counted')
if stage == 'display':
check(bool(commits), f'{name}/display: no commits')
for label, rexe in replays:
for x in commits:
d = out / 'adreno' / x['dir']
if not (d / 'commands.bin').is_file():
continue
# A commit without an IB package (an empty command buffer: one 0x1000 header) has nothing to admit.
if not any(kind == 0x1002 for kind, _ in ds.P.kernel_commands((d / 'commands.bin').read_bytes())):
continue
if label != 'main':
d2 = out / f'replay-{label}' / x['dir']
d2.mkdir(parents=True, exist_ok=True)
(d2 / 'commands.bin').write_bytes((d / 'commands.bin').read_bytes())
d = d2
pending.append((label, rexe, d, x['dir']))
row[stage] = dict(commits=len(commits), omitted=len(omissions))
summary[name] = row
def replay_one(task):
label, rexe, d, rel = task
try:
ds.replay(rexe, d, f'{label}:{rel}', window=0x3a00000)
return ''
except Exception as error: # the replay module's own failure class
return str(error) or 'refused'
# Each replay reads only its own commit's capture and writes only its own replay directory.
with ThreadPoolExecutor(max_workers=max(1, args.jobs)) as pool:
for (label, _rexe, _d, rel), admitted in zip(pending, pool.map(replay_one, pending)):
check(not admitted, f'{rel}: {label} kext validator: {admitted[:300]}')
check(len(invalid) <= max(3, len(got['cases']) // 10), f'{len(invalid)} cases without a reference image: {invalid[:8]}')
result = dict(checks=checks, failures=failures, invalid=invalid, cases=len(got['cases']), summary=summary, kext_gated=gated,
cs_samplers=args.cs_samplers, validators=[label for label, _ in replays],
omitted_reasons={k: sorted(v) for k, v in omitted_reasons.items()}, expect_before=args.expect_before)
(out / 'result.json').write_text(json.dumps(result, indent=1))
if args.expect_before:
if failures:
print(f'PASS: the unchanged plug-in fails {len(failures)} of {checks} settings-icon checks '
f'({len(omitted_reasons)} omission reasons; first: {failures[0][:200]}); {out / "result.json"}')
return 0
print(f'FAIL: the unchanged plug-in passed every settings-icon check; {out / "result.json"}')
return 1
if failures:
for f in failures[:60]:
print(f'FAIL {f[:400]}')
print(f'FAIL: {len(failures)} of {checks} settings-icon checks ({len(got["cases"])} cases, {len(invalid)} invalid); '
f'{out / "result.json"}')
return 1
extra = f'; {len(gated)} cases with kernels awaiting AdrenoIBComputeSamplers: {sorted(gated)}' if gated else ''
print(f'PASS: {checks} settings-icon checks, {len(got["cases"])} cases ({len(invalid)} without a reference image), '
f'validators {[label for label, _ in replays]}{extra}; {out / "result.json"}')
return 0
if __name__ == '__main__':
sys.exit(main())