Files
d4r/vulkan/ptx2glsl.py
ayo 25493b3198 feat: add d4r inference engines and Windows parity tooling
Add native K/M/RR inference engines, HIP kernels, replay and validation
harnesses, and Windows DLSS setup and runtime support. Include GPU
profiling tools, build and packaging scripts, platform integration
patches, and implementation documentation.
2026-10-16 08:54:39 -04:00

1001 lines
47 KiB
Python

#!/usr/bin/env python3
"""Translate one NVIDIA PTX texture kernel into a Vulkan GLSL compute shader.
usage: ptx2glsl.py MODULE.ptx KERNEL [--tex OFF:linear|nearest ...] > KERNEL.comp
Every PTX instruction becomes the GLSL expression of the same operation that kernels/native/ptx2hip.py
emits for HIP, so the shader computes what the translated kernel computes. Registers become locals and
basic blocks become guarded sections of a program-counter loop (GLSL has no goto). The 8-byte handles in
the parameter block become bindings: textures by their parameter offset (T<off>), surfaces (S<off>);
pointers stay 64-bit addresses and are read through buffer references.
"""
import re
import sys
REG_RE = re.compile(r'%([a-zA-Z]+)(\d+)$')
GT = {'f': 'float', 'r': 'uint', 'rd': 'uint64_t', 'rs': 'uint', 'p': 'bool'}
SREG = {'%tid.x': 'gl_LocalInvocationID.x', '%tid.y': 'gl_LocalInvocationID.y', '%tid.z': 'gl_LocalInvocationID.z',
'%ctaid.x': 'gl_WorkGroupID.x', '%ctaid.y': 'gl_WorkGroupID.y', '%ctaid.z': 'gl_WorkGroupID.z',
'%ntid.x': 'gl_WorkGroupSize.x', '%ntid.y': 'gl_WorkGroupSize.y',
'%nctaid.x': 'gl_NumWorkGroups.x', '%nctaid.y': 'gl_NumWorkGroups.y', '%laneid': 'gl_SubgroupInvocationID'}
class Err(Exception):
pass
def tokenize(body):
out, i, n = [], 0, len(body)
while i < n:
c = body[i]
if c.isspace():
i += 1; continue
if c == '{':
out.append(('open', None)); i += 1; continue
if c == '}':
out.append(('close', None)); i += 1; continue
m = re.match(r'(\$?[A-Za-z_][\w$]*):', body[i:])
if m:
out.append(('label', m.group(1))); i += m.end(); continue
j = body.index(';', i)
out.append(('stmt', ' '.join(body[i:j].split())))
i = j + 1
return out
def split_operands(s):
ops, depth, cur = [], 0, ''
for ch in s:
if ch in '{[':
depth += 1
elif ch in '}]':
depth -= 1
if ch == ',' and depth == 0:
ops.append(cur.strip()); cur = ''
else:
cur += ch
if cur.strip():
ops.append(cur.strip())
return ops
class T:
def __init__(self, ptx, kernel):
text = re.sub(r'//[^\n]*', '', ptx)
m = re.search(r'\.entry\s+' + re.escape(kernel) + r'\s*\((.*?)\)', text, re.S)
if not m:
raise Err('kernel not found')
pm = re.search(r'\.param\s+\.align\s+(\d+)\s+\.b8\s+(\w+)\[(\d+)\]', m.group(1))
self.kernel, self.pname, self.psize = kernel, pm.group(2), int(pm.group(3))
rest = text[m.end():]
mt = re.search(r'\.maxntid\s+(\d+)\s*,\s*(\d+)\s*,\s*(\d+)', rest[:rest.index('{')])
self.maxntid = [int(x) for x in mt.groups()] if mt else [256, 1, 1]
start = rest.index('{'); depth, k = 0, start
while True:
if rest[k] == '{':
depth += 1
elif rest[k] == '}':
depth -= 1
if depth == 0:
break
k += 1
self.toks = tokenize(rest[start + 1:k])
self.regs = {}
self.shared = {} # name -> byte base
self.shared_bytes = 0
self.scopes = [{}]
self.scope_id = 0
self.blocks = [[]] # lists of lines
self.labels = {} # label -> block index
self.textures, self.surfaces = set(), {}
self.dyn_tex = False
self.fast = False # --fast: no 'precise', the compiler may contract and reassociate
self.collect = False # first pass: gather how every 16/32-bit register is used
self.ctx, self.links, self.width, self.half = {}, [], {}, set()
self.exp_lut = False
self.prescan()
# ---------------------------------------------------------------- static facts about registers
def prescan(self):
defs = {}
for kind, s in self.toks:
if kind != 'stmt' or s.startswith('.'):
continue
s = re.sub(r'^@!?%p\d+\s+', '', s)
parts = s.split(None, 1)
if len(parts) < 2:
continue
a = split_operands(parts[1])
for d in re.findall(r'%rd\d+', a[0]) if not a[0].startswith('[') else []:
defs.setdefault(d, []).append((parts[0], a))
self.pbase = {} # reg -> constant offset from the parameter block
self.handle = {} # reg -> parameter offset its 8-byte value was loaded from
changed = True
while changed:
changed = False
for r, ds in defs.items():
if len(ds) != 1:
continue
op, a = ds[0]
if r not in self.pbase:
v = None
if op.startswith('mov') and a[1] == self.pname:
v = 0
elif op in ('add.s64', 'add.u64') and a[1] in self.pbase and re.match(r'-?\d+$', a[2]):
v = self.pbase[a[1]] + int(a[2])
if v is not None:
self.pbase[r] = v; changed = True
if r not in self.handle:
v = None
if op == 'ld.param.u64':
v = self.param_off(a[1])
elif op.startswith('mov') and a[1] in self.handle:
v = self.handle[a[1]]
if v is not None:
self.handle[r] = v; changed = True
def param_off(self, tok):
m = re.match(r'\[\s*([^\]+]+?)\s*(?:\+\s*(-?\d+))?\s*\]$', tok)
base, off = m.group(1), int(m.group(2) or 0)
if base == self.pname:
return off
if base in self.pbase:
return self.pbase[base] + off
return None
# ---------------------------------------------------------------- operands
def reg(self, tok):
tok = tok.strip()
for sc in reversed(self.scopes):
if tok in sc:
return sc[tok]
m = REG_RE.match(tok)
if not m or m.group(1) not in GT:
raise Err('not a register: ' + tok)
name = m.group(1) + m.group(2)
if m.group(1) in ('r', 'rs'):
self.width[name] = 32 if m.group(1) == 'r' else 16
self.regs[name] = self.htype(name) or GT[m.group(1)]
return name
def htype(self, name):
return ('f16vec2' if self.width[name] == 32 else 'float16_t') if name in self.half else None
def note(self, name, cls):
if self.collect:
self.ctx.setdefault(name, set()).add(cls)
def hk(self, name):
return 'f16x2' if self.width.get(name) == 32 else 'f16'
def is_reg(self, tok):
return tok.startswith('%') and tok not in SREG or any(tok in sc for sc in self.scopes)
def val(self, tok, t):
tok = tok.strip()
if tok in SREG:
return f'uint({SREG[tok]})' if t != 's32' else f'int({SREG[tok]})'
if tok in ('%SP', '%SPL'):
return 'uint64_t(0ul)' # the local depot is its own array; addresses in it start at 0
if self.is_reg(tok):
n = self.reg(tok); g = self.regs[n]
self.note(n, 'h' if t in ('f16', 'f16x2') else 'u')
if g in ('float16_t', 'f16vec2'):
if t in ('f16', 'f16x2'):
return n
bits = f'HU({n})' if g == 'float16_t' else f'H2U({n})'
if t == 's16':
return f's16({bits})'
if t == 's32':
return f'int({bits})'
if t in ('u64', 'b64'):
return f'uint64_t({bits})'
return bits
if t == 'f32':
return n if g == 'float' else f'uintBitsToFloat({n})'
if t in ('u32', 'b32'):
return f'floatBitsToUint({n})' if g == 'float' else (f'uint({n})' if g == 'uint64_t' else n)
if t == 's32':
return f'floatBitsToInt({n})' if g == 'float' else f'int({n})'
if t in ('u64', 'b64'):
return n if g == 'uint64_t' else f'uint64_t({n})'
if t == 's64':
return f'int64_t({n})'
if t in ('u16', 'b16'):
return n
if t == 's16':
return f's16({n})'
if t == 'f16':
return f'H({n})'
if t == 'f16x2':
return f'H2({n})'
if t == 'pred':
return n
raise Err('type ' + t)
if tok in self.shared:
return f'{self.shared[tok]}u'
if re.match(r'0[fF][0-9A-Fa-f]{8}$', tok):
bits = int(tok[2:], 16)
return f'uintBitsToFloat(0x{bits:08X}u)' if t == 'f32' else f'0x{bits:08X}u'
if re.match(r'-?\d+$', tok) or re.match(r'0x[0-9a-fA-F]+$', tok):
v = int(tok, 0)
if t == 'f32':
raise Err('int immediate as float')
if t in ('u64', 'b64'):
return f'uint64_t({v & 0xffffffffffffffff}ul)'
if t == 's64':
return f'int64_t({v}l)'
if t in ('u16', 'b16'):
return f'{v & 0xffff}u'
if t == 's16':
v &= 0xffff
return f'({v - 0x10000 if v >= 0x8000 else v})'
if t == 'f16':
return f'H({v & 0xffff}u)'
if t == 'f16x2':
return f'H2({v & 0xffffffff}u)'
if t == 's32':
v &= 0xffffffff
return f'int(0x{v:08X}u)' if v >= 1 << 31 else f'{v}'
return f'{v & 0xffffffff}u'
raise Err('operand ' + tok)
def A(self, dst, expr, st):
n = self.reg(dst); g = self.regs[n]
self.note(n, 'h' if st in ('f16', 'f16x2') else 'u')
if g == 'bool' and st in ('u32', 'u16', 'u64'):
self.E(f'{n} = ({expr}) != 0u;'); return # mov.pred with an immediate
if g in ('float16_t', 'f16vec2'):
if st in ('f16', 'f16x2'):
self.E(f'{n} = {expr};')
else:
self.E(f'{n} = {"H" if g == "float16_t" else "H2"}(uint({expr}));')
return
if st == 'float':
e = expr if g == 'float' else f'floatBitsToUint({expr})'
elif st == 'u32':
e = f'uintBitsToFloat({expr})' if g == 'float' else (f'uint64_t({expr})' if g == 'uint64_t' else f'uint({expr})')
elif st == 'u64':
e = f'uint64_t({expr})'
elif st == 'u16':
e = f'(uint({expr}) & 0xffffu)'
elif st == 'f16':
e = f'HU({expr})'
elif st == 'f16x2':
e = f'H2U({expr})'
elif st == 'bool':
e = f'({expr})'
else:
raise Err('src type')
self.E(f'{n} = {e};')
def E(self, s):
self.blocks[-1].append(s)
# ---------------------------------------------------------------- driver
def translate(self):
for kind, text in self.toks:
if kind == 'open':
self.scopes.append({}); continue
if kind == 'close':
self.scopes.pop(); continue
if kind == 'label':
self.blocks.append([])
self.labels[text] = len(self.blocks) - 1
continue
self.stmt(text)
def stmt(self, s):
if s.startswith('.reg'):
m = re.match(r'\.reg\s*\.(\w+)\s*(.*)$', s)
ty, names = m.groups()
if '<' in names:
return
self.scope_id += 1
for nm in names.split(','):
nm = nm.strip()
c = f'l{self.scope_id}_' + re.sub(r'\W', '_', nm)
self.scopes[-1][nm] = c
if ty in ('f16', 'b16', 'f16x2', 'b32', 'u32', 's32'):
self.width[c] = 16 if ty in ('f16', 'b16') else 32
self.regs[c] = self.htype(c) if c in self.half else {'f16': 'uint', 'b16': 'uint', 'f32': 'float', 'b32': 'uint', 'pred': 'bool', 'f16x2': 'uint',
'u32': 'uint', 's32': 'uint', 'b64': 'uint64_t', 'u64': 'uint64_t'}[ty]
return
if s.startswith('.local'):
m = re.match(r'\.local\s+\.align\s+(\d+)\s+\.b8\s+(\w+)\[(\d+)\]', s)
self.local_bytes = max(getattr(self, 'local_bytes', 0), int(m.group(3)))
return
if re.match(r'mov\.u64 %SPL?, __local_depot', s):
return
if s.startswith('.shared'):
m = re.match(r'\.shared\s+\.align\s+(\d+)\s+\.b8\s+(\w+)\[(\d+)\]', s)
self.shared[m.group(2)] = self.shared_bytes
self.shared_bytes += (int(m.group(3)) + 3) & ~3
return
if s.startswith('.'):
return
pred = None
m = re.match(r'@(!?)(%p\d+)\s+(.*)$', s)
if m:
pred = ('!' if m.group(1) else '') + self.reg(m.group(2)); s = m.group(3)
parts = s.split(None, 1)
op = parts[0]
a = split_operands(parts[1]) if len(parts) > 1 else []
if op.startswith('bra'):
tgt = ('L', a[0])
if pred is not None:
self.E(('BR', pred, tgt)) # conditional: falls through into a new block
else:
self.E(('BR', None, tgt))
self.blocks.append([])
return
if op == 'ret':
self.E(('BR', pred, ('END', None)))
self.blocks.append([])
return
if pred is not None:
self.E(f'if ({pred}) {{')
self.op(op, a)
self.E('}')
return
self.op(op, a)
def cmp(self, mods, isf):
table = {'lt': '{0} < {1}', 'gt': '{0} > {1}', 'le': '{0} <= {1}', 'ge': '{0} >= {1}', 'eq': '{0} == {1}', 'ne': '{0} != {1}',
'ltu': '!({0} >= {1})', 'gtu': '!({0} <= {1})', 'leu': '!({0} > {1})', 'geu': '!({0} < {1})',
'equ': '!({0} != {1})', 'neu': '!({0} == {1})'}
for k in table:
if k in mods:
if isf and k == 'ne':
return '({0} < {1} || {0} > {1})'
if isf and k == 'equ':
return '!({0} < {1} || {0} > {1})'
return table[k]
raise Err('compare')
def texref(self, tok):
m = re.match(r'\[\s*(%rd\d+)\s*,\s*\{([^}]*)\}\s*\]', tok)
return m.group(1), [x.strip() for x in m.group(2).split(',')]
def op(self, op, a):
o = op.split('.'); base = o[0]; mods = set(o[1:])
E, A, V = self.E, self.A, self.val
ft = next((t for t in ('f32', 'f16x2', 'f16') if t in mods), None)
if base == 'bar':
E('memoryBarrierShared(); barrier();'); return
if base == 'mov':
dst, src = a
if dst.startswith('{'):
n = [x.strip() for x in dst[1:-1].split(',')]
sr = self.reg(src) if self.is_reg(src) else None
if self.collect and sr:
for x in [sr] + [self.reg(y) for y in n]:
self.ctx.setdefault(x, set()).add('h')
return
if sr and (sr in self.half or any(self.reg(y) in self.half for y in n)):
A(n[0], f'{V(src, "f16x2")}.x', 'f16'); A(n[1], f'{V(src, "f16x2")}.y', 'f16'); return
A(n[0], f'{V(src, "u32")} & 0xffffu', 'u16'); A(n[1], f'{V(src, "u32")} >> 16', 'u16'); return
if src.startswith('{'):
n = [x.strip() for x in src[1:-1].split(',')]
dr = self.reg(dst)
if self.collect:
for x in [dr] + [self.reg(y) for y in n if self.is_reg(y)]:
self.ctx.setdefault(x, set()).add('h')
return
if dr in self.half:
A(dst, f'f16vec2({V(n[0], "f16")}, {V(n[1], "f16")})', 'f16x2'); return
A(dst, f'({V(n[0], "u16")} & 0xffffu) | ({V(n[1], "u16")} << 16)', 'u32'); return
if self.is_reg(dst) and self.reg(dst) in self.width and not (mods & {'f32', 'u64', 'b64', 's64'}):
dr = self.reg(dst)
srcreg = self.reg(src) if self.is_reg(src) and src not in SREG else None
isimm = bool(re.match(r'-?\d+$', src) or re.match(r'0x[0-9a-fA-F]+$', src))
if self.collect and srcreg and srcreg in self.width:
self.links.append((dr, srcreg)); return
if self.collect and isimm:
return
if dr in self.half and (srcreg or isimm):
A(dst, V(src, self.hk(dr)), self.hk(dr)); return
if src == self.pname:
A(dst, '0ul', 'u64'); return
if 'f32' in mods:
A(dst, V(src, 'f32'), 'float'); return
if mods & {'u64', 'b64', 's64'}:
A(dst, V(src, 'u64'), 'u64'); return
if mods & {'u16', 'b16', 's16'}:
A(dst, V(src, 'u16'), 'u16'); return
A(dst, V(src, 'u32'), 'u32'); return
# ------------------------------------------------ f32
if ft == 'f32' and base in ('add', 'sub', 'mul'):
if 'rz' in mods:
A(a[0], f'round_half_away({V(a[1], "f32")})', 'float'); return # the roundf idiom: + copysign(0.5), then trunc
A(a[0], f'{V(a[1], "f32")} {"+-*"[["add", "sub", "mul"].index(base)]} {V(a[2], "f32")}', 'float'); return
if ft == 'f32' and base == 'fma':
A(a[0], f'fma({V(a[1], "f32")}, {V(a[2], "f32")}, {V(a[3], "f32")})', 'float'); return
if ft == 'f32' and base in ('max', 'min'):
A(a[0], f'{base}({V(a[1], "f32")}, {V(a[2], "f32")})', 'float'); return
if ft == 'f32' and base == 'neg':
A(a[0], f'-{V(a[1], "f32")}', 'float'); return
if ft == 'f32' and base == 'abs':
A(a[0], f'abs({V(a[1], "f32")})', 'float'); return
if ft == 'f32' and base in ('rcp', 'ex2', 'lg2', 'rsqrt', 'sqrt'):
fn = {'rcp': 'rcp_a', 'ex2': 'exp2', 'lg2': 'log2', 'rsqrt': 'inversesqrt', 'sqrt': 'sqrt'}[base]
A(a[0], f'{fn}({V(a[1], "f32")})', 'float'); return
if ft == 'f32' and base == 'div':
A(a[0], f'{V(a[1], "f32")} * rcp_a({V(a[2], "f32")})', 'float'); return
# ------------------------------------------------ f16
if ft == 'f16x2' and base in ('add', 'sub', 'mul', 'fma'):
x = [V(t, 'f16x2') for t in a[1:]]
e = f'fma({x[0]}, {x[1]}, {x[2]})' if base == 'fma' else f'({x[0]} {"+-*"[["add", "sub", "mul"].index(base)]} {x[1]})'
if 'sat' in mods:
e = f'min(max({e}, f16vec2(0.0hf)), f16vec2(1.0hf))'
A(a[0], e, 'f16x2'); return
if ft == 'f16x2' and base == 'abs':
A(a[0], f'abs({V(a[1], "f16x2")})', 'f16x2'); return
if ft == 'f16x2' and base == 'ex2':
if self.exp_lut:
A(a[0], f'ex2_lut({V(a[1], "u32")})', 'u32'); return
A(a[0], f'f16vec2(exp2(vec2({V(a[1], "f16x2")})))', 'f16x2'); return
if ft in ('f16', 'f16x2') and base in ('min', 'max'):
A(a[0], f'{base}({V(a[1], ft)}, {V(a[2], ft)})', ft); return
if ft == 'f16' and base in ('add', 'sub', 'mul', 'fma'):
x = [V(t, 'f16') for t in a[1:]]
e = f'fma({x[0]}, {x[1]}, {x[2]})' if base == 'fma' else f'({x[0]} {"+-*"[["add", "sub", "mul"].index(base)]} {x[1]})'
if 'sat' in mods:
e = f'min(max({e}, 0.0hf), 1.0hf)'
A(a[0], e, 'f16'); return
if base == 'set' and 'f16x2' in mods and 'u32' in mods:
c = self.cmp(mods, True); x, y = V(a[1], 'f16x2'), V(a[2], 'f16x2')
A(a[0], f'(({c.format(x + ".x", y + ".x")}) ? 0xffffu : 0u) | (({c.format(x + ".y", y + ".y")}) ? 0xffff0000u : 0u)', 'u32'); return
# ------------------------------------------------ compares / selects
if base == 'setp':
isf = bool(mods & {'f32', 'f16'})
t = next(t for t in ('f32', 'f16', 's32', 'u32', 'b32', 's16', 'u16', 'b16', 's64', 'u64') if t in mods)
if '|' in a[0]:
raise Err('setp form')
A(a[0], self.cmp(mods, isf).format(V(a[1], t), V(a[2], t)), 'bool'); return
if base == 'selp' and not (mods & {'f32', 'u64', 'b64', 's64'}) and self.reg(a[0]) in self.width:
dr = self.reg(a[0])
srcs = [self.reg(x) for x in a[1:3] if self.is_reg(x) and x not in SREG]
if self.collect:
self.links += [(dr, x) for x in srcs if x in self.width]
V(a[3], 'pred')
if any(x not in self.width for x in srcs):
self.note(dr, 'u')
return
if dr in self.half:
k = self.hk(dr)
A(a[0], f'{V(a[3], "pred")} ? {V(a[1], k)} : {V(a[2], k)}', k); return
if base == 'selp':
t = 'f32' if 'f32' in mods else 'u16' if mods & {'u16', 'b16', 's16'} else 'u64' if mods & {'u64', 'b64', 's64'} else 'u32'
A(a[0], f'{V(a[3], "pred")} ? {V(a[1], t)} : {V(a[2], t)}', {'f32': 'float', 'u16': 'u16', 'u64': 'u64', 'u32': 'u32'}[t]); return
if 'pred' in mods and base in ('and', 'or', 'xor', 'not'):
if base == 'not':
A(a[0], f'!{V(a[1], "pred")}', 'bool'); return
A(a[0], f'{V(a[1], "pred")} {{"and": "&&", "or": "||", "xor": "!="}}'.replace('{"and": "&&", "or": "||", "xor": "!="}', {'and': '&&', 'or': '||', 'xor': '!='}[base]) + f' {V(a[2], "pred")}', 'bool'); return
# ------------------------------------------------ integer
if base in ('and', 'or', 'xor', 'not', 'shl', 'shr', 'add', 'sub', 'mul', 'mad', 'min', 'max', 'div', 'rem', 'neg', 'abs') and ft is None:
w = 64 if mods & {'u64', 's64', 'b64'} else 16 if mods & {'u16', 's16', 'b16'} else 32
signed = bool(mods & {'s32', 's64', 's16'})
ut, st = {64: ('u64', 's64'), 32: ('u32', 's32'), 16: ('u16', 's16')}[w]
if base == 'mul' and 'wide' in mods:
if 's32' in mods:
A(a[0], f'uint64_t(int64_t({V(a[1], "s32")}) * int64_t({V(a[2], "s32")}))', 'u64'); return
if 'u32' in mods:
A(a[0], f'uint64_t({V(a[1], "u32")}) * uint64_t({V(a[2], "u32")})', 'u64'); return
if 'u16' in mods:
A(a[0], f'{V(a[1], "u16")} * {V(a[2], "u16")}', 'u32'); return
raise Err(op)
if base == 'not':
A(a[0], f'~{V(a[1], ut)}', ut); return
if base == 'neg':
A(a[0], f'0u - {V(a[1], ut)}', ut); return
if base == 'shl':
A(a[0], f'{V(a[1], ut)} << ({V(a[2], "u32")} & {w - 1}u)', ut); return
if base == 'shr':
x = V(a[1], st if signed else ut)
A(a[0], f'{x} >> ({V(a[2], "u32")} & {w - 1}u)', ut); return
if base in ('and', 'or', 'xor'):
A(a[0], f'{V(a[1], ut)} {{"and": "&", "or": "|", "xor": "^"}}'.replace('{"and": "&", "or": "|", "xor": "^"}', {'and': '&', 'or': '|', 'xor': '^'}[base]) + f' {V(a[2], ut)}', ut); return
if base in ('add', 'sub'):
A(a[0], f'{V(a[1], ut)} {"+" if base == "add" else "-"} {V(a[2], ut)}', ut); return
if base == 'mul' and 'hi' in mods:
if signed:
A(a[0], f'uint(int((int64_t({V(a[1], "s32")}) * int64_t({V(a[2], "s32")})) >> 32))', 'u32'); return
A(a[0], f'uint((uint64_t({V(a[1], "u32")}) * uint64_t({V(a[2], "u32")})) >> 32)', 'u32'); return
if base == 'mul':
A(a[0], f'{V(a[1], ut)} * {V(a[2], ut)}', ut); return
if base == 'mad':
A(a[0], f'{V(a[1], ut)} * {V(a[2], ut)} + {V(a[3], ut)}', ut); return
if base == 'abs':
A(a[0], f'uint(abs({V(a[1], st)}))', ut); return
if base == 'neg' and len(a) == 2:
A(a[0], f'uint(-{V(a[1], st)})', ut); return
x, y = V(a[1], st if signed else ut), V(a[2], st if signed else ut)
if base in ('min', 'max'):
A(a[0], f'{base}({x}, {y})', ut); return
if base == 'div':
A(a[0], f'{x} / {y}', ut); return
if base == 'rem':
A(a[0], f'{x} % {y}', ut); return
if base == 'bfi':
A(a[0], f'bfi32({V(a[1], "u32")}, {V(a[2], "u32")}, {V(a[3], "u32")}, {V(a[4], "u32")})', 'u32'); return
if base == 'cvt':
return self.cvt(op, o[1:], a)
if base == 'cvta':
A(a[0], V(a[1], 'u64'), 'u64'); return
if base == 'ld':
return self.ld(o[1:], a)
if base == 'st':
return self.st(o[1:], a)
if base in ('tex', 'tld4'):
dst = [x.strip() for x in a[0][1:-1].split(',')]
h, c = self.texref(a[1])
off = self.handle.get(h)
if off is None:
raise Err('texture handle of ' + h + ' is not a single parameter load')
self.textures.add(off)
if base == 'tld4':
E(f'{{ vec4 t_ = textureGather(T{off}, vec2({V(c[0], "f32")}, {V(c[1], "f32")}), 0);')
elif 'base' in mods:
E(f'{{ vec4 t_ = texelFetch(T{off}, ivec2({V(c[0], "s32")}, {V(c[1], "s32")}), 0);')
elif 'level' in mods:
E(f'{{ vec4 t_ = textureLod(T{off}, vec2({V(c[0], "f32")}, {V(c[1], "f32")}), {V(a[2], "f32")});')
else:
raise Err(op)
for i, d in enumerate(dst):
if 'f16' in o:
A(d, f'float16_t(t_[{i}])', 'f16') # the texel as a half float
else:
A(d, f't_[{i}]', 'float')
E('}')
return
if base == 'sust':
h, c = self.texref(a[0])
off = self.handle.get(h)
if off is None:
raise Err('surface handle of ' + h)
v = [x.strip() for x in a[1][1:-1].split(',')]
if op == 'sust.p.2d.v4.b32.zero':
self.surfaces.setdefault(off, 'p'); assert self.surfaces[off] == 'p'
E(f'sust_p(S{off}, {V(c[0], "s32")}, {V(c[1], "s32")}, vec4({", ".join(V(x, "f32") for x in v)}));'); return
if op == 'sust.b.2d.b32.zero':
self.surfaces.setdefault(off, 'b'); assert self.surfaces[off] == 'b'
E(f'sust_b(S{off}, {V(c[0], "s32")}, {V(c[1], "s32")}, {V(v[0], "u32")});'); return
if op == 'sust.b.2d.v2.b16.zero': # reading only: two 16-bit values at a byte-addressed position
self.surfaces.setdefault(off, 'b')
E(f'sust_b2x16(S{off}, {V(c[0], "s32")}, {V(c[1], "s32")}, {V(v[0], "u16")}, {V(v[1], "u16")});'); return
raise Err(op)
if op == 'shfl.sync.bfly.b32':
d = a[0].split('|')
E('{ bool ib_;')
A(d[0], f'shfl_bfly({V(a[1], "u32")}, {V(a[2], "s32")}, {V(a[3], "u32")}, ib_)', 'u32')
if len(d) > 1:
A(d[1], 'ib_', 'bool')
E('}')
return
if op == 'shfl.sync.idx.b32':
d = a[0].split('|')
A(d[0], f'subgroupShuffle({V(a[1], "u32")}, {V(a[2], "u32")} & 31u)', 'u32')
if len(d) > 1:
A(d[1], 'true', 'bool')
return
if op == 'tanh.approx.f16':
A(a[0], f'tanh({V(a[1], "f16")})', 'f16'); return
if op == 'prmt.b32':
A(a[0], f'prmt({V(a[1], "u32")}, {V(a[2], "u32")}, {V(a[3], "u32")})', 'u32'); return
if op.startswith('mma.sync'):
# not executable here: the kernel is translated for reading only (tensor-core fragments are not emulated)
self.E(f'// {op} ' + ' '.join(a))
for d in a[0][1:-1].split(','):
A(d.strip(), '0u', 'u32')
return
if op == 'vote.sync.ballot.b32':
# the warp is the subgroup (32 lanes in thread order), as for the shuffles
A(a[0], f'subgroupBallot({V(a[1], "pred")}).x', 'u32'); return
raise Err('unhandled ' + op)
def cvt(self, op, o, a):
A, V = self.A, self.val
mods = set(o)
types = [t for t in o if re.match(r'^(f|s|u|b)\d+(x2)?$', t)]
types = [t for t in o if re.match(r'^(f|s|u|b)\d+(x2)?$|^e4m3x2$', t)]
dt, st = types[0], types[1]
# FP8 pairs (reading only: the helpers are not defined)
if dt == 'f16x2' and st == 'e4m3x2':
A(a[0], f'e4m3x2_to_f16x2({V(a[1], "u16")})', 'f16x2'); return
if dt == 'e4m3x2' and st == 'f16x2':
A(a[0], f'f16x2_to_e4m3x2({V(a[1], "f16x2")})', 'u16'); return
if dt == 'f32' and st == 'f32':
x = V(a[1], 'f32')
if 'sat' in mods:
A(a[0], f'min(max({x}, 0.0), 1.0)', 'float'); return
fn = 'trunc' if 'rzi' in mods else 'floor' if 'rmi' in mods else 'roundEven' if 'rni' in mods else 'ceil' if 'rpi' in mods else ''
A(a[0], f'{fn}({x})', 'float'); return
if dt == 's32' and st == 'f32':
x = V(a[1], 'f32')
fn = '' if 'rzi' in mods else 'floor' if 'rmi' in mods else 'roundEven'
A(a[0], f'uint(int({fn}({x})))', 'u32'); return
if dt == 'u32' and st == 'f32':
x = V(a[1], 'f32')
fn = 'trunc' if 'rzi' in mods else 'floor' if 'rmi' in mods else 'roundEven'
A(a[0], f'uint(clamp({fn}({x}), 0.0, 4294967040.0))', 'u32'); return # CUDA saturates
if dt == 'f32' and st == 's32':
A(a[0], f'float({V(a[1], "s32")})', 'float'); return
if dt == 'f32' and st == 'u32':
A(a[0], f'float({V(a[1], "u32")})', 'float'); return
if dt == 'f32' and st == 'f16':
A(a[0], f'float({V(a[1], "f16")})', 'float'); return
if dt == 'f16' and st == 'f32':
A(a[0], f'float16_t({V(a[1], "f32")})', 'f16'); return
if dt == 'f16x2' and st == 'f32':
A(a[0], f'f16vec2(float16_t({V(a[2], "f32")}), float16_t({V(a[1], "f32")}))', 'f16x2'); return
if dt == 'f16' and st == 'f16':
fn = 'roundEven' if 'rni' in mods else 'floor'
A(a[0], f'{fn}({V(a[1], "f16")})', 'f16'); return
if dt == 's32' and st == 'f16':
fn = 'roundEven' if 'rni' in mods else 'floor'
A(a[0], f'uint(int(float({fn}({V(a[1], "f16")}))))', 'u32'); return
if dt == 'f16' and st == 's32':
A(a[0], f'float16_t({V(a[1], "s32")})', 'f16'); return
if dt == 's64' and st == 's32':
A(a[0], f'uint64_t(int64_t({V(a[1], "s32")}))', 'u64'); return
if dt == 'u64' and st == 'u32':
A(a[0], f'uint64_t({V(a[1], "u32")})', 'u64'); return
if dt in ('u16', 's16') and st in ('u32', 's32'):
A(a[0], V(a[1], 'u32'), 'u16'); return
if dt in ('u32', 's32') and st in ('u16',):
A(a[0], V(a[1], 'u16'), 'u32'); return
if dt == 's32' and st == 's16':
A(a[0], f'uint({V(a[1], "s16")})', 'u32'); return
raise Err('cvt ' + op)
def addr(self, tok):
m = re.match(r'\[\s*([^\]+]+?)\s*(?:\+\s*(-?\d+))?\s*\]$', tok)
return m.group(1), int(m.group(2) or 0)
def ld(self, o, a):
A, V, E = self.A, self.val, self.E
mods = set(o)
ty = next(t for t in o if re.match(r'^(f|s|u|b)\d+$', t))
vec = next((int(t[1]) for t in o if re.match(r'^v\d$', t)), 1)
size = int(ty[1:]) // 8
dsts = [x.strip() for x in a[0][1:-1].split(',')] if a[0].startswith('{') else [a[0]]
base, off = self.addr(a[1])
for i, d in enumerate(dsts):
if 'param' in mods:
po = self.param_off(a[1])
if po is None:
raise Err('param address ' + a[1])
e = f'PARAM{size * 8}({po + i * size}u)'
elif 'shared' in mods and size == 2 and vec > 1:
# a vector access is aligned to its whole size (PTX ISA): its 16-bit halves have fixed places in words
e = f'(sh[({V(base, "u32")} + {off + i * size}u) >> 2] ' + ('& 0xffffu)' if i % 2 == 0 else '>> 16u)')
elif 'shared' in mods:
e = f'SH{size * 8}({V(base, "u32")} + {off + i * size}u)'
elif 'local' in mods:
e = f'loc[(uint({V(base, "u64")}) + {off + i * size}u) >> 2]'
else:
e = f'G{size * 8}({V(base, "u64")} + uint64_t({off + i * size}l))'
if size == 8:
A(d, e, 'u64')
elif size == 2:
A(d, e, 'u16')
elif size == 1:
A(d, e, 'u16')
else:
A(d, e, 'u32')
def st(self, o, a):
V, E = self.val, self.E
mods = set(o)
ty = next(t for t in o if re.match(r'^(f|s|u|b)\d+$', t))
size = int(ty[1:]) // 8
base, off = self.addr(a[0])
if 'shared' in mods:
# vector stores go word by word; 16-bit pairs share a word (the kernels keep them 4-byte aligned)
vals = [x.strip() for x in a[1][1:-1].split(',')] if a[1].startswith('{') else [a[1]]
if size == 4:
for i, v in enumerate(vals):
E(f'sh[({V(base, "u32")} + {off + 4 * i}u) >> 2] = {V(v, "u32")};')
return
if size == 2 and len(vals) % 2 == 0:
for i in range(0, len(vals), 2):
E(f'sh[({V(base, "u32")} + {off + 2 * i}u) >> 2] = (uint({V(vals[i], "u16")}) & 0xFFFFu) | (uint({V(vals[i + 1], "u16")}) << 16);')
return
if 'local' in mods and size == 4:
vals = [x.strip() for x in a[1][1:-1].split(',')] if a[1].startswith('{') else [a[1]]
for i, v in enumerate(vals):
E(f'loc[(uint({V(base, "u64")}) + {off + 4 * i}u) >> 2] = {V(v, "u32")};')
return
if 'global' in mods and size == 2 and not a[1].startswith('{'):
E(f'W16({V(base, "u64")} + uint64_t({off}l)).v[0] = uint16_t({V(a[1], "u16")});'); return
if 'global' in mods and size == 2 and a[1].startswith('{'):
vals = [x.strip() for x in a[1][1:-1].split(',')]
E(f'{{ W16 w_ = W16({V(base, "u64")} + uint64_t({off}l)); ' + ' '.join(f'w_.v[{i}] = uint16_t({V(v, "u16")});' for i, v in enumerate(vals)) + ' }'); return
if 'global' in mods and size == 4:
E(f'{{ W16 w_ = W16({V(base, "u64")} + uint64_t({off}l)); uint v_ = {V(a[1], "u32")}; w_.v[0] = uint16_t(v_ & 0xffffu); w_.v[1] = uint16_t(v_ >> 16); }}'); return
raise Err('store ' + '.'.join(o))
# ---------------------------------------------------------------- output
def preamble(self, w, tex_filter):
w('#version 460')
# scripts/check_redistributable.py refuses files carrying this line: they are NVIDIA's code.
w(f'// Generated by vulkan/ptx2glsl.py from NVIDIA PTX ({self.kernel}); do not commit or redistribute.')
for e in ('GL_EXT_shader_explicit_arithmetic_types', 'GL_EXT_buffer_reference', 'GL_EXT_buffer_reference2', 'GL_EXT_scalar_block_layout',
'GL_KHR_shader_subgroup_basic', 'GL_KHR_shader_subgroup_shuffle', 'GL_KHR_shader_subgroup_ballot', 'GL_EXT_samplerless_texture_functions'):
w(f'#extension {e} : require')
bx = 16 if self.maxntid[0] == 256 else self.maxntid[0]
by = self.maxntid[0] // bx
w(f'layout(local_size_x = {bx}, local_size_y = {by}) in;')
w(f'layout(set = 0, binding = 0, std430) readonly buffer ParamBlock {{ uint w[{(self.psize + 3) // 4}]; }} param;')
b = 1
for off in sorted(self.textures):
w(f'layout(set = 0, binding = {b}) uniform sampler2D T{off}; // {tex_filter.get(off, "?")}')
b += 1
for off in sorted(self.surfaces):
fmt = 'r16ui' if self.surfaces[off] == 'b' else 'rgba16f'
typ = 'uimage2D' if self.surfaces[off] == 'b' else 'image2D'
w(f'layout(set = 0, binding = {b}, {fmt}) uniform writeonly {typ} S{off};')
b += 1
if self.exp_lut:
w(f'layout(set = 0, binding = {b}, scalar) readonly buffer ExpLut {{ uint16_t e[65536]; }} explut;')
w('uint ex2_lut(uint u) { return uint(explut.e[u & 0xffffu]) | (uint(explut.e[u >> 16]) << 16); }')
w(f'shared uint sh[{max(self.shared_bytes // 4, 1)}];')
def runtime(self):
return RUNTIME
def emit(self, tex_filter):
nb = len(self.blocks)
out = []
w = out.append
self.preamble(w, tex_filter)
w(self.runtime())
w('void main()\n{')
if getattr(self, 'local_bytes', 0):
w(f' uint loc[{(self.local_bytes + 3) // 4}];')
for g in ('float', 'float16_t', 'f16vec2', 'uint', 'uint64_t', 'bool'):
names = sorted(n for n, t in self.regs.items() if t == g)
for i in range(0, len(names), 24):
init = {'float': '0.0', 'float16_t': '0.0hf', 'f16vec2': 'f16vec2(0.0hf)', 'uint': '0u', 'uint64_t': '0ul', 'bool': 'false'}[g]
w(f' {"precise " if (g in ("float", "float16_t", "f16vec2") or getattr(self, "precise_all", False) and g != "bool") and not self.fast else ""}{g} ' + ', '.join(f'{n} = {init}' for n in names[i:i + 24]) + ';')
# control flow graph over the blocks; END is block nb
succ = []
for i, blk in enumerate(self.blocks):
br = blk[-1] if blk and isinstance(blk[-1], tuple) else None
if br is None:
succ.append([min(i + 1, nb)])
else:
tgt = nb if br[2][0] == 'END' else self.labels[br[2][1]]
succ.append([tgt] if br[1] is None else [tgt, i + 1])
# ---- structured emission: if/else arms that rejoin at the branch's immediate post-dominator --------------
END = nb
# reachable nodes, back edges (to a node on the DFS stack)
color, back = {}, set()
st = [(0, iter(succ[0]))]; color[0] = 1
while st:
n, it = st[-1]
for m in it:
if m == END:
continue
if color.get(m) == 1:
back.add((n, m))
elif m not in color:
color[m] = 1; st.append((m, iter(succ[m]))); break
else:
color[n] = 2; st.pop()
nodes = sorted(color)
# each loop header h gets a synthetic "continue" node: back edges go there, and it flows to the loop's exit
headers = sorted({h for _, h in back})
cont = {h: END + 1 + i for i, h in enumerate(headers)}
g = {n: [(cont[m] if (n, m) in back else m) for m in succ[n]] for n in nodes}
g[END] = []
for h in headers:
g[cont[h]] = [END] # provisional, replaced below
def postorder():
rpo, seen = [], set()
def dfs(n):
stack = [(n, iter(g[n]))]; seen.add(n)
while stack:
x, it = stack[-1]
for m in it:
if m not in seen:
seen.add(m); stack.append((m, iter(g[m]))); break
else:
rpo.append(x); stack.pop()
dfs(0)
return rpo
# A loop may leave through its header as well as through its latches. Its body is the set of nodes that
# reach a latch without passing the header; the loop region ends at the nearest node that post-dominates
# every exit of the body (for a loop with one latch exit this is that exit, as before).
preds = {}
for n in nodes:
for m in succ[n]:
preds.setdefault(m, []).append(n)
pdom = {END: {END}}
for n in postorder(): # successors first
if n != END:
pdom[n] = {n} | set.intersection(*(pdom[m] for m in g[n]))
for h in headers:
body, work = {h}, [n for n, m in back if m == h]
while work:
n = work.pop()
if n not in body:
body.add(n); work += preds.get(n, [])
exits = {m for n in body for m in succ[n] if m not in body}
if not exits:
raise Err('loop without an exit')
common = set.intersection(*(pdom[e] for e in exits))
g[cont[h]] = [max(common, key=lambda c: len(pdom[c]))]
rpo = postorder()
topo = rpo[::-1] # entry first
index = {n: i for i, n in enumerate(topo)}
ipdom = {END: END}
def inter(x, y):
while x != y:
while index[x] < index[y]:
x = ipdom[x]
while index[y] < index[x]:
y = ipdom[y]
return x
for n in reversed(topo): # END first
if n == END:
continue
d = None
for m in g[n]:
d = m if d is None else inter(d, m)
ipdom[n] = d
emitted = [0]
def body(n, ind):
if n > END:
w(ind + f'again_{n - END - 1} = true;'); return
for line in self.blocks[n]:
if not isinstance(line, tuple):
w(ind + line)
emitted[0] += 1
def region(n, stop, ind):
while n != stop:
if n in cont and (n, 'open') not in region.active:
# loop: body once per iteration until no back edge is taken
e = g[cont[n]][0]
w(ind + f'bool again_{cont[n] - END - 1};'); w(ind + 'do\n' + ind + '{')
w(ind + f' again_{cont[n] - END - 1} = false;')
region.active.add((n, 'open'))
region(n, e, ind + ' ')
region.active.discard((n, 'open'))
w(ind + f'}} while (again_{cont[n] - END - 1});')
n = e
continue
body(n, ind)
sc = g[n]
if len(sc) == 1:
n = sc[0]; continue
br = self.blocks[n][-1]
m = ipdom[n]
t_empty, f_empty = sc[0] == m, sc[1] == m
if t_empty and f_empty:
pass
elif f_empty:
w(ind + f'if ({br[1]})\n' + ind + '{'); region(sc[0], m, ind + ' '); w(ind + '}')
elif t_empty:
w(ind + f'if (!({br[1]}))\n' + ind + '{'); region(sc[1], m, ind + ' '); w(ind + '}')
else:
w(ind + f'if ({br[1]})\n' + ind + '{'); region(sc[0], m, ind + ' ')
w(ind + '}\n' + ind + 'else\n' + ind + '{'); region(sc[1], m, ind + ' '); w(ind + '}')
n = m
region.active = set()
import sys as _s
_s.setrecursionlimit(20000)
region(0, END, ' ')
w('}')
sys.stderr.write(f'reachable blocks={len(nodes)} emitted block copies={emitted[0]} loops={len(headers)}\n')
return '\n'.join(out) + '\n'
RUNTIME = r'''
layout(buffer_reference, scalar, buffer_reference_align = 2) readonly buffer R16 { uint16_t v[4]; };
layout(buffer_reference, scalar, buffer_reference_align = 2) writeonly buffer W16 { uint16_t v[4]; };
float16_t H(uint u) { return uint16BitsToFloat16(uint16_t(u)); }
uint HU(float16_t h) { return uint(float16BitsToUint16(h)); }
f16vec2 H2(uint u) { return uint16BitsToFloat16(u16vec2(uint16_t(u & 0xffffu), uint16_t(u >> 16))); }
uint HU2_(u16vec2 u) { return uint(u.x) | (uint(u.y) << 16); }
uint H2U(f16vec2 h) { return HU2_(float16BitsToUint16(h)); }
int s16(uint u) { return (int(u) << 16) >> 16; }
float rcp_a(float x) { return 1.0 / x; }
// x + copysign(0.5, x) rounded toward zero and then truncated: round half away from zero, exactly
float round_half_away(float x) { float t = trunc(x); return abs(x - t) >= 0.5 ? t + sign(x) : t; }
uint PARAM32(uint o) { return param.w[o >> 2]; }
uint64_t PARAM64(uint o) { return uint64_t(param.w[o >> 2]) | (uint64_t(param.w[(o >> 2) + 1u]) << 32); }
uint PARAM16(uint o) { return (param.w[o >> 2] >> ((o & 2u) * 8u)) & 0xffffu; }
uint PARAM8(uint o) { return (param.w[o >> 2] >> ((o & 3u) * 8u)) & 0xffu; }
uint SH32(uint a) { return sh[a >> 2]; }
uint SH16(uint a) { return (sh[a >> 2] >> ((a & 2u) * 8u)) & 0xffffu; }
uint G16(uint64_t a) { return uint(R16(a).v[0]); }
uint G32(uint64_t a) { R16 r = R16(a); return uint(r.v[0]) | (uint(r.v[1]) << 16); }
uint bfi32(uint ins, uint base, uint pos, uint len)
{
pos &= 0xffu; len &= 0xffu;
if (pos >= 32u) return base;
uint mask = len >= 32u ? (0xffffffffu << pos) : (((1u << len) - 1u) << pos);
return (~mask & base) | (mask & (ins << pos));
}
// prmt.b32 (default mode): byte i of the result is byte (selector >> 4i) & 7 of the eight bytes b:a
uint prmt(uint a, uint b, uint sel)
{
uint r = 0u;
for (uint i = 0u; i < 4u; ++i) { const uint k = (sel >> (4u * i)) & 7u; r |= (((k < 4u ? a : b) >> (8u * (k & 3u))) & 0xffu) << (8u * i); }
return r;
}
// shfl.sync.bfly within a 32-thread warp (ZLUDA's SHFL_SYNC_IMPL)
uint shfl_bfly(uint v, int delta, uint opts, out bool in_bounds)
{
int section_mask = int((opts >> 8) & 31u), warp_end = int(opts & 31u);
int self = int(gl_SubgroupInvocationID & 31u);
int sub_end = (section_mask & self) | (~section_mask & warp_end);
int idx = self ^ (delta & 31);
bool oob = idx > sub_end;
if (oob) idx = self;
in_bounds = !oob;
return subgroupShuffle(v, (gl_SubgroupInvocationID & 32u) | uint(idx));
}
void sust_p(writeonly image2D s, int x, int y, vec4 v)
{
ivec2 n = imageSize(s);
if (uint(x) < uint(n.x) && uint(y) < uint(n.y)) imageStore(s, ivec2(x, y), v);
}
// byte-addressed 32-bit store into a 16-bit single-channel surface: two texels
void sust_b(writeonly uimage2D s, int xb, int y, uint v)
{
ivec2 n = imageSize(s);
int x = xb >> 1;
if (uint(y) < uint(n.y))
{
if (uint(x) < uint(n.x)) imageStore(s, ivec2(x, y), uvec4(v & 0xffffu));
if (uint(x + 1) < uint(n.x)) imageStore(s, ivec2(x + 1, y), uvec4(v >> 16));
}
}
'''
def build(make, lut, fast, no_half):
t = make()
t.exp_lut = lut
if not no_half:
t.collect = True
t.translate()
parent = {}
def find(x):
while parent.setdefault(x, x) != x:
parent[x] = parent[parent[x]]; x = parent[x]
return x
for x, y in t.links:
if t.width.get(x) == t.width.get(y):
parent[find(x)] = find(y)
else:
t.ctx.setdefault(x, set()).add('u'); t.ctx.setdefault(y, set()).add('u')
groups = {}
for x in set(t.ctx) | set(parent):
groups.setdefault(find(x), set()).update(t.ctx.get(x, set()))
half = {x for x in set(t.ctx) | set(parent) if x in t.width and groups[find(x)] == {'h'}}
sys.stderr.write(f'half-typed registers: {len(half)} of {len(t.width)}\n')
lut2 = t.exp_lut
t = make(); t.exp_lut = lut2; t.half = half
t.fast = fast
t.translate()
return t
if __name__ == '__main__':
filt = {}
args = sys.argv[1:]
while '--tex' in args:
i = args.index('--tex'); k, v = args[i + 1].split(':'); filt[int(k)] = v; del args[i:i + 2]
lut = '--exp-lut' in args
if lut:
args.remove('--exp-lut')
fast = '--fast' in args
if fast:
args.remove('--fast')
src = open(args[0]).read()
t = build(lambda: T(src, args[1]), lut, fast, '--no-half-types' in args)
sys.stdout.write(t.emit(filt))
sys.stderr.write(f'blocks={len(t.blocks)} textures={sorted(t.textures)} surfaces={t.surfaces} shared={t.shared_bytes}\n')