genshaders.py (5282B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 import argparse 5 import codecs 6 import locale 7 import os 8 import re 9 import subprocess 10 import sys 11 import tempfile 12 13 import buildconfig 14 import yaml 15 16 17 def shell_main(): 18 parser = argparse.ArgumentParser() 19 parser.add_argument("-o", "--output", type=str, required=True, help="Output file") 20 parser.add_argument("manifest", type=str, help="Manifest source file") 21 args = parser.parse_args() 22 23 with open(args.output, "w") as out_file: 24 process_manifest(out_file, args.manifest) 25 26 27 def main(output_fp, input_filename): 28 return process_manifest(output_fp, input_filename) 29 30 31 HEADER = """// AUTOGENERATED - DO NOT EDIT 32 namespace mozilla { 33 namespace layers { 34 35 struct ShaderBytes { const void* mData; size_t mLength; }; 36 """ 37 FOOTER = """ 38 } // namespace layers 39 } // namespace mozilla""" 40 41 42 def process_manifest(output_fp, manifest_filename): 43 with codecs.open(manifest_filename, "r", "UTF-8") as in_fp: 44 manifest = yaml.safe_load(in_fp) 45 shader_folder, _ = os.path.split(manifest_filename) 46 47 output_fp.write(HEADER) 48 49 deps = set() 50 for block in manifest: 51 if "type" not in block: 52 raise Exception("Expected 'type' key with shader mode") 53 if "file" not in block: 54 raise Exception("Expected 'file' key with shader file") 55 if "shaders" not in block: 56 raise Exception("Expected 'shaders' key with shader name list") 57 58 shader_file = os.path.join(shader_folder, block["file"]) 59 deps.add(shader_file) 60 61 shader_model = block["type"] 62 for shader_name in block["shaders"]: 63 new_deps = run_fxc( 64 shader_model=shader_model, 65 shader_file=shader_file, 66 shader_name=shader_name, 67 output_fp=output_fp, 68 ) 69 deps |= new_deps 70 71 output_fp.write(FOOTER) 72 return deps 73 74 75 def run_fxc(shader_model, shader_file, shader_name, output_fp): 76 fxc_location = buildconfig.substs["FXC"] 77 78 argv = [ 79 fxc_location, 80 "-nologo", 81 f"-T{shader_model}", 82 os.path.relpath(shader_file), 83 f"-E{shader_name}", 84 f"-Vn{shader_name}", 85 "-Vi", 86 ] 87 if "WINNT" not in buildconfig.substs["HOST_OS_ARCH"]: 88 argv.insert(0, buildconfig.substs["WINE"]) 89 if shader_model.startswith("vs_"): 90 argv += ["-DVERTEX_SHADER"] 91 elif shader_model.startswith("ps_"): 92 argv += ["-DPIXEL_SHADER"] 93 94 deps = None 95 with ScopedTempFilename() as temp_filename: 96 argv += [f"-Fh{os.path.relpath(temp_filename)}"] 97 98 sys.stdout.write("{}\n".format(" ".join(argv))) 99 sys.stdout.flush() 100 proc_stdout = subprocess.check_output(argv) 101 proc_stdout = decode_console_text(sys.stdout, proc_stdout) 102 deps = find_dependencies(proc_stdout) 103 assert "fxc2" in fxc_location or len(deps) > 0 104 105 with open(temp_filename) as temp_fp: 106 output_fp.write(temp_fp.read()) 107 108 output_fp.write( 109 f"ShaderBytes s{shader_name} = {{ {shader_name}, sizeof({shader_name}) }};\n" 110 ) 111 return deps 112 113 114 def find_dependencies(fxc_output): 115 # Dependencies look like this: 116 # Resolved to [<path>] 117 # 118 # Microsoft likes to change output strings based on the user's language, so 119 # instead of pattern matching on that string, we take everything in between 120 # brackets. We filter out potentially bogus strings later. 121 deps = set() 122 for line in fxc_output.split("\n"): 123 m = re.search(r"\[([^\]]+)\]", line) 124 if m is None: 125 continue 126 dep_path = m.group(1) 127 dep_path = os.path.normpath(dep_path) 128 # When run via Wine, FXC's output contains Windows paths on the Z drive. 129 # We want to normalize them back to unix paths for the build system. 130 if "WINNT" not in buildconfig.substs[ 131 "HOST_OS_ARCH" 132 ] and dep_path.lower().startswith("z:"): 133 dep_path = dep_path[2:].replace("\\", "/") 134 if os.path.isfile(dep_path): 135 deps.add(dep_path) 136 return deps 137 138 139 # Python reads the raw bytes from stdout, so we need to try our best to 140 # capture that as a valid Python string. 141 142 143 def decode_console_text(pipe, text): 144 try: 145 if pipe.encoding: 146 return text.decode(pipe.encoding, "replace") 147 except Exception: 148 pass 149 try: 150 return text.decode(locale.getpreferredencoding(), "replace") 151 except Exception: 152 return text.decode("utf8", "replace") 153 154 155 # Allocate a temporary file name and delete it when done. We need an extra 156 # wrapper for this since TemporaryNamedFile holds the file open. 157 158 159 class ScopedTempFilename: 160 def __init__(self): 161 self.name = None 162 163 def __enter__(self): 164 with tempfile.NamedTemporaryFile(dir=os.getcwd(), delete=False) as tmp: 165 self.name = tmp.name 166 return self.name 167 168 def __exit__(self, type, value, traceback): 169 if not self.name: 170 return 171 try: 172 os.unlink(self.name) 173 except Exception: 174 pass 175 176 177 if __name__ == "__main__": 178 shell_main()