#!/usr/bin/env python3 """The one instruction-level SPIR-V edit of the GLSL build. Binary, never text. usage: spvedit.py 1. Keeps only the capabilities Shader and Linkage and drops every OpExtension. 2. Deletes each wrapper function with its OpName and decorations, moves its LinkageAttributes Export to its callee, and sets the callee's OpName to the source name. Refuses a wrapper that is not exactly one OpFunctionCall. 3. By-value lowering: dxc -Od passes every parameter as a Function pointer and the caller copies in, so a callee that assigns its parameter comes out of SPIRV-Cross as `inout`. Each Function-pointer OpFunctionParameter becomes a value parameter with a new id; the old id stays as an entry-block OpVariable stored from it (OpName copied to the new id); OpTypeFunction is reused if the signature exists, else added; each OpFunctionCall loads the pointer it passed. The header bound is last id + 1.""" import struct, sys OP_NAME, OP_MEMBER_NAME, OP_CAPABILITY, OP_EXTENSION, OP_DECORATE, OP_MEMBER_DECORATE = 5, 6, 17, 10, 71, 72 OP_TYPE_ARRAY, OP_CONSTANT, OP_SPEC_CONSTANT, DECORATION_SPEC_ID = 28, 43, 50, 1 SPEC_ID = int.from_bytes(b'CMS1', 'big') # CultMath's own id: a consumer's SPIRV_CROSS_CONSTANT_ID_n macro cannot meet it OP_TYPE_POINTER, OP_TYPE_FUNCTION, OP_FUNCTION, OP_PARAMETER, OP_FUNCTION_END = 32, 33, 54, 55, 56 OP_CALL, OP_LABEL, OP_VARIABLE, OP_LOAD, OP_STORE = 57, 248, 59, 61, 62 CAP_SHADER, CAP_LINKAGE, DECORATION_LINKAGE, LINKAGE_EXPORT, FUNCTION_STORAGE = 1, 5, 41, 41, 7 TERMINATORS = (8, 317, 62, 246, 247, 249, 250, 253, 254, 252) def load(path): raw = open(path, 'rb').read() words = list(struct.unpack('<%dI' % (len(raw) // 4), raw)) ins, i = [], 5 while i < len(words): n = words[i] >> 16 ins.append(words[i:i + n]) i += n return words[:5], ins def op(x): return x[0] & 0xffff def mk(o, *ws): return [((len(ws) + 1) << 16) | o, *ws] def decode(ws): b = struct.pack('<%dI' % len(ws), *ws) return b[:b.index(0)].decode() def encode(s): b = s.encode() + b'\0' b += b'\0' * (-len(b) % 4) return list(struct.unpack('<%dI' % (len(b) // 4), b)) def functions(ins): """function id -> [first index, last index, callee ids, ids defined inside]""" fs, cur = {}, None for k, x in enumerate(ins): o = op(x) if o == OP_FUNCTION: cur = x[2] fs[cur] = [k, None, [], set()] if cur is None: continue if o == OP_CALL: fs[cur][2].append(x[3]) if o == OP_LABEL: fs[cur][3].add(x[1]) elif o not in TERMINATORS and len(x) > 2: fs[cur][3].add(x[2]) if o == OP_FUNCTION_END: fs[cur][1] = k cur = None return fs def export_edit(h, ins, rows): names = {x[1]: decode(x[2:]) for x in ins if op(x) == OP_NAME} fs = functions(ins) by_name = {v: k for k, v in names.items() if k in fs} drop_k, drop_ids, set_name, retarget = set(), set(), {}, {} callee_of = [] for wn, name, _, _, _ in rows: fid = by_name.get(wn) callees = fs[fid][2] if fid else None if not callees or len(callees) != 1: sys.exit('refuse: wrapper %s is not exactly one OpFunctionCall' % wn) a, b, _, ids = fs[fid] drop_k.update(range(a, b + 1)) drop_ids |= ids | {fid} set_name[callees[0]] = name retarget[fid] = callees[0] callee_of.append(callees[0]) out, named = [], set() for k, x in enumerate(ins): if k in drop_k: continue o = op(x) if o == OP_CAPABILITY and x[1] not in (CAP_SHADER, CAP_LINKAGE): continue if o == OP_EXTENSION: continue if o == OP_NAME and x[1] in set_name: x = mk(OP_NAME, x[1], *encode(set_name[x[1]])) named.add(x[1]) elif o in (OP_NAME, OP_MEMBER_NAME) and x[1] in drop_ids: continue if o == OP_DECORATE and x[1] in drop_ids: if x[1] in retarget and x[2] == DECORATION_LINKAGE: x = [x[0], retarget[x[1]]] + x[2:] else: continue out.append(x) for fid, name in set_name.items(): if fid not in named: out.insert(next(i for i, x in enumerate(out) if op(x) == OP_NAME), mk(OP_NAME, fid, *encode(name))) return out, callee_of def by_value(h, ins): bound = h[3] ptr = {x[1]: (x[2], x[3]) for x in ins if op(x) == OP_TYPE_POINTER} # id -> (storage, pointee) ftype = {x[1]: x for x in ins if op(x) == OP_TYPE_FUNCTION} fnparams, fntype, cur = {}, {}, None for x in ins: # per function: which parameters are Function pointers if op(x) == OP_FUNCTION: cur = x[2] fnparams[cur], fntype[cur] = [], x[4] elif op(x) == OP_PARAMETER and cur is not None: t = x[1] conv = t in ptr and ptr[t][0] == FUNCTION_STORAGE fnparams[cur].append((x[2], t, ptr[t][1] if conv else None, bound if conv else None)) bound += conv newid = {p[0]: p[3] for ps in fnparams.values() for p in ps if p[2]} newtype, typeins = {}, [] sig = {tuple(x[2:]): x[1] for x in ins if op(x) == OP_TYPE_FUNCTION} for f, ps in fnparams.items(): if any(p[2] for p in ps): key = (ftype[fntype[f]][2], *[p[2] if p[2] else p[1] for p in ps]) if key not in sig: bound += 1 sig[key] = bound typeins.append(mk(OP_TYPE_FUNCTION, bound, *key)) newtype[f] = sig[key] out, cur, pend, need_stores, first, pc = [], None, [], False, True, 0 for x in ins: o = op(x) if o == OP_FUNCTION and first: out += typeins first = False if need_stores and o != OP_VARIABLE: out += [mk(OP_STORE, p, q) for p, _, q in pend] pend, need_stores = [], False if o == OP_FUNCTION: cur, pc = x[2], 0 if cur in newtype: x = x[:4] + [newtype[cur]] + x[5:] elif o == OP_PARAMETER: _, t, pointee, q = fnparams[cur][pc] pc += 1 if pointee: out.append(mk(OP_PARAMETER, pointee, q)) pend.append((x[2], t, q)) continue elif o == OP_LABEL and pend: out.append(x) out += [mk(OP_VARIABLE, t, p, FUNCTION_STORAGE) for p, t, _ in pend] need_stores = True continue elif o == OP_CALL: args = [] for a, (_, _, pointee, _) in zip(x[4:], fnparams[x[3]]): if pointee: bound += 1 out.append(mk(OP_LOAD, pointee, bound, a)) args.append(bound) else: args.append(a) x = mk(OP_CALL, *x[1:4], *args) out.append(x) if o == OP_NAME and x[1] in newid: out.append([x[0], newid[x[1]]] + x[2:]) h[3] = bound + 1 return out, len(newtype), len(newid) def capacity_edit(h, ins, rows, callee_of, default): """Each capacity parameter (wrappers.tsv column 5) of each callee has an OpTypeArray type. Exactly one length id may serve all of them, and nothing but OpTypeArray, OpName and decorations may use it. That OpConstant becomes an OpSpecConstant with SpecId SPEC_ID named cultmath_max_sites, defaulting to the HLSL guard value. The constant is the sentinel wrappers.py compiled the library with, so no array outside the capacity parameters shares its length id. Returns the instructions and the spec constant id.""" ftype = {x[1]: x for x in ins if op(x) == OP_TYPE_FUNCTION} arrays = {x[1]: x for x in ins if op(x) == OP_TYPE_ARRAY} params, cur = {}, None for x in ins: if op(x) == OP_FUNCTION: cur = x[2] params[cur] = [] elif op(x) == OP_PARAMETER: params[cur].append(x[1]) lengths = set() for row, fid in zip(rows, callee_of): for i in [int(c) for c in row[4].split(',') if c]: t = params[fid][i] if t not in arrays: sys.exit('refuse: capacity parameter %d of %s is not an array type' % (i, row[1])) lengths.add(arrays[t][3]) if len(lengths) != 1: sys.exit('refuse: %d array length ids serve the capacity parameters, expected exactly one' % len(lengths)) cap = lengths.pop() site_types = {params[fid][int(c)] for row, fid in zip(rows, callee_of) for c in row[4].split(',') if c} if {t for t, a in arrays.items() if a[3] == cap} != site_types: sys.exit('refuse: an array type of the capacity length is not a capacity parameter type') for x in ins: o = op(x) defines = o in (OP_CONSTANT, OP_SPEC_CONSTANT) and x[2] == cap allowed = o in (OP_TYPE_ARRAY, OP_NAME, OP_DECORATE, OP_MEMBER_DECORATE) or defines if cap in x[1:] and not allowed: sys.exit('refuse: the capacity length id %d is also used by opcode %d' % (cap, o)) out = [] for x in ins: if op(x) == OP_CONSTANT and x[2] == cap: x = [(x[0] & 0xffff0000) | OP_SPEC_CONSTANT, x[1], x[2], default] out.append(x) first = next((k for k, x in enumerate(out) if op(x) in (OP_DECORATE, OP_MEMBER_DECORATE)), None) if first is None: sys.exit('refuse: no annotation section to decorate the capacity constant in') out[first:first] = [mk(OP_NAME, cap, *encode('cultmath_max_sites')), mk(OP_DECORATE, cap, DECORATION_SPEC_ID, SPEC_ID)] return out, cap def main(src, dst, tsv, capacity): h, ins = load(src) rows = [l.split('\t') for l in open(tsv).read().splitlines() if l] ins, callee_of = export_edit(h, ins, rows) ins, changed, params = by_value(h, ins) default = int(open(capacity).read().split()[0]) ins, spec = capacity_edit(h, ins, rows, callee_of, default) words = h + [v for x in ins for v in x] open(dst, 'wb').write(struct.pack('<%dI' % len(words), *words)) print('spvedit: %d wrappers dropped; by-value: %d functions changed, %d parameters; capacity spec constant id %d' % (len(rows), changed, params, spec)) if __name__ == '__main__': main(*sys.argv[1:5])