From d43df34e409222f4a5a8ef5ed4741012e89f82bc Mon Sep 17 00:00:00 2001 From: Acelogic <7362641+Acelogic@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:31:58 -0400 Subject: [PATCH] tools: add the icon-pixels guest pixel test The A16 showed partial System Settings icons while neither the plug-in nor the kext refused anything, so the GPU ran the icon kernels and some produced wrong or empty pixels; the encoding tests cannot see that. build-icon-pixels.py builds and ad-hoc signs SnapMetal's icon-pixels for arm64e and records its size, SHA-256 and CDHash (and the trust row the guest would need if its AMFI refused an ad-hoc executable). run-icon-pixels.py runs it in the guest from the root console shell: pushes it with deploy-guest-plugin.py's checked chunk transfer, checks that it executes, runs the synthetic CoreImage cases as root and the system icons both in-process (root) and through NSWorkspace as the console user (launchctl asuser + sudo -u), fetches every output file three CRC-checked lines per command, optionally renames the icon caches aside and restarts iconservicesd, iconservicesagent, the Dock and System Settings (multi-user), collects the plug-in's per-process and unified-log diagnostics, and compares. Offline it refuses the steps that would act on this Mac's own processes. compare-icon-pixels.py compares the guest's lines with this Mac's within a tolerance and names the first matching explanation per case: unwritten or partial output, readback or repeat differences, black or transparent output, swapped red/blue, an extra or missing sRGB decode, the missing or foreign input of a multi-input kernel (with its Metal texture index), a broken blend band, otherwise the channels and tiles that are off; for icons empty, black, partial, wrong colour or shape. --census-only compares the dispatched kernels and formats of an intercepted plug-in run. icon-pixels-reference-synth.txt is this Mac's synthetic reference (generated inputs only; M3 Max, 26A428, capabilities answered like the deployed plug-in 378ff525). The icons reference summarises Apple's icons and is not committed. test-icon-pixels.py: the reference reproduces exactly on this Mac; the plug-in device (commits intercepted) dispatches the same kernels and formats with 8 CS sampler units and drops the multi-input ones without; 20 injected faults are each diagnosed and a within-tolerance perturbation passes; the whole console procedure runs over the console stand-in with loss and noise and fetches lines identical to a direct run. --- tools/build-icon-pixels.py | 78 +++++++ tools/compare-icon-pixels.py | 312 ++++++++++++++++++++++++++ tools/icon-pixels-reference-synth.txt | 79 +++++++ tools/run-icon-pixels.py | 233 +++++++++++++++++++ tools/test-icon-pixels.py | 306 +++++++++++++++++++++++++ 5 files changed, 1008 insertions(+) create mode 100644 tools/build-icon-pixels.py create mode 100644 tools/compare-icon-pixels.py create mode 100644 tools/icon-pixels-reference-synth.txt create mode 100644 tools/run-icon-pixels.py create mode 100644 tools/test-icon-pixels.py diff --git a/tools/build-icon-pixels.py b/tools/build-icon-pixels.py new file mode 100644 index 0000000..56d1ea3 --- /dev/null +++ b/tools/build-icon-pixels.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Build and ad-hoc sign the icon-pixels guest tool (arm64e), without running it in the guest. + +The tool (gpu/adreno-kext/plugin/harness/icon-pixels*.m, SnapMetal) runs the CoreImage kernels +of the System Settings pane icons on the default Metal device and prints checksummed lines per case; see its header. +Built like the other guest probes (tools/build-adreno-clear-probe.py): xcrun clang -arch arm64e for macOS 27, ad-hoc +signature, architecture and signature checked. Writes OUTPUT/icon-pixels and OUTPUT/build.json (size, SHA-256, CDHash, +compiler, command lines, source SHA-256s), and prints the record. OUTPUT must not exist. + + python3 tools/build-icon-pixels.py --output build/icon-pixels/tool-YYYYMMDD + +The CDHash is the trust-cache row the guest needs only if its AMFI refuses an ad-hoc arm64e executable +(tools/run-icon-pixels.py checks that first and stops with this hint). +""" +from pathlib import Path +import argparse +import hashlib +import json +import re +import subprocess + +ROOT = Path(__file__).resolve().parents[1] +HARNESS = ROOT / 'gpu/adreno-kext/plugin/harness' +SOURCES = [HARNESS / 'icon-pixels.m', HARNESS / 'icon-pixels-census.m', HARNESS / 'icon-pixels-icons.m', + HARNESS / 'icon-pixels.h'] +FRAMEWORKS = ['Foundation', 'Metal', 'CoreImage', 'CoreGraphics', 'IOKit', 'AppKit', 'UniformTypeIdentifiers'] + + +def sha(p): + return hashlib.sha256(Path(p).read_bytes()).hexdigest() + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('--output', type=Path, required=True) + a = ap.parse_args() + out = a.output.resolve() + out.mkdir(parents=True, exist_ok=False) + binary = out / 'icon-pixels' + sources = {str(p.relative_to(ROOT)): sha(p) for p in SOURCES} + commands = [] + + def run(name, argv): + commands.append(argv) + r = subprocess.run(argv, cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + (out / f'{name}.log').write_text(r.stdout) + if r.returncode: + raise SystemExit(f'{name} failed ({r.returncode}): {out / (name + ".log")}') + return r.stdout + compiler = run('compiler', ['xcrun', 'clang', '--version']).splitlines()[0] + argv = ['xcrun', 'clang', '-arch', 'arm64e', '-mmacosx-version-min=27.0', '-fobjc-arc', '-fobjc-arc-exceptions', '-O2', + '-Wall', '-Wextra', '-Werror', '-Wno-unused-parameter', + *[str(p.relative_to(ROOT)) for p in SOURCES if p.suffix == '.m'], '-o', str(binary)] + for f in FRAMEWORKS: + argv += ['-framework', f] + # -x drops local symbols: a smaller binary is fewer console chunks to push. + run('build', argv + ['-lz', '-Wl,-dead_strip', '-Wl,-x']) + run('sign', ['codesign', '-f', '-s', '-', str(binary)]) + run('verify-signature', ['codesign', '--verify', '--strict', '--verbose=2', str(binary)]) + arch = run('architecture', ['lipo', '-archs', str(binary)]).strip() + if arch != 'arm64e': + raise SystemExit(f'architecture {arch}, not arm64e') + signing = run('signature', ['codesign', '-dvvv', str(binary)]) + cdhash, = re.findall(r'^CDHash=([0-9a-f]+)$', signing, re.M) + version = run('version', [str(binary), 'version']).splitlines()[0] + if sources != {k: sha(ROOT / k) for k in sources}: + raise SystemExit('a source changed during the build') + record = dict(status='ICON_PIXELS_BUILT_NOT_RUN_ON_A16', binary=str(binary), bytes=binary.stat().st_size, + sha256=sha(binary), cdhash=cdhash, architecture=arch, compiler=compiler, host_version_line=version, + sources_sha256=sources, commands=commands, + trust_row=dict(name='icon-pixels', cdhash=cdhash, category=2, flags=0)) + (out / 'build.json').write_text(json.dumps(record, indent=1) + '\n') + print(json.dumps({k: record[k] for k in ('status', 'binary', 'bytes', 'sha256', 'cdhash')}, indent=1)) + + +if __name__ == '__main__': + main() diff --git a/tools/compare-icon-pixels.py b/tools/compare-icon-pixels.py new file mode 100644 index 0000000..cba18ac --- /dev/null +++ b/tools/compare-icon-pixels.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Compare icon-pixels output from the guest's GPU with this Mac's reference, case by case. + +icon-pixels (SnapMetal harness/icon-pixels.m, built by tools/build-icon-pixels.py) prints one checksummed line per case. +This reads two such outputs (lines whose CRC does not match, such as console noise or a torn line, are dropped and +counted) and prints PASS or FAIL per case with what is off: + + python3 tools/compare-icon-pixels.py REFERENCE.txt GUEST.txt [--tolerance 6] [--census-only] [--cases REGEX] + [--json OUT.json] + +synth cases (C lines). A case passes when its status is ok, no destination pixel kept the sentinel, both readbacks and +all repeats agree, and every sampled value (9 points, 16 middle-row samples, 64 tile means, per-stripe means) is within +--tolerance (8-bit units) of the reference in every channel. A failing case names the first matching explanation: + ERROR / MISSING the tool raised or never printed the case + UNWRITTEN / PARTIAL destination pixels still hold the sentinel: the dispatch never ran, or ran in part + READBACK getBytes and a GPU blit of the same texture differ (CPU cache maintenance or coherency) + NONDETERMINISTIC repeats from identical inputs differ (synchronisation) + BLACK / TRANSPARENT the kernel read zeros (an unbound or null T#, a wrong T# slot, a wrong sampler) + SWAP-RB red and blue exchanged (the T# SWAP field: WZYX vs WXYZ) + SRGB-EXTRA / SRGB-MISSING colour channels look decoded twice / not decoded (the T# SRGB bit vs CoreImage's own) + INPUT-k multi-input cases: the stripe of input k (Metal texture index from the census) is wrong, + missing (transparent) or shows input j's content + CHANNEL otherwise: which channels are off, by how much, and where (tiles) +Icon cases (I lines): coverage, mean colour and the 8x8 alpha/luminance grids against the reference: EMPTY, BLACK, +PARTIAL (alpha cells lost), WRONG-COLOUR, SHAPE. +K lines (census) are compared too: a different kernel or texture format set is reported as a note, since the pixel +comparison is then of different work. --census-only compares only status, census and diagnostics (for an output of +the plug-in device with intercepted commits, whose pixels were never computed). +Exit status: 0 every case passes, 1 a case fails, 2 unusable input. +""" +import argparse +import json +from pathlib import Path +import re +import sys +import zlib + +SENTINEL = '3ca55ac3' + + +def parse(path): + """{'V': [...], 'C': {name: fields}, 'K': {name: line}, 'I': {...}, 'D': [...], 'E': [...], 'END': ..., 'noise': n}""" + out = dict(V=[], C={}, K={}, I={}, D=[], E=[], W=[], END=None, noise=0, lines=0) + for raw in Path(path).read_text(errors='replace').splitlines(): + raw = raw.strip() + if not raw: + continue + body, sep, crc = raw.rpartition(' *') + if not sep or not re.fullmatch(r'[0-9a-f]{8}', crc) or f'{zlib.crc32(body.encode()) & 0xffffffff:08x}' != crc: + out['noise'] += 1 + continue + out['lines'] += 1 + kind = body.split(' ', 1)[0] + if kind in ('C', 'I'): + words = body.split(' ') + fields = dict(name=words[1], size=words[2]) + for w in words[3:]: + k, eq, v = w.partition('=') + if eq: + fields[k] = v + out[kind][words[1]] = fields + elif kind == 'K': + words = body.split(' ', 2) + out['K'][words[1]] = words[2] if len(words) > 2 else '' + elif kind == 'END': + out['END'] = body + elif kind in out: + out[kind].append(body) + return out + + +def px(h): + return [int(h[i:i + 2], 16) for i in (0, 2, 4, 6)] + + +def values(f): + """Every sampled RGBA value of a C line, labelled by where it came from.""" + vals = [] + for i, h in enumerate(f.get('p', '').split(',')): + if len(h) == 8: + vals.append((f'point{i}', px(h))) + for i, h in enumerate(f.get('g', '').split(',')): + if len(h) == 8: + vals.append((f'row{i}', px(h))) + t = f.get('t', '') + for i in range(len(t) // 8): + vals.append((f'tile{i // 8},{i % 8}', px(t[8 * i:8 * i + 8]))) + return vals + + +def stripes(f): + s = f.get('s') + if not s: + return None, None + top, _, band = s.partition('/') + return [px(h) for h in top.split(',')], [px(h) for h in band.split(',')] + + +def to_linear(v): + c = v / 255.0 + return 255.0 * (c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4) + + +def to_srgb(v): + c = v / 255.0 + return 255.0 * (c * 12.92 if c <= 0.0031308 else 1.055 * c ** (1 / 2.4) - 0.055) + + +def close(a, b, tol, channels=range(4)): + return all(abs(a[c] - b[c]) <= tol for c in channels) + + +def explain_values(ref, got, tol): + """The explanation for sampled values that differ, from the most specific pattern to the generic one.""" + pairs = [(label, r, g) for (label, r), (_, g) in zip(ref, got)] + bad = [(label, r, g) for label, r, g in pairs if not close(r, g, tol)] + if not bad: + return None + if all(g == [0, 0, 0, 0] for _, _, g in bad): + return 'TRANSPARENT: the kernel wrote transparent black where the reference has content (null or wrong T#, sampler)' + if all(g[:3] == [0, 0, 0] for _, r, g in bad if max(r[:3]) > 24) and any(max(r[:3]) > 24 for _, r, _ in bad): + return 'BLACK: colour channels read zero (unbound or null T#, wrong T# slot, or wrong format)' + swapped = [(r, g) for _, r, g in pairs if abs(r[0] - r[2]) > 2 * tol] + if swapped and all(abs(g[0] - r[2]) <= tol and abs(g[2] - r[0]) <= tol and abs(g[1] - r[1]) <= tol for r, g in swapped): + return 'SWAP-RB: red and blue are exchanged (T# SWAP: WZYX for RGBA, WXYZ for BGRA)' + mid = [(r, g) for _, r, g in bad if any(40 <= r[c] <= 215 for c in range(3))] + + def frac(fn): + hits = [all(abs(g[c] - fn(r[c])) <= tol + 2 for c in range(3) if 40 <= r[c] <= 215) for r, g in mid] + return sum(hits) / len(hits) if hits else 0.0 + if mid and frac(to_linear) >= 0.8: + return 'SRGB-EXTRA: colour channels are darker as if sRGB-decoded once more (T# SRGB bit on top of the kernel\'s decode)' + if mid and frac(to_srgb) >= 0.8: + return 'SRGB-MISSING: colour channels are lighter as if never sRGB-decoded (T# SRGB bit absent)' + names = 'RGBA' + worst = [max(abs(r[c] - g[c]) for _, r, g in bad) for c in range(4)] + off = [names[c] for c in range(4) if worst[c] > tol] + zeroed = [names[c] for c in range(4) if all(g[c] == 0 for _, r, g in bad if r[c] > 24) + and any(r[c] > 24 for _, r, _ in bad)] + tiles = sorted({label for label, _, _ in bad if label.startswith('tile')}) + where = '' + if tiles: + rows = sorted({int(t[4:].split(',')[0]) for t in tiles}) + cols = sorted({int(t.split(',')[1]) for t in tiles}) + where = f'; {len(tiles)}/64 tiles off (rows {rows[0]}-{rows[-1]}, cols {cols[0]}-{cols[-1]})' + first = bad[0] + detail = f'channels {"".join(off)} off (max delta ' + ','.join(str(w) for w in worst) + ')' + if zeroed: + detail += f'; {"".join(zeroed)} reads 0' + return f'CHANNEL: {detail}{where}; first at {first[0]}: reference {"".join(f"{x:02x}" for x in first[1])} guest ' \ + f'{"".join(f"{x:02x}" for x in first[2])}' + + +def texture_index_of_inputs(census): + """'1=rgba8s:i5,2=rgba8s:i4,...' -> {5: 1, 4: 2, ...} (input k -> Metal texture index).""" + out = {} + for m in re.finditer(r'(\d+)=[a-z0-9]+:i(\d+)', census or ''): + out.setdefault(int(m.group(2)), int(m.group(1))) + return out + + +def compare_synth(name, r, g, rk, gk, tol, census_only): + notes = [] + if gk is not None and rk is not None and gk != rk: + notes.append(f'census differs: reference {rk[:160]} | guest {gk[:160]}') + if g is None: + return 'MISSING: the case printed no line (the tool stopped or the case was filtered)', notes + if g.get('st') != 'ok': + return f'ERROR: status {g.get("st")}', notes + if census_only: + if gk != rk: + return 'CENSUS: CoreImage dispatched other kernels or bound other texture formats', notes + return None, notes + w, _, h = g['size'].partition('x') + total = int(w) * int(h) if w.isdigit() and h.isdigit() else 0 + sent = int(g.get('sent', '0')) + if total and sent >= total: + return 'UNWRITTEN: no destination pixel was written (dispatch dropped or refused, or the GPU never ran it)', notes + hashes = g.get('h', '').split(',') + if sent: + return f'PARTIAL: {sent} of {total} pixels still hold the sentinel (part of the dispatch never wrote)', notes + if g.get('hb') not in ('na', hashes[0]): + return 'READBACK: getBytes and the GPU blit of the same texture differ (CPU cache maintenance/coherency)', notes + if len(set(hashes)) > 1: + notes.append('repeats differ') + if r is None: + return 'MISSING-REFERENCE: the reference has no such case', notes + if r['size'] != g['size']: + return f'SIZE: {g["size"]} instead of {r["size"]}', notes + rt, rb = stripes(r) + gt, gb = stripes(g) + if rt and gt: + index = texture_index_of_inputs(rk) + for k, (want, got) in enumerate(zip(rt, gt)): + if close(want, got, tol): + continue + slot = f' (texture({index[k]}))' if k in index else '' + if got[3] <= tol: + return f'INPUT-{k}: stripe {k} is transparent: input {k}{slot} was not read', notes + other = [j for j, o in enumerate(rt) if j != k and close(o, got, tol)] + if other: + return f'INPUT-{k}: stripe {k} shows input {other[0]}\'s colour: input {k}{slot} reads another texture', notes + return f'INPUT-{k}: stripe {k} is {"".join(f"{x:02x}" for x in got)}, reference ' \ + f'{"".join(f"{x:02x}" for x in want)} (input {k}{slot})', notes + for k, (want, got) in enumerate(zip(rb, gb)): + if not close(want, got, tol): + return f'BLEND: the half-transparent band under stripe {k} is {"".join(f"{x:02x}" for x in got)}, ' \ + f'reference {"".join(f"{x:02x}" for x in want)} (blend order or premultiplication)', notes + why = explain_values(values(r), values(g), tol) + if why: + return why, notes + if len(set(hashes)) > 1: + return 'NONDETERMINISTIC: repeats from identical inputs differ (within tolerance of the reference at the samples)', notes + return None, notes + + +def grid(s): + return [int(c, 16) for c in s] + + +def compare_icon(name, r, g, rk, gk, tol): + notes = [] + if g is None: + return 'MISSING: the icon printed no line', notes + if g.get('st') != 'ok': + return f'ERROR: status {g.get("st")}', notes + if r is None: + return 'MISSING-REFERENCE: the reference has no such icon', notes + if r.get('st') != 'ok': + return f'REFERENCE-ERROR: the reference status is {r.get("st")}', notes + rc, gc = int(r['cov']), int(g['cov']) + if gc == 0 and rc > 0: + return 'EMPTY: the icon has no visible pixel', notes + rrgb, grgb = px(r['rgb'] + 'ff')[:3], px(g['rgb'] + 'ff')[:3] + if max(grgb) < 0x10 and max(rrgb) >= 0x30: + return f'BLACK: covered pixels are black (mean {g["rgb"]}, reference {r["rgb"]})', notes + ra, ga = grid(r['ag']), grid(g['ag']) + lost = [i for i, (a, b) in enumerate(zip(ra, ga)) if a >= 3 and b <= a - 3] + if lost: + return f'PARTIAL: {len(lost)} of 64 alpha cells lost (first row {lost[0] // 8} col {lost[0] % 8}; ' \ + f'coverage {gc} vs {rc} per mille)', notes + if abs(gc - rc) > 30: + return f'SHAPE: coverage {gc} vs reference {rc} per mille', notes + if any(abs(a - b) > 2 * tol for a, b in zip(rrgb, grgb)): + d = ','.join(f'{b - a:+d}' for a, b in zip(rrgb, grgb)) + return f'WRONG-COLOUR: mean colour {g["rgb"]} vs reference {r["rgb"]} (R,G,B delta {d})', notes + rl, gl = grid(r['lg']), grid(g['lg']) + off = [i for i, (a, b) in enumerate(zip(rl, gl)) if abs(a - b) > 1] + if off or any(abs(a - b) > 1 for a, b in zip(ra, ga)): + return f'SHAPE: {len(off)} luminance cells differ by more than one level', notes + return None, notes + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('reference', type=Path) + ap.add_argument('guest', type=Path) + ap.add_argument('--tolerance', type=int, default=6, help='per-channel tolerance, 8-bit units (default 6)') + ap.add_argument('--census-only', action='store_true') + ap.add_argument('--cases', help='compare only the cases whose name matches this regular expression') + ap.add_argument('--json', type=Path) + a = ap.parse_args(argv) + ref, got = parse(a.reference), parse(a.guest) + if not ref['lines'] or not ref['V']: + print(f'unusable reference {a.reference}: {ref["lines"]} valid lines') + return 2 + if a.cases: + keep = re.compile(a.cases) + for kind in ('C', 'I'): + for side in (ref, got): + side[kind] = {n: f for n, f in side[kind].items() if keep.search(n)} + rows = [] + for name in list(ref['C']) + [n for n in got['C'] if n not in ref['C']]: + why, notes = compare_synth(name, ref['C'].get(name), got['C'].get(name), ref['K'].get(name), got['K'].get(name), + a.tolerance, a.census_only) + rows.append(dict(case=name, kind='synth', verdict='FAIL' if why else 'PASS', why=why, notes=notes)) + for name in list(ref['I']) + [n for n in got['I'] if n not in ref['I']]: + why, notes = compare_icon(name, ref['I'].get(name), got['I'].get(name), ref['K'].get(name), got['K'].get(name), + a.tolerance) + rows.append(dict(case=name, kind='icon', verdict='FAIL' if why else 'PASS', why=why, notes=notes)) + # The plug-in's own record of work it would not encode (dropped: lenient builds, refused: strict ones). + for d in got['D']: + m = re.search(r' dropped=(\d+) refused=(\d+)', d) + if m and (int(m.group(1)) or int(m.group(2))): + rows.append(dict(case='plugin-diagnostics', kind='diag', verdict='FAIL', notes=[], + why=f'the plug-in dropped {m.group(1)} and refused {m.group(2)} pieces of work in this process ' + '(the D lines below name them)')) + for row in rows: + line = f'{row["verdict"]} {row["case"]}' + (f': {row["why"]}' if row['why'] else '') + if row['notes']: + line += ' [' + '; '.join(row['notes']) + ']' + print(line) + fails = [r for r in rows if r['verdict'] == 'FAIL'] + print(f'guest: {got["V"][0] if got["V"] else "no V line"}') + print(f'guest lines {got["lines"]}, noise dropped {got["noise"]}, errors {len(got["E"])}, end {got["END"] or "absent"}') + for d in got['D'][:7]: + print(f'guest {d[:300]}') + for w in got['W']: + print(f'guest {w[:300]}') + print(f'{"PASS" if not fails else "FAIL"}: {len(rows) - len(fails)} of {len(rows)} cases pass' + + (f'; failing: {", ".join(r["case"] for r in fails[:20])}' if fails else '')) + if a.json: + a.json.write_text(json.dumps(dict(rows=rows, guest_v=got['V'], guest_d=got['D'], guest_end=got['END'], + noise=got['noise']), indent=1) + '\n') + if not got['V'] or not rows: + return 2 + return 1 if fails else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/icon-pixels-reference-synth.txt b/tools/icon-pixels-reference-synth.txt new file mode 100644 index 0000000..f4b7784 --- /dev/null +++ b/tools/icon-pixels-reference-synth.txt @@ -0,0 +1,79 @@ +V icon-pixels 1 mode=host-caps device=Apple_M3_Max os=26A428 pid=2871 repeat=2 cs=- plugin=local:AdrenoMetal.bundle@378ff525b00426d1 *57bb7186 +C tex-rgba8s-lin 128x128 st=ok in=rgba8s h=b42791ae5393cd61,b42791ae5393cd61 hb=b42791ae5393cd61 sent=0 p=00000dff,f80086ff,00f886ff,f8f80dff,36363bff,0d0d3bff,850d3bff,0d853bff,85853bff g=01362bff,02362bff,053668ff,0a3668ff,10362bff,18362bff,233668ff,2f3668ff,3e362bff,4f362bff,633668ff,793668ff,92362bff,ad362bff,cc3668ff,ed3668ff t=010111ff070180ff140113ff280180ff450113ff6c0180ff9e0113ffda0182ff010780ff070714ff14077fff280714ff45077fff6c0714ff9e077fffda0713ff011413ff07147fff141414ff28147fff451414ff6c147fff9e1414ffda1480ff012880ff072814ff14287fff282814ff45287fff6c2814ff9e287fffda2813ff014513ff07457fff144514ff28457fff454514ff6c457fff9e4514ffda4580ff016c80ff076c14ff146c7fff286c14ff456c7fff6c6c14ff9e6c7fffda6c13ff019e13ff079e7fff149e14ff289e7fff459e14ff6c9e7fff9e9e14ffda9e80ff01da82ff07da13ff14da80ff28da13ff45da80ff6cda13ff9eda80ffdada11ff ms=120 *c2f928ab +K tex-rgba8s-lin n=2 d=0 ci_affine[0=bgra8:o,1=rgba8s:i0,s0]*2 *b29b244c +C tex-rgba8s-near 128x128 st=ok in=rgba8s h=18fd86522e0c0a7d,18fd86522e0c0a7d hb=18fd86522e0c0a7d sent=0 p=00000dff,f80086ff,00f886ff,f8f80dff,37370dff,0d0d0dff,860d0dff,0d860dff,86860dff g=01370dff,02370dff,053786ff,0a3786ff,11370dff,19370dff,233786ff,303786ff,3f370dff,50370dff,643786ff,7a3786ff,93370dff,af370dff,ce3786ff,ef3786ff t=01010dff070186ff14010dff280186ff46010dff6c0186ff9e010dffdb0186ff010786ff07070dff140786ff28070dff460786ff6c070dff9e0786ffdb070dff01140dff071486ff14140dff281486ff46140dff6c1486ff9e140dffdb1486ff012886ff07280dff142886ff28280dff462886ff6c280dff9e2886ffdb280dff01460dff074686ff14460dff284686ff46460dff6c4686ff9e460dffdb4686ff016c86ff076c0dff146c86ff286c0dff466c86ff6c6c0dff9e6c86ffdb6c0dff019e0dff079e86ff149e0dff289e86ff469e0dff6c9e86ff9e9e0dffdb9e86ff01db86ff07db0dff14db86ff28db0dff46db86ff6cdb0dff9edb86ffdbdb0dff ms=3 *c0b0203f +K tex-rgba8s-near n=2 d=0 ci_affine_nearest[0=bgra8:o,1=rgba8s:i0,s0]*2 *175e24da +C tex-bgra8s-lin 128x128 st=ok in=bgra8s h=b42791ae5393cd61,b42791ae5393cd61 hb=b42791ae5393cd61 sent=0 p=00000dff,f80086ff,00f886ff,f8f80dff,36363bff,0d0d3bff,850d3bff,0d853bff,85853bff g=01362bff,02362bff,053668ff,0a3668ff,10362bff,18362bff,233668ff,2f3668ff,3e362bff,4f362bff,633668ff,793668ff,92362bff,ad362bff,cc3668ff,ed3668ff t=010111ff070180ff140113ff280180ff450113ff6c0180ff9e0113ffda0182ff010780ff070714ff14077fff280714ff45077fff6c0714ff9e077fffda0713ff011413ff07147fff141414ff28147fff451414ff6c147fff9e1414ffda1480ff012880ff072814ff14287fff282814ff45287fff6c2814ff9e287fffda2813ff014513ff07457fff144514ff28457fff454514ff6c457fff9e4514ffda4580ff016c80ff076c14ff146c7fff286c14ff456c7fff6c6c14ff9e6c7fffda6c13ff019e13ff079e7fff149e14ff289e7fff459e14ff6c9e7fff9e9e14ffda9e80ff01da82ff07da13ff14da80ff28da13ff45da80ff6cda13ff9eda80ffdada11ff ms=1 *14caf135 +K tex-bgra8s-lin n=2 d=0 ci_affine[0=bgra8:o,1=bgra8s:i0,s0]*2 *5a84bd88 +C tex-bgra8s-near 128x128 st=ok in=bgra8s h=18fd86522e0c0a7d,18fd86522e0c0a7d hb=18fd86522e0c0a7d sent=0 p=00000dff,f80086ff,00f886ff,f8f80dff,37370dff,0d0d0dff,860d0dff,0d860dff,86860dff g=01370dff,02370dff,053786ff,0a3786ff,11370dff,19370dff,233786ff,303786ff,3f370dff,50370dff,643786ff,7a3786ff,93370dff,af370dff,ce3786ff,ef3786ff t=01010dff070186ff14010dff280186ff46010dff6c0186ff9e010dffdb0186ff010786ff07070dff140786ff28070dff460786ff6c070dff9e0786ffdb070dff01140dff071486ff14140dff281486ff46140dff6c1486ff9e140dffdb1486ff012886ff07280dff142886ff28280dff462886ff6c280dff9e2886ffdb280dff01460dff074686ff14460dff284686ff46460dff6c4686ff9e460dffdb4686ff016c86ff076c0dff146c86ff286c0dff466c86ff6c6c0dff9e6c86ffdb6c0dff019e0dff079e86ff149e0dff289e86ff469e0dff6c9e86ff9e9e0dffdb9e86ff01db86ff07db0dff14db86ff28db0dff46db86ff6cdb0dff9edb86ffdbdb0dff ms=2 *7880f28f +K tex-bgra8s-near n=2 d=0 ci_affine_nearest[0=bgra8:o,1=bgra8s:i0,s0]*2 *8fc2a065 +C tex-rgba8-lin 128x128 st=ok in=rgba8 h=c89ba91151c892b9,c89ba91151c892b9 hb=c89ba91151c892b9 sent=0 p=000040ff,fc00c0ff,00fcc0ff,fcfc40ff,7f7f70ff,3f3f70ff,bf3f70ff,3fbf70ff,bfbf70ff g=077f60ff,177f60ff,277fa0ff,377fa0ff,477f60ff,577f60ff,677fa0ff,777fa0ff,877f60ff,977f60ff,a77fa0ff,b77fa0ff,c77f60ff,d77f60ff,e77fa0ff,f77fa0ff t=0e0e44ff2e0ebaff4e0e46ff6e0ebaff8e0e46ffae0ebaffce0e46ffee0ebcff0e2ebaff2e2e48ff4e2eb8ff6e2e48ff8e2eb8ffae2e48ffce2eb8ffee2e46ff0e4e46ff2e4eb8ff4e4e48ff6e4eb8ff8e4e48ffae4eb8ffce4e48ffee4ebaff0e6ebaff2e6e48ff4e6eb8ff6e6e48ff8e6eb8ffae6e48ffce6eb8ffee6e46ff0e8e46ff2e8eb8ff4e8e48ff6e8eb8ff8e8e48ffae8eb8ffce8e48ffee8ebaff0eaebaff2eae48ff4eaeb8ff6eae48ff8eaeb8ffaeae48ffceaeb8ffeeae46ff0ece46ff2eceb8ff4ece48ff6eceb8ff8ece48ffaeceb8ffcece48ffeecebaff0eeebcff2eee46ff4eeebaff6eee46ff8eeebaffaeee46ffceeebaffeeee44ff ms=2 *0db0258a +K tex-rgba8-lin n=2 d=0 ci_affine[0=bgra8:o,1=rgba8:i0,s0]*2 *b24b21b5 +C tex-rgba8-near 128x128 st=ok in=rgba8 h=93f58ae72abcc325,93f58ae72abcc325 hb=93f58ae72abcc325 sent=0 p=000040ff,fc00c0ff,00fcc0ff,fcfc40ff,808040ff,404040ff,c04040ff,40c040ff,c0c040ff g=088040ff,188040ff,2880c0ff,3880c0ff,488040ff,588040ff,6880c0ff,7880c0ff,888040ff,988040ff,a880c0ff,b880c0ff,c88040ff,d88040ff,e880c0ff,f880c0ff t=0e0e40ff2e0ec0ff4e0e40ff6e0ec0ff8e0e40ffae0ec0ffce0e40ffee0ec0ff0e2ec0ff2e2e40ff4e2ec0ff6e2e40ff8e2ec0ffae2e40ffce2ec0ffee2e40ff0e4e40ff2e4ec0ff4e4e40ff6e4ec0ff8e4e40ffae4ec0ffce4e40ffee4ec0ff0e6ec0ff2e6e40ff4e6ec0ff6e6e40ff8e6ec0ffae6e40ffce6ec0ffee6e40ff0e8e40ff2e8ec0ff4e8e40ff6e8ec0ff8e8e40ffae8ec0ffce8e40ffee8ec0ff0eaec0ff2eae40ff4eaec0ff6eae40ff8eaec0ffaeae40ffceaec0ffeeae40ff0ece40ff2ecec0ff4ece40ff6ecec0ff8ece40ffaecec0ffcece40ffeecec0ff0eeec0ff2eee40ff4eeec0ff6eee40ff8eeec0ffaeee40ffceeec0ffeeee40ff ms=2 *8c9b9633 +K tex-rgba8-near n=2 d=0 ci_affine_nearest[0=bgra8:o,1=rgba8:i0,s0]*2 *1b963012 +C tex-bgra8-lin 128x128 st=ok in=bgra8 h=c89ba91151c892b9,c89ba91151c892b9 hb=c89ba91151c892b9 sent=0 p=000040ff,fc00c0ff,00fcc0ff,fcfc40ff,7f7f70ff,3f3f70ff,bf3f70ff,3fbf70ff,bfbf70ff g=077f60ff,177f60ff,277fa0ff,377fa0ff,477f60ff,577f60ff,677fa0ff,777fa0ff,877f60ff,977f60ff,a77fa0ff,b77fa0ff,c77f60ff,d77f60ff,e77fa0ff,f77fa0ff t=0e0e44ff2e0ebaff4e0e46ff6e0ebaff8e0e46ffae0ebaffce0e46ffee0ebcff0e2ebaff2e2e48ff4e2eb8ff6e2e48ff8e2eb8ffae2e48ffce2eb8ffee2e46ff0e4e46ff2e4eb8ff4e4e48ff6e4eb8ff8e4e48ffae4eb8ffce4e48ffee4ebaff0e6ebaff2e6e48ff4e6eb8ff6e6e48ff8e6eb8ffae6e48ffce6eb8ffee6e46ff0e8e46ff2e8eb8ff4e8e48ff6e8eb8ff8e8e48ffae8eb8ffce8e48ffee8ebaff0eaebaff2eae48ff4eaeb8ff6eae48ff8eaeb8ffaeae48ffceaeb8ffeeae46ff0ece46ff2eceb8ff4ece48ff6eceb8ff8ece48ffaeceb8ffcece48ffeecebaff0eeebcff2eee46ff4eeebaff6eee46ff8eeebaffaeee46ffceeebaffeeee44ff ms=2 *25d45f8a +K tex-bgra8-lin n=2 d=0 ci_affine[0=bgra8:o,1=bgra8:i0,s0]*2 *e8c1b4d0 +C tex-bgra8-near 128x128 st=ok in=bgra8 h=93f58ae72abcc325,93f58ae72abcc325 hb=93f58ae72abcc325 sent=0 p=000040ff,fc00c0ff,00fcc0ff,fcfc40ff,808040ff,404040ff,c04040ff,40c040ff,c0c040ff g=088040ff,188040ff,2880c0ff,3880c0ff,488040ff,588040ff,6880c0ff,7880c0ff,888040ff,988040ff,a880c0ff,b880c0ff,c88040ff,d88040ff,e880c0ff,f880c0ff t=0e0e40ff2e0ec0ff4e0e40ff6e0ec0ff8e0e40ffae0ec0ffce0e40ffee0ec0ff0e2ec0ff2e2e40ff4e2ec0ff6e2e40ff8e2ec0ffae2e40ffce2ec0ffee2e40ff0e4e40ff2e4ec0ff4e4e40ff6e4ec0ff8e4e40ffae4ec0ffce4e40ffee4ec0ff0e6ec0ff2e6e40ff4e6ec0ff6e6e40ff8e6ec0ffae6e40ffce6ec0ffee6e40ff0e8e40ff2e8ec0ff4e8e40ff6e8ec0ff8e8e40ffae8ec0ffce8e40ffee8ec0ff0eaec0ff2eae40ff4eaec0ff6eae40ff8eaec0ffaeae40ffceaec0ffeeae40ff0ece40ff2ecec0ff4ece40ff6ecec0ff8ece40ffaecec0ffcece40ffeecec0ff0eeec0ff2eee40ff4eeec0ff6eee40ff8eeec0ffaeee40ffceeec0ffeeee40ff ms=2 *13ecd36a +K tex-bgra8-near n=2 d=0 ci_affine_nearest[0=bgra8:o,1=bgra8:i0,s0]*2 *cb4d9571 +C tex-rg8-lin 128x128 st=ok in=rg8 h=b31b3210953ea559,b31b3210953ea559 hb=b31b3210953ea559 sent=0 p=000000ff,fc0000ff,00fc00ff,fcfc00ff,7f7f00ff,3f3f00ff,bf3f00ff,3fbf00ff,bfbf00ff g=077f00ff,177f00ff,277f00ff,377f00ff,477f00ff,577f00ff,677f00ff,777f00ff,877f00ff,977f00ff,a77f00ff,b77f00ff,c77f00ff,d77f00ff,e77f00ff,f77f00ff t=0e0e00ff2e0e00ff4e0e00ff6e0e00ff8e0e00ffae0e00ffce0e00ffee0e00ff0e2e00ff2e2e00ff4e2e00ff6e2e00ff8e2e00ffae2e00ffce2e00ffee2e00ff0e4e00ff2e4e00ff4e4e00ff6e4e00ff8e4e00ffae4e00ffce4e00ffee4e00ff0e6e00ff2e6e00ff4e6e00ff6e6e00ff8e6e00ffae6e00ffce6e00ffee6e00ff0e8e00ff2e8e00ff4e8e00ff6e8e00ff8e8e00ffae8e00ffce8e00ffee8e00ff0eae00ff2eae00ff4eae00ff6eae00ff8eae00ffaeae00ffceae00ffeeae00ff0ece00ff2ece00ff4ece00ff6ece00ff8ece00ffaece00ffcece00ffeece00ff0eee00ff2eee00ff4eee00ff6eee00ff8eee00ffaeee00ffceee00ffeeee00ff ms=2 *21f1215a +K tex-rg8-lin n=2 d=0 ci_affine[0=bgra8:o,1=rg8:i0,s0]*2 *715936a4 +C tex-rg8-near 128x128 st=ok in=rg8 h=ea80530e6ae1d325,ea80530e6ae1d325 hb=ea80530e6ae1d325 sent=0 p=000000ff,fc0000ff,00fc00ff,fcfc00ff,808000ff,404000ff,c04000ff,40c000ff,c0c000ff g=088000ff,188000ff,288000ff,388000ff,488000ff,588000ff,688000ff,788000ff,888000ff,988000ff,a88000ff,b88000ff,c88000ff,d88000ff,e88000ff,f88000ff t=0e0e00ff2e0e00ff4e0e00ff6e0e00ff8e0e00ffae0e00ffce0e00ffee0e00ff0e2e00ff2e2e00ff4e2e00ff6e2e00ff8e2e00ffae2e00ffce2e00ffee2e00ff0e4e00ff2e4e00ff4e4e00ff6e4e00ff8e4e00ffae4e00ffce4e00ffee4e00ff0e6e00ff2e6e00ff4e6e00ff6e6e00ff8e6e00ffae6e00ffce6e00ffee6e00ff0e8e00ff2e8e00ff4e8e00ff6e8e00ff8e8e00ffae8e00ffce8e00ffee8e00ff0eae00ff2eae00ff4eae00ff6eae00ff8eae00ffaeae00ffceae00ffeeae00ff0ece00ff2ece00ff4ece00ff6ece00ff8ece00ffaece00ffcece00ffeece00ff0eee00ff2eee00ff4eee00ff6eee00ff8eee00ffaeee00ffceee00ffeeee00ff ms=2 *0cde3fc2 +K tex-rg8-near n=2 d=0 ci_affine_nearest[0=bgra8:o,1=rg8:i0,s0]*2 *4102bcce +C tex-rg16f-lin 128x128 st=ok in=rg16f h=8d61eb2aececb90d,8d61eb2aececb90d hb=8d61eb2aececb90d sent=0 p=000000ff,fe0000ff,00fe00ff,fefe00ff,bbbb00ff,888800ff,e08800ff,88e000ff,e0e000ff g=2ebb00ff,55bb00ff,6dbb00ff,80bb00ff,90bb00ff,9ebb00ff,aabb00ff,b6bb00ff,c0bb00ff,cabb00ff,d3bb00ff,dcbb00ff,e5bb00ff,edbb00ff,f4bb00ff,fbbb00ff t=3c3c00ff753c00ff963c00ffaf3c00ffc53c00ffd73c00ffe83c00fff73c00ff3c7500ff757500ff967500ffaf7500ffc57500ffd77500ffe87500fff77500ff3c9600ff759600ff969600ffaf9600ffc59600ffd79600ffe89600fff79600ff3caf00ff75af00ff96af00ffafaf00ffc5af00ffd7af00ffe8af00fff7af00ff3cc500ff75c500ff96c500ffafc500ffc5c500ffd7c500ffe8c500fff7c500ff3cd700ff75d700ff96d700ffafd700ffc5d700ffd7d700ffe8d700fff7d700ff3ce800ff75e800ff96e800ffafe800ffc5e800ffd7e800ffe8e800fff7e800ff3cf700ff75f700ff96f700ffaff700ffc5f700ffd7f700ffe8f700fff7f700ff ms=3 *a0d34c03 +K tex-rg16f-lin n=2 d=0 ci_affine_linear_to_srgb[0=bgra8:o,1=rg16f:i0,s0]*2 *d8fd156b +C tex-rg16f-near 128x128 st=ok in=rg16f h=98714485f6035aed,98714485f6035aed hb=98714485f6035aed sent=0 p=000000ff,fe0000ff,00fe00ff,fefe00ff,bcbc00ff,898900ff,e18900ff,89e100ff,e1e100ff g=32bc00ff,56bc00ff,6ebc00ff,81bc00ff,91bc00ff,9fbc00ff,abbc00ff,b7bc00ff,c1bc00ff,cbbc00ff,d4bc00ff,ddbc00ff,e5bc00ff,edbc00ff,f5bc00ff,fcbc00ff t=3b3b00ff753b00ff963b00ffaf3b00ffc53b00ffd83b00ffe83b00fff73b00ff3b7500ff757500ff967500ffaf7500ffc57500ffd87500ffe87500fff77500ff3b9600ff759600ff969600ffaf9600ffc59600ffd89600ffe89600fff79600ff3baf00ff75af00ff96af00ffafaf00ffc5af00ffd8af00ffe8af00fff7af00ff3bc500ff75c500ff96c500ffafc500ffc5c500ffd8c500ffe8c500fff7c500ff3bd800ff75d800ff96d800ffafd800ffc5d800ffd8d800ffe8d800fff7d800ff3be800ff75e800ff96e800ffafe800ffc5e800ffd8e800ffe8e800fff7e800ff3bf700ff75f700ff96f700ffaff700ffc5f700ffd8f700ffe8f700fff7f700ff ms=3 *22f3a87f +K tex-rg16f-near n=2 d=0 ci_affine_nearest_linear_to_srgb[0=bgra8:o,1=rg16f:i0,s0]*2 *99d83cef +C tex-r8-lin 128x128 st=ok in=r8 h=d830594d4eb56325,d830594d4eb56325 hb=d830594d4eb56325 sent=0 p=000000ff,fc0000ff,000000ff,fc0000ff,7f0000ff,3f0000ff,bf0000ff,3f0000ff,bf0000ff g=070000ff,170000ff,270000ff,370000ff,470000ff,570000ff,670000ff,770000ff,870000ff,970000ff,a70000ff,b70000ff,c70000ff,d70000ff,e70000ff,f70000ff t=0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff ms=2 *9ad3db8f +K tex-r8-lin n=2 d=0 ci_affine[0=bgra8:o,1=r8:i0,s0]*2 *62fafdfa +C tex-r8-near 128x128 st=ok in=r8 h=a3104ca9de072325,a3104ca9de072325 hb=a3104ca9de072325 sent=0 p=000000ff,fc0000ff,000000ff,fc0000ff,800000ff,400000ff,c00000ff,400000ff,c00000ff g=080000ff,180000ff,280000ff,380000ff,480000ff,580000ff,680000ff,780000ff,880000ff,980000ff,a80000ff,b80000ff,c80000ff,d80000ff,e80000ff,f80000ff t=0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff0e0000ff2e0000ff4e0000ff6e0000ff8e0000ffae0000ffce0000ffee0000ff ms=2 *39ff57e3 +K tex-r8-near n=2 d=0 ci_affine_nearest[0=bgra8:o,1=r8:i0,s0]*2 *2cd96ce3 +C tex-r16f-lin 128x128 st=ok in=r16f h=2202a2ffe05b7d25,2202a2ffe05b7d25 hb=2202a2ffe05b7d25 sent=0 p=000000ff,fe0000ff,000000ff,fe0000ff,bb0000ff,880000ff,e00000ff,880000ff,e00000ff g=2e0000ff,550000ff,6d0000ff,800000ff,900000ff,9e0000ff,aa0000ff,b60000ff,c00000ff,ca0000ff,d30000ff,dc0000ff,e50000ff,ed0000ff,f40000ff,fb0000ff t=3c0000ff750000ff960000ffaf0000ffc50000ffd70000ffe80000fff70000ff3c0000ff750000ff960000ffaf0000ffc50000ffd70000ffe80000fff70000ff3c0000ff750000ff960000ffaf0000ffc50000ffd70000ffe80000fff70000ff3c0000ff750000ff960000ffaf0000ffc50000ffd70000ffe80000fff70000ff3c0000ff750000ff960000ffaf0000ffc50000ffd70000ffe80000fff70000ff3c0000ff750000ff960000ffaf0000ffc50000ffd70000ffe80000fff70000ff3c0000ff750000ff960000ffaf0000ffc50000ffd70000ffe80000fff70000ff3c0000ff750000ff960000ffaf0000ffc50000ffd70000ffe80000fff70000ff ms=2 *3a098d33 +K tex-r16f-lin n=2 d=0 ci_affine_linear_to_srgb[0=bgra8:o,1=r16f:i0,s0]*2 *ae33193c +C tex-r16f-near 128x128 st=ok in=r16f h=a49e6124c80b5b25,a49e6124c80b5b25 hb=a49e6124c80b5b25 sent=0 p=000000ff,fe0000ff,000000ff,fe0000ff,bc0000ff,890000ff,e10000ff,890000ff,e10000ff g=320000ff,560000ff,6e0000ff,810000ff,910000ff,9f0000ff,ab0000ff,b70000ff,c10000ff,cb0000ff,d40000ff,dd0000ff,e50000ff,ed0000ff,f50000ff,fc0000ff t=3b0000ff750000ff960000ffaf0000ffc50000ffd80000ffe80000fff70000ff3b0000ff750000ff960000ffaf0000ffc50000ffd80000ffe80000fff70000ff3b0000ff750000ff960000ffaf0000ffc50000ffd80000ffe80000fff70000ff3b0000ff750000ff960000ffaf0000ffc50000ffd80000ffe80000fff70000ff3b0000ff750000ff960000ffaf0000ffc50000ffd80000ffe80000fff70000ff3b0000ff750000ff960000ffaf0000ffc50000ffd80000ffe80000fff70000ff3b0000ff750000ff960000ffaf0000ffc50000ffd80000ffe80000fff70000ff3b0000ff750000ff960000ffaf0000ffc50000ffd80000ffe80000fff70000ff ms=2 *4ca8686b +K tex-r16f-near n=2 d=0 ci_affine_nearest_linear_to_srgb[0=bgra8:o,1=r16f:i0,s0]*2 *7df56571 +C tex-a8-lin 128x128 st=ok in=a8 h=01928e67ca062325,01928e67ca062325 hb=01928e67ca062325 sent=0 p=00000000,000000fc,00000000,000000fc,0000007f,0000003f,000000bf,0000003f,000000bf g=00000007,00000017,00000027,00000037,00000047,00000057,00000067,00000077,00000087,00000097,000000a7,000000b7,000000c7,000000d7,000000e7,000000f7 t=0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee ms=2 *5d340e59 +K tex-a8-lin n=2 d=0 ci_affine[0=bgra8:o,1=a8:i0,s0]*2 *7ec0ab16 +C tex-a8-near 128x128 st=ok in=a8 h=e82ccbb286bc2325,e82ccbb286bc2325 hb=e82ccbb286bc2325 sent=0 p=00000000,000000fc,00000000,000000fc,00000080,00000040,000000c0,00000040,000000c0 g=00000008,00000018,00000028,00000038,00000048,00000058,00000068,00000078,00000088,00000098,000000a8,000000b8,000000c8,000000d8,000000e8,000000f8 t=0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee0000000e0000002e0000004e0000006e0000008e000000ae000000ce000000ee ms=2 *49a477d7 +K tex-a8-near n=2 d=0 ci_affine_nearest[0=bgra8:o,1=a8:i0,s0]*2 *63ee9884 +C tex-rgba16f-lin 128x128 st=ok in=rgba16f h=bb9f9a7d9949feed,bb9f9a7d9949feed hb=bb9f9a7d9949feed sent=0 p=000089ff,fe00e1ff,00fee1ff,fefe89ff,bbbbb1ff,8888b1ff,e088b1ff,88e0b1ff,e0e0b1ff g=2ebba5ff,55bba5ff,6dbbd0ff,80bbd0ff,90bba5ff,9ebba5ff,aabbd0ff,b6bbd0ff,c0bba5ff,cabba5ff,d3bbd0ff,dcbbd0ff,e5bba5ff,edbba5ff,f4bbd0ff,fbbbd0ff t=3c3c8cff753cdeff963c8effaf3cdeffc53c8effd73cdeffe83c8efff73cdfff3c75deff757590ff9675ddffaf7590ffc575ddffd77590ffe875ddfff7758eff3c968eff7596ddff969690ffaf96ddffc59690ffd796ddffe89690fff796deff3cafdeff75af90ff96afddffafaf90ffc5afddffd7af90ffe8afddfff7af8eff3cc58eff75c5ddff96c590ffafc5ddffc5c590ffd7c5ddffe8c590fff7c5deff3cd7deff75d790ff96d7ddffafd790ffc5d7ddffd7d790ffe8d7ddfff7d78eff3ce88eff75e8ddff96e890ffafe8ddffc5e890ffd7e8ddffe8e890fff7e8deff3cf7dfff75f78eff96f7deffaff78effc5f7deffd7f78effe8f7defff7f78cff ms=2 *885422d6 +K tex-rgba16f-lin n=2 d=0 ci_affine_premul_linear_to_srgb_unpremul[0=bgra8:o,1=rgba16f:i0,s0]*2 *a3befd7f +C tex-rgba16f-near 128x128 st=ok in=rgba16f h=04809933e3a7288d,04809933e3a7288d hb=04809933e3a7288d sent=0 p=000089ff,fe00e1ff,00fee1ff,fefe89ff,bcbc89ff,898989ff,e18989ff,89e189ff,e1e189ff g=32bc89ff,56bc89ff,6ebce1ff,81bce1ff,91bc89ff,9fbc89ff,abbce1ff,b7bce1ff,c1bc89ff,cbbc89ff,d4bce1ff,ddbce1ff,e5bc89ff,edbc89ff,f5bce1ff,fcbce1ff t=3b3b89ff753be1ff963b89ffaf3be1ffc53b89ffd83be1ffe83b89fff73be1ff3b75e1ff757589ff9675e1ffaf7589ffc575e1ffd87589ffe875e1fff77589ff3b9689ff7596e1ff969689ffaf96e1ffc59689ffd896e1ffe89689fff796e1ff3bafe1ff75af89ff96afe1ffafaf89ffc5afe1ffd8af89ffe8afe1fff7af89ff3bc589ff75c5e1ff96c589ffafc5e1ffc5c589ffd8c5e1ffe8c589fff7c5e1ff3bd8e1ff75d889ff96d8e1ffafd889ffc5d8e1ffd8d889ffe8d8e1fff7d889ff3be889ff75e8e1ff96e889ffafe8e1ffc5e889ffd8e8e1ffe8e889fff7e8e1ff3bf7e1ff75f789ff96f7e1ffaff789ffc5f7e1ffd8f789ffe8f7e1fff7f789ff ms=2 *d2fae3b9 +K tex-rgba16f-near n=2 d=0 ci_affine_nearest_premul_linear_to_srgb_[0=bgra8:o,1=rgba16f:i0,s0]*2 *5e72601b +C tex-rgba16u-lin 128x128 st=ok in=rgba16u h=c89ba91151c892b9,c89ba91151c892b9 hb=c89ba91151c892b9 sent=0 p=000040ff,fc00c0ff,00fcc0ff,fcfc40ff,7f7f70ff,3f3f70ff,bf3f70ff,3fbf70ff,bfbf70ff g=077f60ff,177f60ff,277fa0ff,377fa0ff,477f60ff,577f60ff,677fa0ff,777fa0ff,877f60ff,977f60ff,a77fa0ff,b77fa0ff,c77f60ff,d77f60ff,e77fa0ff,f77fa0ff t=0e0e44ff2e0ebaff4e0e46ff6e0ebaff8e0e46ffae0ebaffce0e46ffee0ebcff0e2ebaff2e2e48ff4e2eb8ff6e2e48ff8e2eb8ffae2e48ffce2eb8ffee2e46ff0e4e46ff2e4eb8ff4e4e48ff6e4eb8ff8e4e48ffae4eb8ffce4e48ffee4ebaff0e6ebaff2e6e48ff4e6eb8ff6e6e48ff8e6eb8ffae6e48ffce6eb8ffee6e46ff0e8e46ff2e8eb8ff4e8e48ff6e8eb8ff8e8e48ffae8eb8ffce8e48ffee8ebaff0eaebaff2eae48ff4eaeb8ff6eae48ff8eaeb8ffaeae48ffceaeb8ffeeae46ff0ece46ff2eceb8ff4ece48ff6eceb8ff8ece48ffaeceb8ffcece48ffeecebaff0eeebcff2eee46ff4eeebaff6eee46ff8eeebaffaeee46ffceeebaffeeee44ff ms=2 *4d970514 +K tex-rgba16u-lin n=2 d=0 ci_affine[0=bgra8:o,1=rgba16u:i0,s0]*2 *7f98cc94 +C tex-rgba16u-near 128x128 st=ok in=rgba16u h=93f58ae72abcc325,93f58ae72abcc325 hb=93f58ae72abcc325 sent=0 p=000040ff,fc00c0ff,00fcc0ff,fcfc40ff,808040ff,404040ff,c04040ff,40c040ff,c0c040ff g=088040ff,188040ff,2880c0ff,3880c0ff,488040ff,588040ff,6880c0ff,7880c0ff,888040ff,988040ff,a880c0ff,b880c0ff,c88040ff,d88040ff,e880c0ff,f880c0ff t=0e0e40ff2e0ec0ff4e0e40ff6e0ec0ff8e0e40ffae0ec0ffce0e40ffee0ec0ff0e2ec0ff2e2e40ff4e2ec0ff6e2e40ff8e2ec0ffae2e40ffce2ec0ffee2e40ff0e4e40ff2e4ec0ff4e4e40ff6e4ec0ff8e4e40ffae4ec0ffce4e40ffee4ec0ff0e6ec0ff2e6e40ff4e6ec0ff6e6e40ff8e6ec0ffae6e40ffce6ec0ffee6e40ff0e8e40ff2e8ec0ff4e8e40ff6e8ec0ff8e8e40ffae8ec0ffce8e40ffee8ec0ff0eaec0ff2eae40ff4eaec0ff6eae40ff8eaec0ffaeae40ffceaec0ffeeae40ff0ece40ff2ecec0ff4ece40ff6ecec0ff8ece40ffaecec0ffcece40ffeecec0ff0eeec0ff2eee40ff4eeec0ff6eee40ff8eeec0ffaeee40ffceeec0ffeeee40ff ms=2 *0a644c45 +K tex-rgba16u-near n=2 d=0 ci_affine_nearest[0=bgra8:o,1=rgba16u:i0,s0]*2 *4f64f6c7 +C tex-rgb10a2-lin 128x128 st=ok in=rgb10a2 h=c89ba91151c892b9,c89ba91151c892b9 hb=c89ba91151c892b9 sent=0 p=000040ff,fc00c0ff,00fcc0ff,fcfc40ff,7f7f70ff,3f3f70ff,bf3f70ff,3fbf70ff,bfbf70ff g=077f60ff,177f60ff,277fa0ff,377fa0ff,477f60ff,577f60ff,677fa0ff,777fa0ff,877f60ff,977f60ff,a77fa0ff,b77fa0ff,c77f60ff,d77f60ff,e77fa0ff,f77fa0ff t=0e0e44ff2e0ebaff4e0e46ff6e0ebaff8e0e46ffae0ebaffce0e46ffee0ebcff0e2ebaff2e2e48ff4e2eb8ff6e2e48ff8e2eb8ffae2e48ffce2eb8ffee2e46ff0e4e46ff2e4eb8ff4e4e48ff6e4eb8ff8e4e48ffae4eb8ffce4e48ffee4ebaff0e6ebaff2e6e48ff4e6eb8ff6e6e48ff8e6eb8ffae6e48ffce6eb8ffee6e46ff0e8e46ff2e8eb8ff4e8e48ff6e8eb8ff8e8e48ffae8eb8ffce8e48ffee8ebaff0eaebaff2eae48ff4eaeb8ff6eae48ff8eaeb8ffaeae48ffceaeb8ffeeae46ff0ece46ff2eceb8ff4ece48ff6eceb8ff8ece48ffaeceb8ffcece48ffeecebaff0eeebcff2eee46ff4eeebaff6eee46ff8eeebaffaeee46ffceeebaffeeee44ff ms=3 *d17e3ca5 +K tex-rgb10a2-lin n=2 d=0 ci_affine_premul[0=bgra8:o,1=rgb10a2:i0,s0]*2 *c94be537 +C tex-rgb10a2-near 128x128 st=ok in=rgb10a2 h=93f58ae72abcc325,93f58ae72abcc325 hb=93f58ae72abcc325 sent=0 p=000040ff,fc00c0ff,00fcc0ff,fcfc40ff,808040ff,404040ff,c04040ff,40c040ff,c0c040ff g=088040ff,188040ff,2880c0ff,3880c0ff,488040ff,588040ff,6880c0ff,7880c0ff,888040ff,988040ff,a880c0ff,b880c0ff,c88040ff,d88040ff,e880c0ff,f880c0ff t=0e0e40ff2e0ec0ff4e0e40ff6e0ec0ff8e0e40ffae0ec0ffce0e40ffee0ec0ff0e2ec0ff2e2e40ff4e2ec0ff6e2e40ff8e2ec0ffae2e40ffce2ec0ffee2e40ff0e4e40ff2e4ec0ff4e4e40ff6e4ec0ff8e4e40ffae4ec0ffce4e40ffee4ec0ff0e6ec0ff2e6e40ff4e6ec0ff6e6e40ff8e6ec0ffae6e40ffce6ec0ffee6e40ff0e8e40ff2e8ec0ff4e8e40ff6e8ec0ff8e8e40ffae8ec0ffce8e40ffee8ec0ff0eaec0ff2eae40ff4eaec0ff6eae40ff8eaec0ffaeae40ffceaec0ffeeae40ff0ece40ff2ecec0ff4ece40ff6ecec0ff8ece40ffaecec0ffcece40ffeecec0ff0eeec0ff2eee40ff4eeec0ff6eee40ff8eeec0ffaeee40ffceeec0ffeeee40ff ms=2 *4549d2a9 +K tex-rgb10a2-near n=2 d=0 ci_affine_nearest_premul[0=bgra8:o,1=rgb10a2:i0,s0]*2 *34f8d0f8 +C cg-rgba8 128x128 st=ok in=cg-rgba8 h=d6f0f1b08c4ed531,d6f0f1b08c4ed531 hb=d6f0f1b08c4ed531 sent=0 p=00fcc0ff,fcfc40ff,000040ff,fc00c0ff,7f7d90ff,3fbd90ff,bfbd90ff,3f3d90ff,bf3d90ff g=077da0ff,177da0ff,277d60ff,377d60ff,477da0ff,577da0ff,677d60ff,777d60ff,877da0ff,977da0ff,a77d60ff,b77d60ff,c77da0ff,d77da0ff,e77d60ff,f77d60ff t=0eeebcff2eee46ff4eeebaff6eee46ff8eeebaffaeee46ffceeebaffeeee44ff0ece46ff2eceb8ff4ece48ff6eceb8ff8ece48ffaeceb8ffcece48ffeecebaff0eaebaff2eae48ff4eaeb8ff6eae48ff8eaeb8ffaeae48ffceaeb8ffeeae46ff0e8e46ff2e8eb8ff4e8e48ff6e8eb8ff8e8e48ffae8eb8ffce8e48ffee8ebaff0e6ebaff2e6e48ff4e6eb8ff6e6e48ff8e6eb8ffae6e48ffce6eb8ffee6e46ff0e4e46ff2e4eb8ff4e4e48ff6e4eb8ff8e4e48ffae4eb8ffce4e48ffee4ebaff0e2ebaff2e2e48ff4e2eb8ff6e2e48ff8e2eb8ffae2e48ffce2eb8ffee2e46ff0e0e44ff2e0ebaff4e0e46ff6e0ebaff8e0e46ffae0ebaffce0e46ffee0ebcff ms=2 *2dc6981f +K cg-rgba8 n=2 d=0 ci_affine_clamp_to_alpha[0=bgra8:o,1=rgba8:x,s0]*2 *beb8dff7 +C cg-ga8 128x128 st=ok in=cg-ga8 h=5f191369f1d1c7ea,5f191369f1d1c7ea hb=5f191369f1d1c7ea sent=0 p=00000081,7f7f7f81,000000ff,fcfcfcff,606060c0,272727a0,787878a0,373737e0,a8a8a8e0 g=050505c0,111111c0,1d1d1dc0,292929c0,353535c0,424242c0,4e4e4ec0,5a5a5ac0,666666c0,727272c0,7e7e7ec0,8a8a8ac0,969696c0,a2a2a2c0,afafafc0,bbbbbbc0 t=08080888191919882a2a2a883b3b3b884c4c4c885d5d5d886e6e6e887f7f7f88080808981b1b1b982e2e2e984242429855555598686868987b7b7b988e8e8e98090909a81e1e1ea8333333a8494949a85e5e5ea8737373a8888888a89d9d9da80a0a0ab8212121b8383838b84f4f4fb8676767b87d7d7db8959595b8acacacb80b0b0bc8242424c83d3d3dc8565656c86f6f6fc8888888c8a1a1a1c8bbbbbbc80c0c0cd8272727d8424242d85d5d5dd8787878d8939393d8aeaeaed8c9c9c9d80d0d0de82a2a2ae8474747e8646464e8818181e89e9e9ee8bbbbbbe8d8d8d8e80e0e0ef82d2d2df84c4c4cf86b6b6bf88a8a8af8a9a9a9f8c8c8c8f8e7e7e7f8 ms=2 *6be1b569 +K cg-ga8 n=2 d=0 ci_affine_clamp_to_alpha_premul_linear_t[0=bgra8:o,1=rg8:x,s0]*2 *2f6000dc +C cg-ga16f 128x128 st=ok in=cg-ga16f h=b3d788b877ab5d13,b3d788b877ab5d13 hb=b3d788b877ab5d13 sent=0 p=00000081,80808081,000000ff,fefefeff,8d8d8dc0,565656a0,8d8d8da0,787878e0,c6c6c6e0 g=232323c0,404040c0,525252c0,606060c0,6c6c6cc0,777777c0,818181c0,898989c0,919191c0,999999c0,a0a0a0c0,a6a6a6c0,acacacc0,b2b2b2c0,b9b9b9c0,bebebec0 t=202020883e3e3e88505050885e5e5e8869696988737373887c7c7c88848484882323239846464698595959986868689875757598808080988a8a8a9893939398282828a84d4d4da8636363a8737373a8828282a88e8e8ea8999999a8a3a3a3a82b2b2bb8555555b86c6c6cb87f7f7fb88e8e8eb89b9b9bb8a7a7a7b8b3b3b3b82f2f2fc85c5c5cc8767676c88a8a8ac89a9a9ac8a9a9a9c8b6b6b6c8c2c2c2c8333333d8636363d87f7f7fd8959595d8a7a7a7d8b6b6b6d8c5c5c5d8d1d1d1d8373737e86b6b6be8888888e89f9f9fe8b3b3b3e8c4c4c4e8d3d3d3e8e1e1e1e83b3b3bf8727272f8929292f8aaaaaaf8bfbfbff8d1d1d1f8e2e2e2f8f0f0f0f8 ms=3 *423b4430 +K cg-ga16f n=2 d=0 ci_affine_clamp_to_alpha_premul_linear_t[0=bgra8:o,1=rg16f:x,s0]*2 *6219902b +C cg-g8 128x128 st=ok in=cg-g8 h=e8026f5ff62d2f25,e8026f5ff62d2f25 hb=e8026f5ff62d2f25 sent=0 p=000000ff,fcfcfcff,000000ff,fcfcfcff,7f7f7fff,3f3f3fff,bfbfbfff,3f3f3fff,bfbfbfff g=070707ff,171717ff,272727ff,373737ff,474747ff,575757ff,676767ff,777777ff,878787ff,979797ff,a7a7a7ff,b7b7b7ff,c7c7c7ff,d7d7d7ff,e7e7e7ff,f7f7f7ff t=0e0e0eff2e2e2eff4e4e4eff6e6e6eff8e8e8effaeaeaeffcececeffeeeeeeff0e0e0eff2e2e2eff4e4e4eff6e6e6eff8e8e8effaeaeaeffcececeffeeeeeeff0e0e0eff2e2e2eff4e4e4eff6e6e6eff8e8e8effaeaeaeffcececeffeeeeeeff0e0e0eff2e2e2eff4e4e4eff6e6e6eff8e8e8effaeaeaeffcececeffeeeeeeff0e0e0eff2e2e2eff4e4e4eff6e6e6eff8e8e8effaeaeaeffcececeffeeeeeeff0e0e0eff2e2e2eff4e4e4eff6e6e6eff8e8e8effaeaeaeffcececeffeeeeeeff0e0e0eff2e2e2eff4e4e4eff6e6e6eff8e8e8effaeaeaeffcececeffeeeeeeff0e0e0eff2e2e2eff4e4e4eff6e6e6eff8e8e8effaeaeaeffcececeffeeeeeeff ms=2 *629ba8a8 +K cg-g8 n=2 d=0 ci_affine_swizzle_rrr1[0=bgra8:o,1=r8:x,s0]*2 *d93d598e +C lanczos-rgba8s 40x40 st=ok in=rgba8s h=4ba0f959b0d0cc3d,4ba0f959b0d0cc3d hb=4ba0f959b0d0cc3d sent=0 p=00000ce9,e0007be9,00e07be9,e0e00ce9,38382bff,0e0e2bff,880e2bff,0e882bff,88882bff g=013921ff,02381cff,053884ff,093884ff,10381cff,17381cff,233884ff,2d3884ff,3e381cff,4c381cff,633884ff,753884ff,93381cff,a9381cff,cd3884ff,eb3985ff t=010112fb070184fd140114fd280184fd450114fd6c0184fd9d0114fdd70184fb010784fd070717ff140785ff280717ff450785ff6c0717ff9e0785ffd90714fd011414fd071485ff141417ff281485ff451417ff6c1485ff9e1417ffd91484fd012884fd072817ff142885ff282817ff452885ff6c2817ff9e2885ffd92814fd014514fd074585ff144517ff284585ff454517ff6c4585ff9e4517ffd94584fd016c84fd076c17ff146c85ff286c17ff456c85ff6c6c17ff9e6c85ffd96c14fd019d14fd079e85ff149e17ff289e85ff459e17ff6c9e85ff9e9e17ffd99d84fd01d784fb07d914fd14d984fd28d914fd45d984fd6cd914fd9dd984fdd7d712fb ms=4 *3af626a4 +K lanczos-rgba8s n=6 d=0 ci_premul_srgb_to_linear_unpremul[0=rgba16f:x,1=rgba8s:i0,s0]*2;ci_lanczosDownH[0=rgba16f:x,1=rgba16f:x,s0]*2;ci_clamp_to_alpha_premul_linear_to_srgb_[0=bgra8:o,1=rgba16f:x,s0]*2 *48730eb7 +C blur-rgba8 128x128 st=ok in=rgba8 h=fb07a0817d8fe8b1,fb07a0817d8fe8b1 hb=fb07a0817d8fe8b1 sent=0 p=010140ff,fb01c0ff,01fbc0ff,fbfb40ff,7f7f88ff,3f3f88ff,bf3f88ff,3fbf88ff,bfbf88ff g=077f82ff,177f82ff,277f97ff,377f96ff,477f82ff,577f82ff,677f97ff,777f96ff,877f82ff,977f82ff,a77f97ff,b77f96ff,c77f82ff,d77f82ff,e77f97ff,f77f98ff t=0e0e51ff2e0eb1ff4e0e59ff6e0eb1ff8e0e59ffae0eb1ffce0e59ffee0eb6ff0e2eb1ff2e2e60ff4e2eadff6e2e60ff8e2eadffae2e60ffce2eadffee2e59ff0e4e59ff2e4eadff4e4e60ff6e4eadff8e4e60ffae4eadffce4e60ffee4eb1ff0e6eb1ff2e6e60ff4e6eadff6e6e60ff8e6eadffae6e60ffce6eadffee6e59ff0e8e59ff2e8eadff4e8e60ff6e8eadff8e8e60ffae8eadffce8e60ffee8eb1ff0eaeb1ff2eae60ff4eaeadff6eae60ff8eaeadffaeae60ffceaeadffeeae59ff0ece59ff2eceadff4ece60ff6eceadff8ece60ffaeceadffcece60ffeeceb1ff0eeeb6ff2eee59ff4eeeb1ff6eee59ff8eeeb1ffaeee59ffceeeb1ffeeee51ff ms=9 *61f773cf +K blur-rgba8 n=8 d=0 ci_affine_clampRect_premul_srgb_to_linea[0=rgba16f:x,1=rgba8:i0,s0]*2;F13V[0=rgba16f:x,1=rgba16f:x,s0]*2;F13H[0=rgba16f:x,1=rgba16f:x,s0]*2;ci_affine_clamp_to_alpha_premul_linear_t[0=bgra8:o,1=rgba16f:x,s0]*2 *dc84b1a6 +C fill 64x64 st=ok in=none h=df48cf684e74c325,df48cf684e74c325 hb=df48cf684e74c325 sent=0 p=4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff g=4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff,4080bfff t=4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff4080bfff ms=2 *55c0282d +K fill n=2 d=0 ci_fill[0=bgra8:o]*2 *26dc5965 +C multi-2 96x96 st=ok in=rgba8s+rgba8s h=68f1e190bbbc8225,68f1e190bbbc8225 hb=68f1e190bbbc8225 sent=0 p=ca0505ff,05930cff,262804c0,262804c0,05930cff,ca0505ff,05930cff,262804c0,262804c0 g=ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,05930cff,05930cff,05930cff,05930cff,05930cff,05930cff,05930cff,05930cff t=ca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cff262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0 s=ca0505ff,05930cff/262804c0,262804c0 ms=2 *bfd81aa1 +K multi-2 n=2 d=0 ci_clamp_to_alpha_premul_linear_to_srgb_[0=bgra8:o,1=rgba8s:i1,2=rgba8s:i0,s0,s1]*2 *28cc071a +C multi-3 96x96 st=ok in=rgba8s+rgba8s+rgba8s h=10c471ae9b3e1325,10c471ae9b3e1325 hb=10c471ae9b3e1325 sent=0 p=ca0505ff,0814caff,1c1f3adf,1c1f3adf,05930cff,ca0505ff,0814caff,1c1f3adf,1c1f3adf g=ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,05930cff,05930cff,05930cff,05930cff,05930cff,05930cff,0814caff,0814caff,0814caff,0814caff,0814caff t=ca0505ffca0505ff883407ff05930cff05930cff073e8bff0814caff0814caffca0505ffca0505ff883407ff05930cff05930cff073e8bff0814caff0814caffca0505ffca0505ff883407ff05930cff05930cff073e8bff0814caff0814caffca0505ffca0505ff883407ff05930cff05930cff073e8bff0814caff0814caffca0505ffca0505ff883407ff05930cff05930cff073e8bff0814caff0814caffca0505ffca0505ff883407ff05930cff05930cff073e8bff0814caff0814caff1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf1c1f3adf s=ca0505ff,05930cff,0814caff/1c1f3adf,1c1f3adf,1c1f3adf ms=2 *1fa05b3d +K multi-3 n=2 d=0 ci_clamp_to_alpha_premul_linear_to_srgb_[0=bgra8:o,1=rgba8s:i2,2=rgba8s:i1,3=rgba8s:i0,s0,s1,s2]*2 *56939a93 +C multi-4 96x96 st=ok in=rgba8s+rgba8s+rgba8s+rgba8s h=45c0c22021566325,45c0c22021566325 hb=45c0c22021566325 sent=0 p=ca0505ff,de9303ff,46322aef,46322aef,0814caff,05930cff,de9303ff,46322aef,46322aef g=ca0505ff,ca0505ff,ca0505ff,ca0505ff,05930cff,05930cff,05930cff,05930cff,0814caff,0814caff,0814caff,0814caff,de9303ff,de9303ff,de9303ff,de9303ff t=ca0505ffca0505ff05930cff05930cff0814caff0814caffde9303ffde9303ffca0505ffca0505ff05930cff05930cff0814caff0814caffde9303ffde9303ffca0505ffca0505ff05930cff05930cff0814caff0814caffde9303ffde9303ffca0505ffca0505ff05930cff05930cff0814caff0814caffde9303ffde9303ffca0505ffca0505ff05930cff05930cff0814caff0814caffde9303ffde9303ffca0505ffca0505ff05930cff05930cff0814caff0814caffde9303ffde9303ff46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef46322aef s=ca0505ff,05930cff,0814caff,de9303ff/46322aef,46322aef,46322aef,46322aef ms=2 *d97e59b2 +K multi-4 n=2 d=0 ci_clamp_to_alpha_premul_linear_to_srgb_[0=bgra8:o,1=rgba8s:i3,2=rgba8s:i2,3=rgba8s:i1,4=rgba8s:i0,s0,s1,s2,s3]*2 *0717400f +C multi-6 96x96 st=ok in=rgba8s+rgba8s+rgba8s+rgba8s+rgba8s+rgba8s h=48a2f683317e9b25,48a2f683317e9b25 hb=48a2f683317e9b25 sent=0 p=ca0505ff,03a4a4ff,2b3940fb,2b3940fb,de9303ff,05930cff,740c93ff,2b3940fb,2b3940fb g=ca0505ff,ca0505ff,ca0505ff,05930cff,05930cff,0814caff,0814caff,0814caff,de9303ff,de9303ff,de9303ff,740c93ff,740c93ff,03a4a4ff,03a4a4ff,03a4a4ff t=ca0505ff47640aff06694bff0814caffde9303ff973963ff4e3f99ff03a4a4ffca0505ff47640aff06694bff0814caffde9303ff973963ff4e3f99ff03a4a4ffca0505ff47640aff06694bff0814caffde9303ff973963ff4e3f99ff03a4a4ffca0505ff47640aff06694bff0814caffde9303ff973963ff4e3f99ff03a4a4ffca0505ff47640aff06694bff0814caffde9303ff973963ff4e3f99ff03a4a4ffca0505ff47640aff06694bff0814caffde9303ff973963ff4e3f99ff03a4a4ff2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb2b3940fb s=ca0505ff,05930cff,0814caff,de9303ff,740c93ff,03a4a4ff/2b3940fb,2b3940fb,2b3940fb,2b3940fb,2b3940fb,2b3940fb ms=4 *597c7046 +K multi-6 n=2 d=0 ci_clamp_to_alpha_premul_linear_to_srgb_[0=bgra8:o,1=rgba8s:i5,2=rgba8s:i4,3=rgba8s:i3,4=rgba8s:i2,5=rgba8s:i1,6=rgba8s:i0,s0,s1,s2,s3,s4,s5]*2 *f5cc0dfb +C multi-6-bgra8 96x96 st=ok in=bgra8+bgra8+bgra8+bgra8+bgra8+bgra8 h=3cf3913f088aa325,3cf3913f088aa325 hb=3cf3913f088aa325 sent=0 p=e62828ff,1ed2d2ff,86adbdfb,86adbdfb,f0c81eff,28c83cff,b43cc8ff,86adbdfb,86adbdfb g=e62828ff,e62828ff,e62828ff,28c83cff,28c83cff,3250e6ff,3250e6ff,3250e6ff,f0c81eff,f0c81eff,f0c81eff,b43cc8ff,b43cc8ff,1ed2d2ff,1ed2d2ff,1ed2d2ff t=e62828ff679335ff2ba075ff3250e6fff0c81effc86b8fff826ecbff1ed2d2ffe62828ff679335ff2ba075ff3250e6fff0c81effc86b8fff826ecbff1ed2d2ffe62828ff679335ff2ba075ff3250e6fff0c81effc86b8fff826ecbff1ed2d2ffe62828ff679335ff2ba075ff3250e6fff0c81effc86b8fff826ecbff1ed2d2ffe62828ff679335ff2ba075ff3250e6fff0c81effc86b8fff826ecbff1ed2d2ffe62828ff679335ff2ba075ff3250e6fff0c81effc86b8fff826ecbff1ed2d2ff86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb86adbdfb s=e62828ff,28c83cff,3250e6ff,f0c81eff,b43cc8ff,1ed2d2ff/86adbdfb,86adbdfb,86adbdfb,86adbdfb,86adbdfb,86adbdfb ms=3 *53eeecc5 +K multi-6-bgra8 n=2 d=0 ci_clamp_to_alpha_premul_linear_to_srgb_[0=bgra8:o,1=bgra8:i5,2=bgra8:i4,3=bgra8:i3,4=bgra8:i2,5=bgra8:i1,6=bgra8:i0,s0,s1,s2,s3,s4,s5]*2 *271cbc39 +C filter-srcover-2 96x96 st=ok in=rgba8s+rgba8s h=68f1e190bbbc8225,68f1e190bbbc8225 hb=68f1e190bbbc8225 sent=0 p=ca0505ff,05930cff,262804c0,262804c0,05930cff,ca0505ff,05930cff,262804c0,262804c0 g=ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,ca0505ff,05930cff,05930cff,05930cff,05930cff,05930cff,05930cff,05930cff,05930cff t=ca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cffca0505ffca0505ffca0505ffca0505ff05930cff05930cff05930cff05930cff262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0262804c0 s=ca0505ff,05930cff/262804c0,262804c0 ms=2 *d9d8d287 +K filter-srcover-2 n=2 d=0 ci_clamp_to_alpha_premul_linear_to_srgb_[0=bgra8:o,1=rgba8s:i1,2=rgba8s:i0,s0,s1]*2 *96bcbc95 +C mixed-3 96x96 st=ok in=cg-ga8+rgba8s+bgra8 h=c895b1e070a11b25,c895b1e070a11b25 hb=c895b1e070a11b25 sent=0 p=30303080,3250e6ff,3c51aeff,1e3890c0,05930cff,616161ff,3250e6ff,3c51aeff,1e3890c0 g=616161ff,616161ff,616161ff,616161ff,616161ff,05930cff,05930cff,05930cff,05930cff,05930cff,05930cff,3250e6ff,3250e6ff,3250e6ff,3250e6ff,3250e6ff t=3030308030303080225124aa05930cff05930cff23669dff3250e6ff3250e6ff3030308030303080225124aa05930cff05930cff23669dff3250e6ff3250e6ff616161ff616161ff427245ff05930cff05930cff23669dff3250e6ff3250e6ff616161ff616161ff427245ff05930cff05930cff23669dff3250e6ff3250e6ff616161ff616161ff427245ff05930cff05930cff23669dff3250e6ff3250e6ff616161ff616161ff427245ff05930cff05930cff23669dff3250e6ff3250e6ff3c51aeff3c51aeff3249a4ea1e3890c01e3890c01e3890c01e3890c01e3890c03c51aeff3c51aeff3249a4ea1e3890c01e3890c01e3890c01e3890c01e3890c0 s=515151d6,05930cff,3250e6ff/3c51aeff,1e3890c0,1e3890c0 ms=2 *fa87a856 +K mixed-3 n=2 d=0 ci_clamp_to_alpha_premul_linear_to_srgb_[0=bgra8:o,1=bgra8:i2,2=rgba8s:i1,3=rg8:x,s0,s1,s2]*2 *b002e24e +D diag=/private/var/tmp/adreno-icon-pixels-2871.log absent *8c0545f2 +END command=synth cases=38 errors=0 *0510e4e8 diff --git a/tools/run-icon-pixels.py b/tools/run-icon-pixels.py new file mode 100644 index 0000000..616c625 --- /dev/null +++ b/tools/run-icon-pixels.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Run icon-pixels in the A16 guest from its root console shell, fetch its lines and compare them with this Mac's. + +During a --console desktop boot, once the multi-user root shell answers, as the only console client (pass the run's +--owner). Writes nothing outside the guest's /private/var/tmp/icon-pixels (and, with --move-icon-caches, renames the +icon caches aside). Every console command's output stays under about 4 KB. + + python3 tools/run-icon-pixels.py --device udp://enN --launch-record build/PREFIX-early-stop-launch.json \\ + --owner HEX --tool build/icon-pixels/TOOL/icon-pixels --output NEW-DIR \\ + [--reference-synth REF.txt] [--reference-icons REF.txt] [--steps push,version,synth,icons,diag] \\ + [--move-icon-caches TAG] [--synth-timeout 1200] + +Steps (--steps picks and orders them; each is recorded in OUTPUT/run.json): + push the tool as base64 in 512-character printf chunks (tools/deploy-guest-plugin.py's transfer: one numbered file + per chunk, a SHA-256 listing finds chunks not held intact and resends them), joined, decoded, its SHA-256 + checked, then installed as /private/var/tmp/icon-pixels/icon-pixels (mode 755) + version `icon-pixels version`: the binary executes (an ad-hoc arm64e executable; if the guest's AMFI kills it, the + step stops and names the trust-cache row build-icon-pixels.py recorded) and finds the plug-in device + synth `icon-pixels synth --repeat 2` as root: CoreImage kernels on synthetic inputs, on the plug-in device + icons `icon-pixels icons --source gen` as root (IconServices' generator in-process, on the plug-in device), then, + as the console user in that user's bootstrap context (launchctl asuser UID sudo -u #UID), + `icon-pixels icons --source ws` (NSWorkspace, through iconservicesagent and its caches) + recache with --move-icon-caches TAG: rename the IconServices store, every user's com.apple.iconservices and + com.apple.iconservicesagent caches and the Dock's com.apple.dock.iconcache to NAME.TAG (reversible: mv back), + restart iconservicesd (launchctl kickstart -k), stop every iconservicesagent, the Dock and System Settings, so + the next icons drawn are rendered again with the installed plug-in. Put it before `icons` + diag the plug-in's diagnostics: per-process files /private/var/tmp/adreno-*.log (counts, distinct drop/refusal + reasons), and the unified log subsystem org.snapintosh.adreno for iconservicesagent and System Settings +Each output file is fetched 3 lines per command; every line carries a CRC, so console noise is dropped and a block that +came back short is fetched again (up to 4 times). Then tools/compare-icon-pixels.py against the references. +Offline (the console stand-in, tools/test-icon-pixels.py): --target 127.0.0.1:PORT --session HEX --guest-root DIR. +""" +import argparse +import base64 +import hashlib +import importlib.util +import json +from pathlib import Path +import re +import shlex +import subprocess +import sys +import time +import zlib + +ROOT = Path(__file__).resolve().parents[1] +REMOTE = '/private/var/tmp/icon-pixels' +BLOCK = 3 +STEPS = 'push,version,synth,icons,diag' + + +def load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def valid(line): + body, sep, crc = line.strip().rpartition(' *') + return bool(sep) and re.fullmatch(r'[0-9a-f]{8}', crc or '') is not None and \ + f'{zlib.crc32(body.encode()) & 0xffffffff:08x}' == crc + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + where = ap.add_mutually_exclusive_group(required=True) + where.add_argument('--device') + where.add_argument('--target', help='host:port of the console stand-in (offline test)') + ap.add_argument('--launch-record') + ap.add_argument('--session') + ap.add_argument('--owner', required=True, help='hex console owner of this run (one owner for every console step)') + ap.add_argument('--tool', type=Path, required=True) + ap.add_argument('--output', type=Path, required=True) + ap.add_argument('--steps', default=STEPS) + ap.add_argument('--reference-synth', type=Path) + ap.add_argument('--reference-icons', type=Path) + ap.add_argument('--move-icon-caches', metavar='TAG') + ap.add_argument('--synth-args', default='--repeat 2') + ap.add_argument('--icons-args', default='--size 20 --scale 2') + ap.add_argument('--synth-timeout', type=int, default=1200) + ap.add_argument('--icons-timeout', type=int, default=600) + ap.add_argument('--guest-root', default='/', help='prefix of every guest path (offline test)') + ap.add_argument('--console-user', help='uid for the ws icons (default: the owner of /dev/console)') + ap.add_argument('--poll-ms', type=int, default=50) + ap.add_argument('--wait-ms', type=int, default=150) + ap.add_argument('--attempts', type=int, default=30) + a = ap.parse_args(argv) + steps = [s for s in a.steps.split(',') if s] + unknown = set(steps) - {'push', 'version', 'synth', 'icons', 'recache', 'diag'} + if unknown or ('recache' in steps) != bool(a.move_icon_caches): + ap.error(f'unknown steps {sorted(unknown)}, or recache without --move-icon-caches TAG (or the reverse)') + # Offline the "guest" shell is this Mac's: never restart its processes, never ask its sudo for a password. + if a.target and 'recache' in steps: + ap.error('recache restarts the shell machine\'s icon services, Dock and System Settings: only with --device') + if a.target and 'icons' in steps and a.console_user is None: + ap.error('offline, pass --console-user (0 skips the NSWorkspace icons)') + if a.move_icon_caches and not re.fullmatch(r'[A-Za-z0-9._-]{1,48}', a.move_icon_caches): + ap.error('the cache tag is letters, digits, dot, dash and underscore') + out = a.output.resolve() + out.mkdir(parents=True, exist_ok=False) + deploy = load('deploy', ROOT / 'tools/deploy-guest-plugin.py') + tool = a.tool.read_bytes() + tool_sha = hashlib.sha256(tool).hexdigest() + build = a.tool.resolve().parent / 'build.json' + trust_row = json.loads(build.read_text()).get('trust_row') if build.is_file() else None + args = argparse.Namespace(session=a.session, launch_record=a.launch_record, device=a.device, target=a.target, + owner=a.owner, wait_ms=a.wait_ms, poll_ms=a.poll_ms, test_skip_chunk=[]) + root = a.guest_root.rstrip('/') + remote = root + REMOTE + exe = f'{remote}/icon-pixels' + rec = dict(schema='snapintosh-icon-pixels-run-v1', started=time.time(), tool=str(a.tool), tool_sha256=tool_sha, + tool_bytes=len(tool), remote=remote, steps=steps, status='RUNNING') + shell = deploy.Shell(args) + + def fetch(name, path): + """The file's lines, BLOCK per command, checksummed; written to OUTPUT/NAME.""" + count = shell.run(f'{name}-lines', f"awk 'END{{print NR}}' {shlex.quote(path)}", ok=(0, 1)) + m = re.search(r'^(\d+)\s*$', count, re.M) + lines = int(m.group(1)) if m else 0 + kept, refetched, t0 = [], 0, time.monotonic() + for first in range(1, lines + 1, BLOCK): + last = min(lines, first + BLOCK - 1) + want = last - first + 1 + good = [] + for _ in range(5): + text = shell.run('fetch', f"sed -n '{first},{last}p' {shlex.quote(path)}", timeout=60, ok=(0, 1)) + good = [l.strip() for l in text.splitlines() if valid(l)] + if len(good) >= want: + break + refetched += 1 + kept += good[:want] + (out / name).write_text('\n'.join(kept) + ('\n' if kept else '')) + rec[name] = dict(lines=lines, fetched=len(kept), refetched_blocks=refetched, + seconds=round(time.monotonic() - t0, 1)) + return kept + + def run_tool(name, command, timeout): + t0 = time.monotonic() + res = shell.run(name, f'cd {shlex.quote(remote)} && {command} > {name}.txt 2> {name}.err; echo rc=$?; ' + f"awk 'END{{print NR}}' {name}.txt; tail -c 600 {name}.err", timeout=timeout, ok=(0, 1)) + m = re.search(r'rc=(\d+)\s+(\d+)', res) + rec[f'{name}_rc'] = int(m.group(1)) if m else None + rec[f'{name}_seconds'] = round(time.monotonic() - t0, 1) + rec[f'{name}_stderr_tail'] = res[res.find('rc='):][-700:] if 'rc=' in res else res[-700:] + return fetch(f'{name}.txt', f'{remote}/{name}.txt') + + try: + rec['shell_attempts'] = deploy.wait_for_shell(shell, a.attempts, 20) + for step in steps: + if step == 'push': + shell.run('work', f'mkdir -p {shlex.quote(remote)} && rm -rf {shlex.quote(remote)}/push') + t0 = time.monotonic() + f = dict(name='icon-pixels', text=base64.b64encode(tool).decode(), payload_sha256=tool_sha, + new_sha256=tool_sha, patch=False, path=None) + rec['push'] = deploy.transfer(shell, args, f'{remote}/push', f) + shell.run('install', f'cp {shlex.quote(remote)}/push/icon-pixels.new {shlex.quote(exe)}.new && ' + f'chmod 755 {shlex.quote(exe)}.new && mv -f {shlex.quote(exe)}.new {shlex.quote(exe)} && ' + f'rm -rf {shlex.quote(remote)}/push') + if shell.digest('installed', exe) != tool_sha: + raise deploy.Refused('the installed tool differs from the one pushed') + rec['push_seconds'] = round(time.monotonic() - t0, 1) + elif step == 'version': + res = shell.run('version', f'{shlex.quote(exe)} version 2>&1; echo rc=$?', timeout=120, ok=(0, 1)) + m = re.search(r'rc=(\d+)', res) + rec['version_rc'] = int(m.group(1)) if m else None + rec['version'] = [l.strip() for l in res.splitlines() if valid(l)] + if rec['version_rc'] != 0 or not any(l.startswith('V ') for l in rec['version']): + hint = (f' If it was killed (rc 137, "Killed: 9"), the guest refuses the ad-hoc signature: add the row ' + f'{json.dumps(trust_row)} to the next run\'s trust additions.' if trust_row else '') + raise deploy.Refused(f'icon-pixels does not run: {res[-300:]!r}.{hint}') + elif step == 'synth': + run_tool('synth', f'./icon-pixels synth {a.synth_args}', a.synth_timeout) + elif step == 'icons': + run_tool('icons-gen', f'./icon-pixels icons --source gen {a.icons_args}', a.icons_timeout) + uid = a.console_user or shell.run('console-user', 'stat -f %u /dev/console').strip().splitlines()[-1] + rec['console_user'] = uid + if uid.isdigit() and uid != '0': + run_tool('icons-ws', f'launchctl asuser {uid} sudo -u "#{uid}" ./icon-pixels icons --source ws ' + f'{a.icons_args}', a.icons_timeout) + else: + rec['icons_ws'] = f'skipped: /dev/console belongs to {uid!r} (no logged-in user)' + elif step == 'recache': + tag = a.move_icon_caches + paths = (f'{root}/Library/Caches/com.apple.iconservices.store ' + f'{root}/private/var/folders/*/*/C/com.apple.iconservices ' + f'{root}/private/var/folders/*/*/C/com.apple.iconservicesagent ' + f'{root}/private/var/folders/*/*/C/com.apple.dock.iconcache') + rec['recache'] = shell.run('recache', ( + f'for p in {paths}; do if [ -e "$p" ]; then mv "$p" "$p.{tag}" && echo "moved $p" || echo "FAILED $p"; ' + f'fi; done; launchctl kickstart -k system/com.apple.iconservices.iconservicesd 2>&1 | tail -1; ' + f'killall iconservicesagent Dock "System Settings" 2>&1 | tail -3; echo recache-done'), + timeout=120, ok=(0, 1))[-3000:] + elif step == 'diag': + rec['diag_files'] = shell.run('diag-files', ( + f'ls {root}/private/var/tmp/ | grep -c "^adreno-.*\\.log$"; ' + f'ls -t {root}/private/var/tmp/ | grep "^adreno-.*\\.log$" | head -12'), ok=(0, 1))[-1500:] + rec['diag_reasons'] = shell.run('diag-reasons', ( + f'cat {root}/private/var/tmp/adreno-*.log 2>/dev/null | grep -E "dropped:|refused:|zero texture" | ' + f'sed -E "s/^.*(dropped:|refused:|zero texture:)/\\1/" | cut -c1-200 | sort | uniq -c | sort -rn | ' + f'head -15'), + timeout=120, ok=(0, 1))[-3500:] + rec['diag_unified'] = shell.run('diag-unified', ( + 'log show --last 60m --style compact --predicate \'subsystem == "org.snapintosh.adreno" AND ' + '(process == "iconservicesagent" OR process == "System Settings")\' 2>/dev/null | ' + 'grep -E "dropped|refused|not built|zero texture|failed" | cut -c1-220 | tail -15'), + timeout=600, ok=(0, 1))[-3500:] + rec['status'] = 'DONE' + except deploy.Refused as error: + rec['status'] = 'REFUSED' + rec['error'] = str(error) + finally: + rec['commands'] = [s for s in shell.steps if s['step'] not in ('chunk', 'fetch')] + rec['finished'] = time.time() + (out / 'run.json').write_text(json.dumps(rec, indent=1) + '\n') + compares = {} + for name, ref, cases in (('synth.txt', a.reference_synth, '.'), ('icons-gen.txt', a.reference_icons, '^gen-'), + ('icons-ws.txt', a.reference_icons, '^ws-')): + if ref and (out / name).is_file(): + r = subprocess.run([sys.executable, str(ROOT / 'tools/compare-icon-pixels.py'), str(ref), str(out / name), + '--cases', cases, '--json', str(out / f'{name}.compare.json')], capture_output=True, text=True) + (out / f'{name}.compare.txt').write_text(r.stdout + r.stderr) + compares[name] = r.stdout.strip().splitlines()[-1] if r.stdout.strip() else f'exit {r.returncode}' + print(r.stdout[-2500:]) + summary = {k: v for k, v in rec.items() if k not in ('commands', 'push')} + summary['compare'] = compares + print(json.dumps(summary, indent=1)[-4000:]) + return 0 if rec['status'] == 'DONE' else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/test-icon-pixels.py b/tools/test-icon-pixels.py new file mode 100644 index 0000000..c832a20 --- /dev/null +++ b/tools/test-icon-pixels.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""icon-pixels offline: the tool on this Mac's GPU, the comparison's diagnoses, and the console procedure end to end. + + python3 tools/test-icon-pixels.py --output NEW-DIR [--bundle ADRENO-BUNDLE] [--skip-console] [--noise-rate 40] + +1. tools/build-icon-pixels.py builds the tool; it runs `synth` on this Mac's GPU twice (deterministic: identical C + lines) and against tools/icon-pixels-reference-synth.txt (the committed synthetic reference, made with --host-caps + on the deployed plug-in): every case must PASS, or the reference is stale for this Mac's macOS/CoreImage. +2. With --bundle (an ADRENO_SETTINGS_ICONS plug-in bundle with its compiler runtime): `synth --plugin BUNDLE + --cs-samplers 8` (the plug-in device, commits intercepted) must dispatch exactly the kernels and texture formats + of the reference (compare --census-only PASS) with nothing dropped or refused; without the CS sampler stand-in the + multi-input kernels must be dropped (the deployed-kext gate) and the comparison must say so. +3. Fault injection: guest outputs derived from the reference, each with one fault the comparison must name: red/blue + swapped, an extra or a missing sRGB decode, transparent output, a zeroed channel, an unwritten and a partially + written destination, differing readbacks, differing repeats, a missing or foreign input stripe (with its texture + index), a broken blend band, an empty/black/partial/recoloured icon, console noise; and a perturbation within + tolerance that must still PASS. +4. Unless --skip-console: tools/run-icon-pixels.py over the console stand-in (tools/test-console-link.py: q1n1's + console handler, the monitor's console link, /bin/sh on a pty, request/reply loss and mid-line console noise) + pushes the tool, runs `version`, a few synth cases and two generated icons on this Mac's GPU and the diag step, + fetches every line and compares them with a direct run of the same cases: identical C and K lines. (recache and + the NSWorkspace icons act on the shell's own machine, so the runner refuses them offline.) +""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import zlib + +ROOT = Path(__file__).resolve().parents[1] +REFERENCE = ROOT / 'tools/icon-pixels-reference-synth.txt' +checks, failures = 0, [] + + +def check(ok, what): + global checks + checks += 1 + if not ok: + failures.append(what) + print('FAIL', what) + return ok + + +def load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def seal(body): + return f'{body} *{zlib.crc32(body.encode()) & 0xffffffff:08x}' + + +def body(line): + return line.rpartition(' *')[0] + + +def kind_lines(text, kinds): + return [l for l in text.splitlines() if l[:2] in kinds] + + +def edit(lines, case, fn, kind='C'): + """A copy of lines with case's line rewritten by fn(fields dict in order) and resealed.""" + out = [] + for l in lines: + b = body(l) + words = b.split(' ') + if len(words) > 2 and words[0] == kind and words[1] == case: + head, fields = words[:3], {} + order = [] + for w in words[3:]: + k, _, v = w.partition('=') + fields[k] = v + order.append(k) + fn(fields) + l = seal(' '.join(head + [f'{k}={fields[k]}' for k in order if k in fields])) + out.append(l) + return out + + +def map_pixels(fields, fn, keys=('p', 'g', 't', 's')): + """Apply fn([r,g,b,a]) -> [r,g,b,a] to every RRGGBBAA value in the sampled fields.""" + def one(h): + v = fn([int(h[i:i + 2], 16) for i in (0, 2, 4, 6)]) + return ''.join(f'{max(0, min(255, int(round(x)))):02x}' for x in v) + for k in keys: + if k not in fields: + continue + if k == 't': + t = fields['t'] + fields['t'] = ''.join(one(t[i:i + 8]) for i in range(0, len(t), 8)) + elif k == 's': + top, _, band = fields['s'].partition('/') + fields['s'] = ','.join(one(h) for h in top.split(',')) + '/' + ','.join(one(h) for h in band.split(',')) + else: + fields[k] = ','.join(one(h) for h in fields[k].split(',')) + + +def run_compare(cmp_, ref_lines, guest_lines, out, name, *extra): + ref, guest = out / f'{name}.ref.txt', out / f'{name}.guest.txt' + ref.write_text('\n'.join(ref_lines) + '\n') + guest.write_text('\n'.join(guest_lines) + '\n') + r = subprocess.run([sys.executable, str(ROOT / 'tools/compare-icon-pixels.py'), str(ref), str(guest), *extra], + capture_output=True, text=True) + (out / f'{name}.compare.txt').write_text(r.stdout + r.stderr) + verdicts = {} + for l in r.stdout.splitlines(): + m = re.match(r'(PASS|FAIL) (\S+?)(?:: (.*))?$', l) + if m and m.group(2) not in ('of',): + verdicts[m.group(2)] = (m.group(1), m.group(3) or '') + return r.returncode, verdicts, r.stdout + + +def fault_injection(out, ref_lines, icon_lines): + cmp_ = None + lines = ref_lines + icon_lines + rc, v, text = run_compare(cmp_, lines, lines, out, 'identical') + check(rc == 0 and all(x[0] == 'PASS' for x in v.values()), f'identical output must pass: {text[-300:]}') + + def expect(name, guest, case, verdict_prefix, contains=None): + rc, v, text = run_compare(cmp_, lines, guest, out, name) + got = v.get(case, ('MISSING', '')) + ok = got[0] == 'FAIL' and got[1].startswith(verdict_prefix) and (contains is None or contains in got[1]) + check(ok and rc == 1, f'{name}: {case} must FAIL with {verdict_prefix}{" ... " + contains if contains else ""}; ' + f'got {got} rc {rc}') + others = [c for c, x in v.items() if c != case and x[0] == 'FAIL'] + check(not others, f'{name}: only {case} may fail; also {others[:5]}') + + from_srgb = lambda c: 255 * ((c / 255) / 12.92 if c / 255 <= 0.04045 else ((c / 255 + 0.055) / 1.055) ** 2.4) + to_srgb = lambda c: 255 * ((c / 255) * 12.92 if c / 255 <= 0.0031308 else 1.055 * (c / 255) ** (1 / 2.4) - 0.055) + expect('swap', edit(lines, 'tex-rgba8-lin', lambda f: map_pixels(f, lambda p: [p[2], p[1], p[0], p[3]])), + 'tex-rgba8-lin', 'SWAP-RB') + expect('srgb-extra', edit(lines, 'tex-rgba8-lin', lambda f: map_pixels(f, lambda p: [from_srgb(p[0]), from_srgb(p[1]), + from_srgb(p[2]), p[3]])), + 'tex-rgba8-lin', 'SRGB-EXTRA') + expect('srgb-missing', edit(lines, 'tex-rgba8s-lin', lambda f: map_pixels(f, lambda p: [to_srgb(p[0]), to_srgb(p[1]), + to_srgb(p[2]), p[3]])), + 'tex-rgba8s-lin', 'SRGB-MISSING') + expect('transparent', edit(lines, 'tex-bgra8-near', lambda f: map_pixels(f, lambda p: [0, 0, 0, 0])), + 'tex-bgra8-near', 'TRANSPARENT') + expect('green-zero', edit(lines, 'tex-rg8-lin', lambda f: map_pixels(f, lambda p: [p[0], 0, p[2], p[3]])), + 'tex-rg8-lin', 'CHANNEL', 'G reads 0') + expect('unwritten', edit(lines, 'tex-rg16f-lin', lambda f: f.update(sent='16384')), 'tex-rg16f-lin', 'UNWRITTEN') + expect('partial', edit(lines, 'tex-r8-lin', lambda f: f.update(sent='512')), 'tex-r8-lin', 'PARTIAL') + expect('readback', edit(lines, 'tex-a8-lin', lambda f: f.update(hb='0123456789abcdef')), 'tex-a8-lin', 'READBACK') + expect('repeats', edit(lines, 'fill', lambda f: f.update(h=f['h'].split(',')[0] + ',0123456789abcdef')), + 'fill', 'NONDETERMINISTIC') + + def stripe(k, fn): + def apply(f): + top, _, band = f['s'].partition('/') + t = top.split(',') + t[k] = fn(t) + f['s'] = ','.join(t) + '/' + band + return apply + expect('input-missing', edit(lines, 'multi-4', stripe(2, lambda t: '00000000')), 'multi-4', 'INPUT-2', 'texture(2)') + expect('input-foreign', edit(lines, 'multi-6', stripe(5, lambda t: t[1])), 'multi-6', 'INPUT-5', "input 1's colour") + + def band(f): + top, _, b = f['s'].partition('/') + f['s'] = top + '/' + ','.join('10203040' for _ in b.split(',')) + expect('blend', edit(lines, 'mixed-3', band), 'mixed-3', 'BLEND') + expect('missing-case', [l for l in lines if not body(l).startswith('C cg-ga8 ')], 'cg-ga8', 'MISSING') + expect('error', edit(lines, 'cg-ga16f', lambda f: f.update(st='exception')), 'cg-ga16f', 'ERROR') + # Perturbation within tolerance (+-3 on every channel, alternating) must still pass. + flip = [1] + + def jitter(p): + flip[0] = -flip[0] + return [x + 3 * flip[0] for x in p] + rc, v, text = run_compare(cmp_, lines, edit(lines, 'tex-rgba16f-lin', lambda f: map_pixels(f, jitter)), out, 'jitter') + check(v.get('tex-rgba16f-lin', ('?',))[0] == 'PASS', f'jitter within tolerance must pass: {v.get("tex-rgba16f-lin")}') + # Console noise and a torn line are dropped (counted), the rest still compares. + noisy = [] + for i, l in enumerate(lines): + noisy.append(l) + if i % 5 == 0: + noisy.append('AdrenoDisplay: perf pace s=1 pps=59.8 late=0 gpu=1/4357') + torn = [l if not body(l).startswith('C fill ') else l[:-3] for l in noisy] + rc, v, text = run_compare(cmp_, lines, torn, out, 'noise') + check(v.get('fill', ('?',))[0] == 'FAIL' and 'MISSING' in v.get('fill', ('', ''))[1] and + sum(1 for x in v.values() if x[0] == 'FAIL') == 1 and 'noise dropped' in text, + f'noise: only the torn fill line is lost: {[c for c, x in v.items() if x[0] == "FAIL"]}') + if icon_lines: + icon = next(body(l).split(' ')[1] for l in icon_lines if body(l).startswith('I gen-folder ')) + expect('icon-empty', edit(lines, icon, lambda f: f.update(cov='0', rgb='000000', a='00', ag='0' * 64, lg='8' * 64), + kind='I'), icon, 'EMPTY') + expect('icon-black', edit(lines, icon, lambda f: f.update(rgb='020202'), kind='I'), icon, 'BLACK') + expect('icon-partial', edit(lines, icon, lambda f: f.update(ag='0' * 32 + f['ag'][32:]), kind='I'), icon, 'PARTIAL') + expect('icon-colour', edit(lines, icon, lambda f: f.update(rgb=f['rgb'][4:6] + f['rgb'][2:4] + f['rgb'][0:2]), + kind='I'), icon, 'WRONG-COLOUR') + + +def console_end_to_end(out, tool, noise_rate): + cl = load('consolelink', ROOT / 'tools/test-console-link.py') + binary = cl.build(out) + cases = 'tex-rgba8s-lin|tex-rg8-near|multi-3|fill' + direct = subprocess.run([str(tool), 'synth', '--repeat', '1', '--cases', cases], capture_output=True, text=True, + timeout=600) + (out / 'console-direct.txt').write_text(direct.stdout) + noise = out / 'noise.txt' + noise.write_text(''.join(f'AdrenoDisplay: perf pace s={i} pps=59.8 late=0 gpu=1/4357\n' for i in range(200))) + guest = out / 'guest' + (guest / 'private/var/tmp').mkdir(parents=True) + h = cl.Harness(binary, out, 'console', '--duration', 1500, '--shell', "PS1='sh-3.2# ' exec /bin/sh -i", + '--loss', 0.03, '--reply-loss', 0.03, '--seed', 47, '--poll-ms', 20, + '--noise', noise, '--noise-rate', noise_rate) + try: + r = subprocess.run([sys.executable, str(ROOT / 'tools/run-icon-pixels.py'), '--target', f'127.0.0.1:{h.port}', + '--session', f'{cl.SESSION:x}', '--owner', '5eedc0de51', '--guest-root', str(guest), + '--tool', str(tool), '--output', str(out / 'console-run'), + '--steps', 'push,version,synth,icons,diag', '--console-user', '0', + '--synth-args', f"--repeat 1 --cases '{cases}'", + '--icons-args', "--cases 'gen-(home|folder)'", '--poll-ms', '20', '--wait-ms', '60', + '--attempts', '5'], + capture_output=True, text=True, timeout=1500, env=dict(os.environ, PYTHONDONTWRITEBYTECODE='1')) + (out / 'console-run.log').write_text(r.stdout + r.stderr) + finally: + h.process.terminate() + h.process.wait(timeout=30) + check(r.returncode == 0, f'run-icon-pixels exit {r.returncode}: {r.stdout[-300:]} {r.stderr[-300:]}') + rec = json.loads((out / 'console-run/run.json').read_text()) if (out / 'console-run/run.json').is_file() else {} + check(rec.get('status') == 'DONE' and rec.get('synth_rc') == 0 and rec.get('version_rc') == 0, + f'console run: status {rec.get("status")} {rec.get("error")} synth rc {rec.get("synth_rc")}') + got = (out / 'console-run/synth.txt').read_text() if (out / 'console-run/synth.txt').is_file() else '' + # Identical but for the elapsed-time field. + same = lambda text: [re.sub(r' ms=\d+$', '', body(l)) for l in kind_lines(text, ('C ', 'K '))] + want = same(direct.stdout) + check(same(got) == want and len(want) == 8, + f'fetched C/K lines differ from a direct run ({len(same(got))} vs {len(want)} lines)') + icons = (out / 'console-run/icons-gen.txt').read_text() if (out / 'console-run/icons-gen.txt').is_file() else '' + check(len(kind_lines(icons, ('I ',))) == 2 and all(' st=ok ' in l for l in kind_lines(icons, ('I ',))), + f'icons step: {kind_lines(icons, ("I ",))}') + check('skipped' in str(rec.get('icons_ws')) and 'diag_reasons' in rec and 'diag_unified' in rec, + f'icons-ws skipped offline and the diag step ran: {rec.get("icons_ws")}') + return dict(push_seconds=rec.get('push_seconds'), synth=rec.get('synth.txt'), chunks=(rec.get('push') or {}).get('chunks')) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('--output', type=Path, required=True) + ap.add_argument('--bundle', type=Path, help='an ADRENO_SETTINGS_ICONS AdrenoMetal.bundle with its compiler runtime') + ap.add_argument('--skip-console', action='store_true') + ap.add_argument('--noise-rate', type=int, default=40) + a = ap.parse_args() + out = a.output.resolve() + out.mkdir(parents=True, exist_ok=False) + if a.bundle: + a.bundle = a.bundle.resolve() + r = subprocess.run([sys.executable, str(ROOT / 'tools/build-icon-pixels.py'), '--output', str(out / 'tool')], + capture_output=True, text=True) + if not check(r.returncode == 0, f'build: {r.stdout[-300:]} {r.stderr[-300:]}'): + return 1 + tool = out / 'tool/icon-pixels' + runs = [] + for n in range(2): + r = subprocess.run([str(tool), 'synth'] + (['--host-caps', str(a.bundle)] if a.bundle else []), + capture_output=True, text=True, timeout=600) + (out / f'host-synth-{n}.txt').write_text(r.stdout) + check(r.returncode == 0, f'host synth run {n} exit {r.returncode}: {r.stderr[-300:]}') + runs.append(r.stdout) + pixels = lambda text: [re.sub(r' ms=\d+$', '', body(l)) for l in kind_lines(text, ('C ',))] + check(pixels(runs[0]) and pixels(runs[0]) == pixels(runs[1]), 'two runs on this Mac\'s GPU must print the same pixels') + rc = subprocess.run([sys.executable, str(ROOT / 'tools/compare-icon-pixels.py'), str(REFERENCE), str(out / 'host-synth-0.txt'), + '--tolerance', '0'], capture_output=True, text=True) + (out / 'reference-check.txt').write_text(rc.stdout) + check(rc.returncode == 0, f'this Mac no longer reproduces {REFERENCE.name} exactly: {rc.stdout.strip().splitlines()[-1:]}') + if a.bundle: + for units, label in (('8', 'cs8'), (None, 'cs1')): + argv = [str(tool), 'synth', '--plugin', str(a.bundle)] + (['--cs-samplers', units] if units else []) + p = subprocess.run(argv, capture_output=True, text=True, timeout=900) + (out / f'plugin-{label}.txt').write_text(p.stdout) + check(p.returncode == 0, f'plug-in synth ({label}) exit {p.returncode}: {p.stderr[-300:]}') + c = subprocess.run([sys.executable, str(ROOT / 'tools/compare-icon-pixels.py'), str(REFERENCE), + str(out / f'plugin-{label}.txt'), '--census-only'], capture_output=True, text=True) + (out / f'plugin-{label}.compare.txt').write_text(c.stdout) + if units: + check(c.returncode == 0, f'plug-in census (8 CS sampler units) differs: {c.stdout.strip().splitlines()[-1:]}') + else: + dropped = re.search(r'dropped=(\d+)', p.stdout) + check(c.returncode == 1 and dropped and int(dropped.group(1)) > 0 and 'plugin-diagnostics' in c.stdout, + 'without AdrenoIBComputeSamplers the multi-input kernels must be dropped and reported') + ref_lines = [l for l in REFERENCE.read_text().splitlines() if l.strip()] + icons = subprocess.run([str(tool), 'icons', '--size', '20'], capture_output=True, text=True, timeout=600) + icon_lines = [l for l in icons.stdout.splitlines() if l.startswith(('I ', 'K gen-', 'K ws-'))] + check(icons.returncode == 0 and len([l for l in icon_lines if l.startswith('I ')]) >= 10, + f'icons on this Mac: exit {icons.returncode}, {len(icon_lines)} lines') + (out / 'faults').mkdir() + fault_injection(out / 'faults', ref_lines, icon_lines) + console = None + if not a.skip_console: + console = console_end_to_end(out, tool, a.noise_rate) + result = dict(checks=checks, failures=failures, console=console, tool=json.loads((out / 'tool/build.json').read_text())) + (out / 'result.json').write_text(json.dumps(result, indent=1) + '\n') + print(f'{"PASS" if not failures else "FAIL"}: {checks} checks ({len(failures)} failed); {out / "result.json"}') + return 0 if not failures else 1 + + +if __name__ == '__main__': + sys.exit(main())