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)
def ctab_order(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)
assert count > 2, count
for n in range(count):
name = struct.unpack_from("<I", data, base + info + 20 * n)[0]
data[base + name] = 0 if n == count // 2 else ord("z") - n
return bytes(data)
def instructions(data):
words = struct.unpack(f"<{len(data) // 4}I", data)
at, out = 1, []
while at < len(words) and words[at] != 0xFFFF:
token = words[at]
if token & 0xFFFF == 0xFFFE:
at += 1 + (token >> 16 & 0x7FFF)
continue
out.append((at, token & 0xFFFF, token >> 24 & 15))
at += 1 + (token >> 24 & 15)
return out
def def_specials(data):
data = bytearray(data)
values = [
[0x7F800000, 0xFF800000, 0x7FC00000, 0xFFC00000],
[0x7FA00000, 0xFFA00000, 0x7F800001, 0xFF800001],
[0x7FFFFFFF, 0xFFFFFFFF, 0x00000001, 0x80000000],
]
defs = [at for at, opcode, _ in instructions(data) if opcode == 0x51]
assert len(defs) == 3, defs
for at, four in zip(defs, values):
struct.pack_into("<4I", data, 4 * (at + 2), *four)
return bytes(data)
def min_precision(data):
data = bytearray(data)
n = 0
for at, opcode, length in instructions(data):
first = at + 2 if opcode == 0x1F else at + 1
last = at + 1 if opcode in (0x51, 0x30, 0x2F) else at + length
for word in range(first, last + 1):
token = struct.unpack_from("<I", data, 4 * word)[0]
if token & 0x80000000:
struct.pack_into("<I", data, 4 * word, token & ~0xC000 | (n % 3 + 1) << 14)
n += 1
assert n > 20, n
return bytes(data)
def definitions(data):
data = bytearray(data)
edits = {0x51: (7 << 20, 0x1), 0x30: (1 << 24, 0x0), 0x2F: (0xF << 24, 0x6)}
for at, opcode, _ in instructions(data):
if opcode in edits:
bits, mask = edits.pop(opcode)
token = struct.unpack_from("<I", data, 4 * (at + 1))[0]
struct.pack_into("<I", data, 4 * (at + 1), token & ~(0xFFF << 16) | bits | mask << 16)
assert not edits, edits
return bytes(data)
def sampler_swizzles(data):
data = bytearray(data)
swizzles = [0x0F, 0x00, 0xC9]
n = 0
for at, opcode, length in instructions(data):
if opcode == 0x1F:
continue
for word in range(at + 2, at + length + 1):
token = struct.unpack_from("<I", data, 4 * word)[0]
if token & 0x80000000 and (token >> 28 & 7 | (token >> 11 & 3) << 3) == 10:
struct.pack_into("<I", data, 4 * word, token & ~(0xFF << 16) | swizzles[n % 3] << 16)
n += 1
assert n >= 3, n
return bytes(data)
def ctab_constants(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)
out = []
for n in range(count):
at = base + info + 20 * n
name, register_set, _, _, _, _, default = struct.unpack_from("<I4HII", data, at)
out.append((at, data[base + name:data.index(b"\0", base + name)].decode(), register_set, default))
return base, out
def default_specials(data):
data = bytearray(data)
base, constants = ctab_constants(data)
values = {
"small": [0x7F800000, 0xFF800000, 0x7FC00000, 0xFFC00000],
"large": [0x7F800001, 0xFFFFFFFF, 0x00000001, 0x80000000],
"count": [0xFFFFFFFF, 0x7FFFFFFF, 0x80000000, 0x3F800000],
"flags": [0x80000000, 0, 0x00000100],
}
for at, name, _, default in constants:
if name in values:
words = values.pop(name)
struct.pack_into(f"<{len(words)}I", data, base + default, *words)
if name == "far":
struct.pack_into("<H", data, at + 6, 1234)
assert not values, values
return bytes(data)
def unaligned_default(data):
data = bytearray(data)
_, constants = ctab_constants(data)
ints = [at for at, _, register_set, _ in constants if register_set == 1]
assert len(ints) == 1, ints
struct.pack_into("<I", data, ints[0] + 16, 3)
return bytes(data)
PATCHED = {
"ctab.types.ps30": ("ctab.many.ps30", ctab_types),
"pixel.specials.ps30": ("pixel.flow.ps30", def_specials),
"pixel.precision.ps30": ("pixel.ps30", min_precision),
"versions.definitions.ps30": ("versions.opcodes.ps30", definitions),
"pixel.samplers.ps2a": ("pixel.ps2a", sampler_swizzles),
"pixel.order.ps30": ("pixel.ps30", ctab_order),
"defaults.specials.ps30": ("defaults.ps30", default_specials),
"pixel.twosets.default.ps30": ("pixel.twosets.ps30", unaligned_default),
}
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, 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 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 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())