diff --git a/dx2cg/README.md b/dx2cg/README.md new file mode 100644 index 0000000..506705d --- /dev/null +++ b/dx2cg/README.md @@ -0,0 +1,10 @@ +# dx2cg +Tools for converting d3d9 shader assembly to HLSL/Cg. +- `disassembler.py`: Takes in d3d9 assembly and gives back the HLSL equivalent. +- `swapper.py`: Searches a shader file for d3d9 assembly and calls the disassembler to replace it with HLSL. +- `main.py`: Executes the swapper on every file in a path, writing the changes to new files. + +## Known issues +- Only vertex shaders with profile `vs_1_1` are supported +- No fragment shaders are supported yet +- Only a limited set of instructions (those used by FF and Unity 2.6) are supported diff --git a/dx2cg/disassembler.py b/dx2cg/disassembler.py new file mode 100644 index 0000000..e7b5754 --- /dev/null +++ b/dx2cg/disassembler.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# coding: utf-8 +# d3d9 to cg crude dissassembler +# ycc 08/08/2022 + +import re + +legacy = False # True for 2.6 + +reserved = { + "_Time", + "_SinTime", + "_CosTime", + + "_ProjectionParams", + + "_PPLAmbient", + + "_ObjectSpaceCameraPos", + "_ObjectSpaceLightPos0", + "_ModelLightColor0", + "_SpecularLightColor0", + + "_Light2World0", "_World2Light0", "_Object2World", "_World2Object", "_Object2Light0", + + "_LightDirectionBias", + "_LightPositionRange", +} + +decls = { + "dcl_position": "float4 {0} = vardat.vertex;", + "dcl_normal": "float4 {0} = float4(vardat.normal.x, vardat.normal.y, vardat.normal.z, 0);", + "dcl_texcoord0": "float4 {0} = vardat.texcoord;", + "dcl_texcoord1": "float4 {0} = vardat.texcoord1;", + "dcl_color": "float4 {0} = vardat.color;", + "def": "const float4 {0} = float4({1}, {2}, {3}, {4});", +} + +ops = { + "mov": "{0} = {1};", + "add": "{0} = {1} + {2};", + "mul": "{0} = {1} * {2};", + "mad": "{0} = {1} * {2} + {3};", + "dp4": "{0} = dot((float4){1}, (float4){2});", + "dp3": "{0} = dot((float3){1}, (float3){2});", + "min": "{0} = min({1}, {2});", + "max": "{0} = max({1}, {2});", + "rsq": "{0} = rsqrt({1});", + "frc": "{0} = float4({1}.x - (float)floor({1}.x), {1}.y - (float)floor({1}.y), {1}.z - (float)floor({1}.z), {1}.w - (float)floor({1}.w));", + "slt": "{0} = float4(({1}.x < {2}.x) ? 1.0f : 0.0f, ({1}.y < {2}.y) ? 1.0f : 0.0f, ({1}.z < {2}.z) ? 1.0f : 0.0f, ({1}.w < {2}.w) ? 1.0f : 0.0f);", + "sge": "{0} = float4(({1}.x >= {2}.x) ? 1.0f : 0.0f, ({1}.y >= {2}.y) ? 1.0f : 0.0f, ({1}.z >= {2}.z) ? 1.0f : 0.0f, ({1}.w >= {2}.w) ? 1.0f : 0.0f);", + "rcp": "{0} = ({1} == 0.0f) ? FLT_MAX : (({1} == 1.0f) ? {0} : (1 / {1}));", +} + +struct_appdata = """struct appdata { +\tfloat4 vertex : POSITION; +\tfloat3 normal : NORMAL; +\tfloat4 texcoord : TEXCOORD0; +\tfloat4 texcoord1 : TEXCOORD1; +\tfloat4 tangent : TANGENT; +\tfloat4 color : COLOR; +}; +""" + +v2f_postype = "POSITION" if legacy else "SV_POSITION" +struct_v2f = f"""struct v2f {{ +\tfloat4 pos : {v2f_postype}; +\tfloat4 t0 : TEXCOORD0; +\tfloat4 t1 : TEXCOORD1; +\tfloat4 t2 : TEXCOORD2; +\tfloat4 t3 : TEXCOORD3; +\tfloat fog : FOG; +\tfloat4 d0 : COLOR0; +\tfloat4 d1 : COLOR1; +}}; +""" + +cg_header = """CGPROGRAM +#include "UnityCG.cginc" +#pragma exclude_renderers xbox360 ps3 gles +""" + +cg_footer = """ENDCG""" + +vertex_header = """v2f vert(appdata vardat) { +\tfloat4 r0, r1, r2, r3, r4; +\tfloat4 tmp; +\tv2f o; +""" + +vertex_footer = """\treturn o; +}""" + +def process_header(prog): + keywords = [] + header = [] + loctab = {} + locdecl = [] + binds = [] + i = 0 + lighting = False + while i < len(prog): + line = prog[i] + if line.startswith("Keywords"): + keywords = re.findall("\"[\w\d]+\"", line) + del prog[i] + i = i - 1 + elif line.startswith("Bind"): + binds.append(line) + del prog[i] + i = i - 1 + elif line.startswith("Local") or line.startswith("Matrix"): + dec = line.split(' ') + key = int(dec[1][:-1]) + if dec[2][0] == '[': + # singleton + val = dec[2][1:-1] + if val[0] == '_' and val not in reserved: + loctype = "float4" if dec[0] == "Local" else "float4x4" + locdecl.append(f"{loctype} {val};") + elif dec[2][0] == '(': + #components + vals = dec[2][1:-1].split(',') + for j, v in enumerate(vals): + if v[0] == '[': + vals[j] = v[1:-1] + if vals[j][0] == '_' and vals[j] not in reserved: + locdecl.append(f"float {vals[j]};") + val = f"float4({vals[0]},{vals[1]},{vals[2]},{vals[3]})" + + lightval = re.match("glstate_light(\d)_([a-zA-Z]+)", val) + if lightval: + val = f"glstate.light[{lightval[1]}].{lightval[2]}" + lighting = True + elif val == "glstate_lightmodel_ambient": + val = "glstate.lightmodel.ambient" + lighting = True + elif val.startswith("glstate_matrix_texture"): + val = f"glstate.matrix.texture[{val[-1]}]" if legacy else f"UNITY_MATRIX_TEXTURE{val[-1]}" + elif val == "glstate_matrix_mvp": + val = "glstate.matrix.mvp" if legacy else "UNITY_MATRIX_MVP" + elif val == "glstate_matrix_modelview0": + val = "glstate.matrix.modelview[0]" if legacy else "UNITY_MATRIX_MV" + elif val == "glstate_matrix_transpose_modelview0": + val = "glstate.matrix.transpose.modelview[0]" if legacy else "UNITY_MATRIX_T_MV" + elif val == "glstate_matrix_invtrans_modelview0": + val = "glstate.matrix.invtrans.modelview[0]" if legacy else "UNITY_MATRIX_IT_MV" + elif val.startswith("glstate"): + raise ValueError(f"Unrecognized glstate: {val}") + + if dec[0] == "Local": + loctab[key] = val + elif dec[0] == "Matrix": + for offset in range(0,4): + loctab[key + offset] = f"{val}[{offset}]" + + del prog[i] + i = i - 1 + i = i + 1 + + if len(binds) > 0: + header.append("BindChannels {") + for b in binds: + header.append(f"\t{b}") + header.append("}") + + if lighting: + header.append("Lighting On") + + return (keywords, header, loctab, locdecl) + +def resolve_args(args, loctab, consts): + for a in range(0, len(args)): + arg = args[a] + + neg = "" + if arg[0] == '-': + arg = arg[1:] + neg = "-" + + # save swizzler! + dot = arg.find(".") + if dot > -1: + swiz = arg[dot:] + arg = arg[:dot] + else: + swiz = "" + + if arg[0] == 'r': + pass + elif arg[0] == 'v': + pass + elif arg[0] == 'c': + if arg not in consts: + key = int(arg[1:]) + arg = loctab[key] + elif arg[0] == 'o': + arg = f"o.{arg[1:].lower()}" + elif re.match("[+-]?([0-9]*[.])?[0-9]+", arg): + pass + else: + raise ValueError(f"Unknown arg {arg}") + + args[a] = neg + arg + swiz + +def decode(code, args): + if code in decls: + return [decls[code].format(*args)] + elif code in ops: + target = args[0] + if target == "o.fog": + return [ops[code].format(*args)] + + dot = re.search("\.[xyzw]+", target) + if dot: + swiz = target[dot.start()+1:] + target = target[:dot.start()] + else: + swiz = "xyzw" + + lines = [ops[code].format("tmp", *args[1:])] + for c in swiz: + lines.append(f"{target}.{c} = tmp.{c};") + return lines + else: + raise ValueError(f"Unknown code {code}") + +def process_asm(asm, loctab): + shadertype = "" + if asm[0] == "\"vs_1_1": + shadertype = "vertex" + else: + raise ValueError(f"Unsupported shader type: {asm[0][1:]}") + + consts = set() + translated = [] + i = 1 + while i < len(asm): + instruction = asm[i] + if instruction == "\"": + break + + space = instruction.find(" ") + if space == -1: + code = instruction + args = [] + else: + code = instruction[:space] + args = instruction[space+1:].split(", ") + + if code == "def": + consts.add(args[0]) + + resolve_args(args, loctab, consts) + disasm = decode(code, args) + # print(f"{instruction} \t==>\t{disasm}") + disasm.insert(0, f"// {instruction}") + translated.extend(disasm) + i = i + 1 + + return (shadertype, translated) + +def disassemble(text): + asm = text.split('\n')[1:-1] + (keywords, header, loctab, locdecl) = process_header(asm) + (shadertype, disasm) = process_asm(asm, loctab) + + text = "\n".join(header) + "\n" + text += cg_header + if keywords: + text += "#pragma multi_compile " + " ".join(keywords) + if shadertype == "vertex": + text += "#pragma vertex vert\n\n" + text += struct_appdata + "\n" + text += struct_v2f + "\n" + text += "\n".join(locdecl) + "\n" + if shadertype == "vertex": + text += vertex_header + "\n" + text += "\t" + "\n\t".join(disasm) + "\n\n" + if shadertype == "vertex": + text += vertex_footer + "\n" + text += cg_footer + return text diff --git a/dx2cg/main.py b/dx2cg/main.py new file mode 100644 index 0000000..617b7fb --- /dev/null +++ b/dx2cg/main.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# coding: utf-8 + +import os +import sys +from swapper import process + +def process_file(filename, suffix): + dot = filename.rfind(".") + if dot > -1: + outfile_name = filename[:dot] + suffix + filename[dot:] + else: + outfile_name = filename + suffix + return process(filename, outfile_name) + +def process_batch(path, suffix="_hlsl"): + files = os.listdir(path) + for f in files: + if os.path.isdir(f): + process_batch(f"{path}/{f}") + else: + try: + if process_file(f"{path}/{f}", suffix): + print(f"Processed {f}") + else: + print(f"Skipping {f}") + except ValueError as err: + print(f"Failed to process {f}: {err}") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: [outfile-suffix]") + elif len(sys.argv) == 2: + process_batch(sys.argv[1]) + else: + process_batch(*sys.argv[1:2]) + diff --git a/dx2cg/swapper.py b/dx2cg/swapper.py new file mode 100644 index 0000000..a83610d --- /dev/null +++ b/dx2cg/swapper.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# coding: utf-8 +# parser for replacing d3d9 subprograms in shaderlab files with HLSL/CG +# ycc 08/08/2022 + +import re +from disassembler import disassemble + +tabs = 3 +def indent(block): + lines = block.split('\n') + for i in range(0, len(lines)-1): + lines[i] = tabs * "\t" + lines[i] + return "\n".join(lines) + +def find_closing_bracket(block, i): + count = 0; + while i < len(block): + if block[i] == '{': + count = count + 1 + if block[i] == '}': + count = count - 1 + if count == 0: + return i + i = i + 1 + raise ValueError(f"Block at {i} has no closing bracket") + +def process_program(prog): + subprog_index = prog.find("SubProgram \"d3d9") + if subprog_index == -1: + raise ValueError(f"Program has no d3d9 subprogram") + subprog_end_index = find_closing_bracket(prog, subprog_index) + subprog = prog[subprog_index:subprog_end_index+1] + processed = disassemble(subprog) + "\n" + return indent(processed) + +def process_shader(shader): + buf = shader + processed = '' + program_index = buf.find('Program') + while program_index > -1: + processed = processed + buf[:program_index] + buf = buf[program_index:] + line = re.search("#LINE [0-9]+\n", buf) + if not line: + raise ValueError(f"Program at {program_index} has no #LINE marker") + end_index = line.end() + 1 + program_section = buf[program_index:end_index+1] + processed = processed + process_program(program_section) + buf = buf[end_index+1:] + + program_index = buf.find('Program') + processed = processed + buf + return processed + +def process(fn_in, fn_out): + with open(fn_in, "r") as fi: + buf = fi.read() + processed = process_shader(buf) + if buf != processed: + with open(fn_out, "w") as fo: + fo.write(processed) + return True + return False