import argparse, ctypes, glob, hashlib, itertools, os, re, shlex, shutil, struct, subprocess, sys
TEXT_FLAGS = [0x2, 0x4, 0x10, 0x20, 0x40, 0x80]
FLAGS = sorted({0, *TEXT_FLAGS, *(a | b for a, b in itertools.combinations(TEXT_FLAGS, 2)), sum(TEXT_FLAGS)})
STAGE = r"C:\dx9-fixtures"
HAND_BUILT = {
"sm1.coissue.ps11": [0xFFFF0101, 0x00000042, 0xB00F0000, 0x00000001, 0x800F0000, 0xB0E40000, 0x00000001,
0x80070000, 0xA0E40000, 0x40000001, 0x80080000, 0x90E40000, 0x0000FFFF],
"sm1.coissue.ps12": [0xFFFF0102, 0x00000042, 0xB00F0000, 0x00000001, 0x800F0000, 0xB0E40000, 0x40000001,
0x80080000, 0x90FF0000, 0x0000FFFF],
"sm1.plain.ps13": [0xFFFF0103, 0x00000042, 0xB00F0000, 0x00000001, 0x800F0000, 0xB0E40000, 0x0000FFFF],
"sm1.coissue.ps14": [0xFFFF0104, 0x00000042, 0x800F0000, 0xB0E40000, 0x0000FFFD, 0x00000005, 0x800F0000,
0x80E40000, 0x90E40000, 0x00000001, 0x80070000, 0xA0E40000, 0x40000001, 0x80080000,
0xA0E40000, 0x0000FFFF],
"sm1.modifiers.ps11": [0xFFFF0101, 0x00000051, 0xA00F0000, 0x3F800000, 0x3F000000, 0x00000000, 0x3F800000,
0x00000042, 0xB00F0000, 0x00000042, 0xB00F0001, 0x00000005, 0x810F0000, 0xB4E40000,
0x90E40000, 0x00000002, 0x801F0000, 0x80E40000, 0xB6E40001, 0x00000050, 0x800F0000,
0x80FF0000, 0xA0E40000, 0x90E40000, 0x0000FFFF],
"sm1.plain.vs11": [0xFFFE0101, 0x0000001F, 0x80000000, 0x900F0000, 0x00000001, 0xC00F0000, 0x90E40000,
0x0000FFFF],
"sm1.relative.vs11": [0xFFFE0101, 0x0000001F, 0x80000000, 0x900F0000, 0x00000014, 0xC00F0000, 0x90E40000,
0xA0E40000, 0x00000001, 0xB0010000, 0x90000000, 0x00000001, 0xD00F0000, 0xA0E42004,
0x0000FFFF],
"sm1.pairs.ps11": [0xFFFF0101, 0x00000042, 0xB00F0000, 0x00000042, 0xB00F0001, 0x00000005, 0x80070000,
0xB0E40000, 0x90E40000, 0x40000005, 0x81080000, 0xB0E40001, 0x90E40000, 0x00000002,
0x80070000, 0x80E40000, 0xB0E40001, 0x40000002, 0x80180000, 0x80E40000, 0xA0E40000,
0x00000001, 0x800F0001, 0x80E40000, 0x0000FFFF],
}
VERSIONS = {"vs11": 0xFFFE0101, "vs20": 0xFFFE0200, "vs2x": 0xFFFE0201, "vs30": 0xFFFE0300, "ps11": 0xFFFF0101,
"ps12": 0xFFFF0102, "ps13": 0xFFFF0103, "ps14": 0xFFFF0104, "ps20": 0xFFFF0200, "ps2x": 0xFFFF0201,
"ps30": 0xFFFF0300}
OPERANDS = {
**dict.fromkeys([0, 28, 29, 39, 42, 43, 44], (False, 0)), **dict.fromkeys([25, 30, 38, 40, 96], (False, 1)), **dict.fromkeys([26, 27, 41, 45], (False, 2)), **dict.fromkeys([65, 87], (True, 0)), **dict.fromkeys([1, 6, 7, 14, 15, 16, 19, 35, 36, 46, 78, 79, 91, 92, 67, 68, 69, 70, 71, 72, 73, 74, 77, 82, 83,
84, 85, 86], (True, 1)),
**dict.fromkeys([2, 3, 5, 8, 9, 10, 11, 12, 13, 17, 20, 21, 22, 23, 24, 32, 33, 76, 89, 94, 95], (True, 2)),
**dict.fromkeys([4, 18, 80, 88, 90], (True, 3)), 93: (True, 4), }
def operands(op, version):
major, minor = (version >> 8) & 0xFF, version & 0xFF
if op == 66: return (True, (1 if minor == 4 else 0) if major == 1 else 2)
if op == 64: return (True, 1 if minor == 4 else 0)
if op in (34, 37): return (True, 3 if major < 3 else 1)
return OPERANDS[op]
def instruction(version, op, destination, sources, flags=0):
words = ([destination] if destination is not None else []) + list(sources)
length = 0 if (version >> 8) & 0xFF == 1 else len(words)
return [op | flags | (length << 24), *words]
def every_opcode(version):
body = []
for op in [*range(0, 31), 31, *range(32, 47), 47, 48, *range(64, 75), *range(76, 81), 81, *range(82, 97), 0xFFFD]:
if op == 31:
body += instruction(version, 31, None, [0x80000005, 0x900F0000]) elif op == 81:
body += instruction(version, 81, None, [0xA00F0000, 0x3F800000, 0, 0, 0x3F800000]) elif op == 48:
body += instruction(version, 48, None, [0xF00F0000, 1, 2, 3, 4]) elif op == 47:
body += instruction(version, 47, None, [0xE00F0800, 1]) elif op == 0xFFFD:
body.append(0xFFFD) else:
destination, count = operands(op, version)
control = 1 << 16 if op in (41, 45, 94) else 0
body += instruction(version, op, 0x800F0000 if destination else None,
[0x80E40001 + n for n in range(count)], control)
body += instruction(version, 31, None, [0x80010005, 0x900F0001]) return [version, *body, 0xFFFF]
def every_register(version):
kind = lambda t: ((t & 7) << 28) | ((t & 0x18) << 8)
sm1 = (version >> 8) & 0xFF == 1
body = instruction(version, 1, 0x80070000, [0x90E40000], 0x40000000)
for t, numbers in [(t, [0]) for t in range(16) if t != 4] + [(4, [0, 1, 2]), (17, [0, 1]), (18, [0]), (19, [0])]:
for n in numbers:
body += instruction(version, 1, 0x800F0000, [0x80E40000 | kind(t) | n])
body += instruction(version, 1, 0x800F0000 | kind(t) | n, [0x80E40001])
for usage in range(14):
for index in (0, 1):
body += instruction(version, 31, None, [0x80000000 | usage | index << 16, 0x900F0000 | usage * 2 + index])
for shift in (1, 2, 3, 13, 14, 15):
body += instruction(version, 1, 0x800F0000 | shift << 24, [0x90E40000])
address = [] if sm1 else [0xB0000000] body += instruction(version, 1, 0x800F2000, [*address, 0x90E40000])
body += instruction(version, 1, 0x800F0000, [0xA0E42000, *address])
body += instruction(version, 1, 0x80070000, [0x90E40000])
body += instruction(version, 1, 0x80080001, [0x90E40001], 0x40000000)
body += instruction(version, 1, 0x800F0000, [0xB0E41000, 0x90E40000], 0x10000000)
body += instruction(version, 2, 0x800F0000, [0xBD001000, 0x90E40000, 0x90E40001], 0x10000000)
return [version, *body, 0xFFFF]
for _name, _version in VERSIONS.items():
HAND_BUILT[f"versions.opcodes.{_name}"] = every_opcode(_version)
HAND_BUILT[f"versions.registers.{_name}"] = every_register(_version)
def ctab_types(data):
data = bytearray(data)
assert data[4:6] == b"\xfe\xff" and data[8:12] == b"CTAB"
base = 12
count, info = struct.unpack_from("<II", data, base + 12)
for n in range(count):
name, _, _, _, _, type_info, _ = struct.unpack_from("<I4HII", data, base + info + 20 * n)
number = int(data[base + name:data.index(b"\0", base + name)].decode()[1:])
if number < 72:
struct.pack_into("<HH", data, base + type_info, 0, number)
else:
struct.pack_into("<H", data, base + type_info, number - 66)
return bytes(data)
PATCHED = {"ctab.types.ps30": ("ctab.many.ps30", ctab_types)}
def disassembler(path):
fn = ctypes.WinDLL(path).D3DDisassemble
fn.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint, ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)]
fn.restype = ctypes.c_long
def call(data, flags):
out = ctypes.c_void_p()
hr = fn(ctypes.create_string_buffer(data, len(data)), len(data), flags, None, ctypes.byref(out))
if hr != 0:
return None, hr & 0xFFFFFFFF
vt = ctypes.cast(out, ctypes.POINTER(ctypes.POINTER(ctypes.c_void_p))).contents
ptr = ctypes.WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(vt[3])(out)
size = ctypes.WINFUNCTYPE(ctypes.c_size_t, ctypes.c_void_p)(vt[4])(out)
text = ctypes.string_at(ptr, size)
ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)(vt[2])(out)
return text, 0
return call
def sha256(path):
return hashlib.sha256(open(path, "rb").read()).hexdigest()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--fxc", required=True)
ap.add_argument("--dll", required=True)
a = ap.parse_args()
here = os.path.dirname(os.path.abspath(__file__))
listings = os.path.join(here, "listings")
os.makedirs(listings, exist_ok=True)
disasm = disassembler(a.dll)
print(f"fxc {a.fxc} {sha256(a.fxc)}")
print(f"dll {a.dll} {sha256(a.dll)}")
built, failed = [], 0
os.makedirs(STAGE, exist_ok=True)
for include in sorted(glob.glob(os.path.join(here, "*.hlsli"))):
shutil.copyfile(include, os.path.join(STAGE, os.path.basename(include)))
for original in sorted(glob.glob(os.path.join(here, "*.hlsl"))):
src = os.path.join(STAGE, os.path.basename(original))
shutil.copyfile(original, src)
for line in open(original, encoding="utf-8"):
m = re.match(r"//\s*fxc:\s*(.*)", line)
if not m:
continue
args = shlex.split(m.group(1), posix=False)
out = os.path.join(here, args[args.index("/Fo") + 1])
args[args.index("/Fo") + 1] = out
r = subprocess.run([a.fxc, "/nologo", *args, src], capture_output=True, text=True)
if r.returncode != 0:
failed += 1
print(f"FAIL {os.path.basename(out)}: {r.stderr.strip() or r.stdout.strip()}")
continue
built.append(out)
for name, (source, change) in PATCHED.items():
out = os.path.join(here, name + ".bin")
open(out, "wb").write(change(open(os.path.join(here, source + ".bin"), "rb").read()))
built.append(out)
for name, words in HAND_BUILT.items():
out = os.path.join(here, name + ".bin")
open(out, "wb").write(struct.pack(f"<{len(words)}I", *words))
built.append(out)
for path in built:
data = open(path, "rb").read()
stem = os.path.join(listings, os.path.basename(path)[: -len(".bin")])
for flags in FLAGS:
text, hr = disasm(data, flags)
if text is None:
failed += 1
print(f"D3DDisassemble {os.path.basename(path)} flags {flags:#x}: hr {hr:#010x}")
continue
name = stem + (".asm" if flags == 0 else f".flag{flags:02x}.asm")
open(name, "wb").write(text.rstrip(b"\0"))
print(f"ok {os.path.basename(path)}")
print(f"{len(built)} built, {failed} failed")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())