Files
UniXNU/scripts/check-air-fragment-outputs.py
2026-10-19 18:49:31 +08:00

105 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""Qualify unchanged QuartzCore fragment colors, sparse targets and MRT identity."""
import argparse
import ctypes as C
import hashlib
import json
import os
from pathlib import Path
import re
import struct
import subprocess
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--compiler', type=Path, required=True)
p.add_argument('--llvm-bindir', type=Path, required=True)
a=p.parse_args()
ROOT=Path(__file__).resolve().parents[1]
OUT=ROOT/'build/air-nir/fragments';OUT.mkdir(parents=True,exist_ok=True)
env=dict(os.environ,IR3_SHADER_DEBUG='asmroundtrip')
def run(cmd, status=0):
r=subprocess.run([str(v) for v in cmd],env=env,capture_output=True,text=True,timeout=15)
if r.returncode!=status:raise RuntimeError(f'Expected {status}, got {r.returncode}: {r.stderr[-1200:]}')
return r
def sha(path):return hashlib.sha256(path.read_bytes()).hexdigest()
def evaluate(src,prefix,expected_targets):
# Evaluate exact original/module-copy IR and NIR independently; no rasterizer.
interface=prefix.with_suffix('.interface.json');abi=prefix.with_suffix('.ir3.json')
case=prefix.with_suffix('.cases');case.write_bytes(bytes(4+16*4*4))
r=run([a.compiler,src,'0x43050a01',prefix,'--compile','--test-resources',case])
if not json.loads(r.stdout)['asm_roundtrip']:raise RuntimeError('Missing actual encoding roundtrip')
schema=json.loads(interface.read_text());device=json.loads(abi.read_text())
if schema['stage']!='fragment' or schema['inputs'] or device['stage']!='fragment' or device['writes_position']:
raise RuntimeError('Actual fragment stage/interface differs')
if [(v['target'],v['components']) for v in schema['outputs']]!=expected_targets:
raise RuntimeError('Logical output identity/width differs')
# NIR store location must follow the source target, not the dense return index.
nir=prefix.with_suffix('.nir').read_text();asm=prefix.with_suffix('.asm').read_text()
for target,components in expected_targets:
if f'FRAG_RESULT_DATA{target}' not in asm or f'VARYING_SLOT_POS' in nir:
raise RuntimeError('Actual backend target mapping differs')
actual={v['slot']:v for v in device['outputs']}
if set(actual)!={4+target for target,_ in expected_targets}:
raise RuntimeError('IR3 allocated output targets differ from source')
for target,components in expected_targets:
# IR3 allocated mask may contain only requested components.
if actual[4+target]['register_id']==252:raise RuntimeError('No actual color output register')
oracle=prefix.with_suffix('.oracle.bc')
run([a.compiler,src,'0x43050a01',oracle,'--test-resource-oracle'])
dylib=prefix.with_suffix('.dylib')
run([a.llvm_bindir/'clang','-O2','-dynamiclib',oracle,'-o',dylib])
lib=C.CDLL(str(dylib));fn=lib.air_test_resource_oracle
fn.argtypes=[C.c_void_p,C.c_void_p,C.c_uint32,C.POINTER(C.c_float)]
expected=(C.c_float*(len(expected_targets)*4))();fn(None,None,0,expected)
if prefix.with_suffix('.eval').read_bytes()!=bytes(expected):raise RuntimeError('Original IR versus NIR bitwise color mismatch')
return dict(binary_sha256=sha(prefix.with_suffix('.bin')),abi_sha256=sha(abi),binary_bytes=device['binary_bytes'],
instructions=device['main_instructions'],targets=expected_targets,original_ir_nir_match=True,gpu_executed=False)
audit=json.loads((ROOT/'build/gpu-analysis/air/target-inputs.json').read_text())
originals=[]
for index,targets in [(69,[(2,1)]),(73,[(0,4),(1,4)]),(157,[(2,1)])]:
src=ROOT/f'build/gpu-analysis/air/QuartzCore/module-{index}.bc'
entry=next(v for v in audit['modules'] if v['library']=='QuartzCore' and v['index']==index)
if sha(src)!=entry['bitcode_sha256']:raise RuntimeError('Supplied original input changed')
result=evaluate(src,OUT/f'QuartzCore-{index}',targets)
originals.append(dict(library='QuartzCore',index=index,source_sha256=sha(src),**result))
# Authored variants preserve target triple/version/options and mutate only ignored
# copies. They prevent all-zero original clears from hiding channel/target swaps.
scalar=(ROOT/'build/gpu-analysis/air/QuartzCore/module-69.ll').read_text()
mrt=(ROOT/'build/gpu-analysis/air/QuartzCore/module-73.ll').read_text()
synthetic=[]
for target in range(8):
text=scalar.replace('ret <{ float }> zeroinitializer','ret <{ float }> <{ float -1.250000e+00 }>')
text=text.replace('!"air.render_target", i32 2, i32 0','!"air.render_target", i32 '+str(target)+', i32 0')
ll=OUT/f'target-{target}.ll';ll.write_text(text);bc=ll.with_suffix('.bc')
run([a.llvm_bindir/'llvm-as',ll,'-o',bc]);synthetic.append(evaluate(bc,OUT/f'target-{target}',[(target,1)]))
text=mrt.replace('ret <{ <4 x float>, <4 x float> }> zeroinitializer',
'ret <{ <4 x float>, <4 x float> }> <{ <4 x float> <float 1.000000e+00, float 2.000000e+00, float 3.000000e+00, float 4.000000e+00>, <4 x float> <float -4.000000e+00, float -3.000000e+00, float -2.000000e+00, float -1.000000e+00> }>')
ll=OUT/'mrt-distinct.ll';ll.write_text(text);bc=ll.with_suffix('.bc');run([a.llvm_bindir/'llvm-as',ll,'-o',bc])
synthetic.append(evaluate(bc,OUT/'mrt-distinct',[(0,4),(1,4)]))
mutations={
'target-out-of-range':scalar.replace('!"air.render_target", i32 2, i32 0','!"air.render_target", i32 8, i32 0'),
'dual-source':scalar.replace('!"air.render_target", i32 2, i32 0','!"air.render_target", i32 2, i32 1'),
'duplicate-target':mrt.replace('!"air.render_target", i32 1, i32 0','!"air.render_target", i32 0, i32 0'),
'depth-kind':scalar.replace('!"air.render_target"','!"air.depth"'),
'undefined-scalar':scalar.replace('ret <{ float }> zeroinitializer','ret <{ float }> poison'),
'undefined-vector':mrt.replace('ret <{ <4 x float>, <4 x float> }> zeroinitializer','ret <{ <4 x float>, <4 x float> }> poison'),
'half-output':mrt.replace('float','half'),
'mixed-stage':scalar.replace('!air.fragment = !{!15}','!air.fragment = !{!15}\n!air.vertex = !{!15}'),
'missing-stage':scalar.replace('!air.fragment = !{!15}',''),
'wrong-stage':scalar.replace('!air.fragment = !{!15}','!air.vertex = !{!15}'),
}
refusals=[]
for name,text in mutations.items():
if text in (scalar,mrt):raise RuntimeError('No-op negative mutation')
ll=OUT/(name+'.ll');ll.write_text(text);bc=ll.with_suffix('.bc');run([a.llvm_bindir/'llvm-as',ll,'-o',bc])
r=run([a.compiler,bc,'0x43050a01',OUT/name,'--compile'],status=4)
if not r.stderr.startswith('AIR refused:'):raise RuntimeError('Expected explicit frontend refusal')
refusals.append(dict(name=name,reason=r.stderr.strip()))
(OUT/'report.json').write_text(json.dumps(dict(originals=originals,authored_color_controls=synthetic,
refusal_controls=refusals,original_inventory_unchanged=True,actual_rasterization=False,
fragment_interpolation=False,texture_sampling=False,metal_pipeline_linked=False),indent=2)+'\n')
print('PASS: three original scalar/MRT fragment shaders, all eight sparse targets and distinct MRT channels, actual roundtrip/IR3 targets and original-IR/NIR bits; ten refusals')