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

124 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Qualify original QuartzCore attribute/multi-buffer compiler paths offline."""
import argparse
import ctypes
import hashlib
import json
import os
from pathlib import Path
import random
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/resources'
OUT.mkdir(parents=True, exist_ok=True)
env = {k: v for k, v in os.environ.items() if not k.startswith(('IR3_', 'FD_MESA_', 'NIR_', 'MESA_SHADER_'))}
env['IR3_SHADER_DEBUG'] = 'asmroundtrip'
audit = json.loads((ROOT / 'build/gpu-analysis/air/target-inputs.json').read_text())
modules = {(m['library'], m['index']): m for m in audit['modules']}
def run(cmd, code=0):
r = subprocess.run([str(x) for x in cmd], env=env, capture_output=True, text=True, timeout=20)
if r.returncode != code:
raise RuntimeError(f'Status {r.returncode}: {r.stderr[-2500:]}')
return r.stdout
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
reports = []
for index in (7, 8, 9, 10, 11, 98):
src = ROOT / f'build/gpu-analysis/air/QuartzCore/module-{index}.bc'
if sha(src) != modules['QuartzCore', index]['bitcode_sha256']:
raise RuntimeError('Original resource shader input differs')
prefix = OUT / f'QuartzCore-{index}'
run([a.compiler, src, '0x43050a01', prefix, '--compile'])
interface = json.loads(prefix.with_suffix('.interface.json').read_text())
attributes = [i for i in interface['inputs'] if i['kind'] == 'air.vertex_input']
buffers = [i for i in interface['inputs'] if i['kind'] == 'air.buffer']
wanted_attrs = [(0,4), (1,2), (2,2)] if index in (11,98) else [(0,4),(1,2),(3,4)]
wanted_buffers = [(2,64),(3,16),(5,8)] if index == 8 else [(2,64),(3,32 if index in (11,98) else 16)]
if [(i['index'],i['components']) for i in attributes] != wanted_attrs or [(i['index'],i['minimum_bytes']) for i in buffers] != wanted_buffers:
raise RuntimeError('Resource interface differs from independently audited original metadata/accesses')
if interface['runtime_resource_layout_qualified']:
raise RuntimeError('Host schema cannot qualify runtime resource layouts')
rng = random.Random(20261020 + index)
cases = []
for case in range(4096):
attrs = struct.pack('<64f', *(rng.uniform(-4,4) + slot * 0.25 for slot in range(64)))
blocks = [struct.pack('<' + 'f'*(i['minimum_bytes']//4), *(rng.uniform(0.25,2) for _ in range(i['minimum_bytes']//4))) for i in buffers]
cases.append((case, attrs, blocks))
inputs = prefix.with_suffix('.cases')
inputs.write_bytes(b''.join(struct.pack('<I',v) + attrs + b''.join(blocks) for v,attrs,blocks in cases))
meta = json.loads(run([a.compiler,src,'0x43050a01',prefix,'--compile','--test-resources',inputs]))
oracle_bc, oracle_dylib = Path(str(prefix)+'.oracle.bc'), Path(str(prefix)+'.dylib')
run([a.compiler,src,'0x43050a01',oracle_bc,'--test-resource-oracle'])
run([a.llvm_bindir/'clang','-dynamiclib','-O0','-ffp-contract=off',oracle_bc,'-o',oracle_dylib])
lib = ctypes.CDLL(str(oracle_dylib))
oracle = lib.air_test_resource_oracle
oracle.argtypes = [ctypes.c_void_p,ctypes.c_void_p,ctypes.c_uint32,ctypes.c_void_p]
oracle.restype = None
raw = prefix.with_suffix('.eval').read_bytes()
stride = len(interface['outputs']) * 16
if len(raw) != len(cases)*stride:
raise RuntimeError('Resource numerical output framing')
for case,(vertex,attrs,blocks) in enumerate(cases):
input_attrs = ctypes.create_string_buffer(attrs)
backing = []
pointers = (ctypes.c_void_p * 16)()
for item,data in zip(buffers,blocks):
alignment = item['minimum_alignment']
block = ctypes.create_string_buffer(len(data)+alignment)
address = (ctypes.addressof(block)+alignment-1) & ~(alignment-1)
ctypes.memmove(address,data,len(data))
pointers[item['index']] = address
backing.append(block)
expected = (ctypes.c_uint32 * 64)()
oracle(input_attrs,pointers,vertex,expected)
actual = struct.unpack_from('<'+'I'*(stride//4),raw,case*stride)
for out in interface['outputs']:
for component in range(out['components']):
channel = out['slot']*4+component
if actual[channel] != expected[channel]:
raise RuntimeError(f'Resource arithmetic mismatch: module {index}, case {case}, channel {channel}: {actual[channel]:08x} != {expected[channel]:08x}')
reports.append(dict(library='QuartzCore',index=index,name=interface['function'],input_sha256=sha(src),
numerical_cases=4096,exact_f32_equal=True,binary_bytes=meta['binary_bytes'],
binary_sha256=sha(prefix.with_suffix('.bin')),interface=interface,asm_roundtrip=meta['asm_roundtrip'],gpu_executed=False))
print(f'PASS: QuartzCore {index}: 4096 original-IR/NIR attribute and multi-buffer cases',flush=True)
# Original undefined output stays a refusal; do not fill a varying with zeros.
run([a.compiler,ROOT/'build/gpu-analysis/air/QuartzCore/module-6.bc','0x43050a01',OUT/'undefined-varying','--compile'],code=4)
# Actual incomplete test frames must refuse instead of reading zero-filled data.
short = OUT/'short.cases';short.write_bytes(b'\0'*8)
run([a.compiler,src,'0x43050a01',OUT/'short','--compile','--test-resources',short],code=4)
original = (ROOT/'build/gpu-analysis/air/QuartzCore/module-9.ll').read_text()
axis_source = (ROOT/'build/gpu-analysis/air/QuartzCore/module-8.ll').read_text()
mutations = {
'attribute-overlap': original.replace('!"air.location_index", i32 1, i32 1','!"air.location_index", i32 0, i32 1',1),
'attribute-too-large': original.replace('!"air.location_index", i32 1, i32 1','!"air.location_index", i32 16, i32 1',1),
'buffer-write': original.replace('!"air.read"','!"air.write"'),
'wrong-address-space': original.replace('!"air.address_space", i32 2','!"air.address_space", i32 1'),
'explicit-buffer-too-short': axis_source.replace('!"air.buffer_size", i32 8','!"air.buffer_size", i32 4'),
'missing-buffer-type': original.replace('!"air.arg_type_size"','!"air.unknown_type_size"'),
'unused-contradiction': original.replace('!"air.vertex_input",','!"air.vertex_input", !"air.arg_unused",'),
'buffer-binding-overlap': original.replace('!"air.buffer_size", i32 16, !"air.location_index", i32 3','!"air.buffer_size", i32 16, !"air.location_index", i32 2'),
}
for name,text in mutations.items():
if text in (original,axis_source): raise RuntimeError('Resource mutation did not change input')
ll,bc,prefix=OUT/(name+'.ll'),OUT/(name+'.bc'),OUT/name
ll.write_text(text); run([a.llvm_bindir/'llvm-as',ll,'-o',bc])
prefix.with_suffix('.bin').unlink(missing_ok=True)
run([a.compiler,bc,'0x43050a01',prefix,'--compile'],code=4)
if prefix.with_suffix('.bin').exists(): raise RuntimeError('Resource refusal exported code')
(OUT/'report.json').write_text(json.dumps(dict(originals=reports,undefined_output_refused=True,
truncated_resource_case_refused=True,metadata_refusals=list(mutations),
oracle_policy='Original IR host copy with fast rewrite permissions cleared to select the frontend separate-rounding evaluation',
generated_isa_executed=False),indent=2)+'\n')