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:
1 parent
b9c12cb158
commit
40556de8cd
2 files changed
+656
No files matched your search
Executable
+288
@@ -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())
|
||||
Executable
+368
@@ -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())
|
||||
Reference in new issue
Block a user