1# Copyright © 2022 Intel Corporation 2# 3# Permission is hereby granted, free of charge, to any person obtaining a 4# copy of this software and associated documentation files (the "Software"), 5# to deal in the Software without restriction, including without limitation 6# the rights to use, copy, modify, merge, publish, distribute, sublicense, 7# and/or sell copies of the Software, and to permit persons to whom the 8# Software is furnished to do so, subject to the following conditions: 9# 10# The above copyright notice and this permission notice (including the next 11# paragraph) shall be included in all copies or substantial portions of the 12# Software. 13# 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20# IN THE SOFTWARE. 21 22# Converts GLSL shader to SPIR-V library 23 24import argparse 25import subprocess 26import sys 27import os 28import typing as T 29 30if T.TYPE_CHECKING: 31 class Arguments(T.Protocol): 32 input: str 33 output: str 34 glslang: str 35 create_entry: T.Optional[str] 36 glsl_ver: T.Optional[str] 37 Olib: bool 38 extra: T.Optional[str] 39 vn: str 40 stage: str 41 includes: T.List[str] 42 defines: T.List[str] 43 depfile: T.Optional[str] 44 45 46def get_args() -> 'Arguments': 47 parser = argparse.ArgumentParser() 48 parser.add_argument('input', help="Name of input file.") 49 parser.add_argument('output', help="Name of output file.") 50 parser.add_argument('glslang', help="path to glslangValidator") 51 52 parser.add_argument("--create-entry", 53 dest="create_entry", 54 help="Create a new entry point and put to the end of a file.") 55 56 parser.add_argument('--glsl-version', 57 dest="glsl_ver", 58 choices=['100', '110', '120', '130', '140', '150', '300es', '310es', '330', '400', '410', '420', '430', '440', '450', '460'], 59 help="Override GLSL #version declaration in source.") 60 61 parser.add_argument("-Olib", 62 action='store_true', 63 help="Any optimizations are disabled and unused functions are saved.") 64 65 parser.add_argument("--extra-flags", 66 dest="extra", 67 help="Pass additional flags to glslangValidator.") 68 69 parser.add_argument("--vn", 70 dest="vn", 71 required=True, 72 help="Variable name. Creates a C header file that contains a uint32_t array.") 73 74 parser.add_argument("--stage", 75 default="vert", 76 choices=['vert', 'tesc', 'tese', 'geom', 'frag', 'comp'], 77 help="Uses specified stage rather than parsing the file extension") 78 79 parser.add_argument("-I", 80 dest="includes", 81 default=[], 82 action='append', 83 help="Include directory") 84 85 parser.add_argument("-D", 86 dest="defines", 87 default=[], 88 action='append', 89 help="Defines") 90 91 parser.add_argument('--depfile', 92 dest="depfile", 93 default=None, 94 action='store', 95 help='Where glslangValidator should write its depfile, if unset no depfile will be written.') 96 97 args = parser.parse_args() 98 return args 99 100 101def create_include_guard(lines: T.List[str], filename: str) -> T.List[str]: 102 filename = filename.replace('.', '_') 103 upper_name = filename.upper() 104 105 guard_head = [f"#ifndef {upper_name}\n", 106 f"#define {upper_name}\n"] 107 guard_tail = [f"\n#endif // {upper_name}\n"] 108 109 # remove '#pragma once' 110 for idx, l in enumerate(lines): 111 if '#pragma once' in l: 112 lines.pop(idx) 113 break 114 115 return guard_head + lines + guard_tail 116 117 118def convert_to_static_variable(lines: T.List[str], varname: str) -> T.List[str]: 119 for idx, l in enumerate(lines): 120 if varname in l: 121 lines[idx] = f"static {l}" 122 return lines 123 raise RuntimeError(f'Did not find {varname}, this is unexpected') 124 125 126def override_version(lines: T.List[str], glsl_version: str) -> T.List[str]: 127 for idx, l in enumerate(lines): 128 if '#version ' in l: 129 lines[idx] = f"#version {glsl_version}\n" 130 return lines 131 raise RuntimeError('Did not find #version directive, this is unexpected') 132 133 134def postprocess_file(args: 'Arguments') -> None: 135 with open(args.output, "r") as r: 136 lines = r.readlines() 137 138 # glslangValidator creates a variable without the static modifier. 139 lines = convert_to_static_variable(lines, args.vn) 140 141 # '#pragma once' is unstandardised. 142 lines = create_include_guard(lines, os.path.basename(r.name)) 143 144 with open(args.output, "w") as w: 145 w.writelines(lines) 146 147 148def preprocess_file(args: 'Arguments', origin_file: T.TextIO, directory: os.PathLike, filemap: T.Dict[str, str]) -> str: 149 if args.create_entry is None and args.glsl_ver is None: 150 return origin_file.name 151 152 with open(os.path.join(directory, os.path.basename(origin_file.name)), "w") as copy_file: 153 lines = origin_file.readlines() 154 155 if args.create_entry is not None: 156 lines.append(f"\nvoid {args.create_entry}() {{}}\n") 157 158 if args.glsl_ver is not None: 159 override_version(lines, args.glsl_ver) 160 161 copy_file.writelines(lines) 162 163 filemap[copy_file.name] = origin_file.name 164 return copy_file.name 165 166 167def fixup_depfile(depfile: str, filemap: T.Mapping[str, str]) -> None: 168 """Replace generated file references in the depfile with their source. 169 """ 170 depends: T.List[str] = [] 171 out: T.Optional[str] = None 172 with open(depfile, 'r') as f: 173 for line in f.readlines(): 174 if ':' in line: 175 out, line = line.split(':', 1) 176 depends.extend(l.strip() for l in line.split()) 177 178 with open(depfile, 'w') as f: 179 f.write(out) 180 f.write(': ') 181 for dep in depends: 182 f.write(filemap.get(dep, dep)) 183 f.write(' ') 184 f.write('\n') 185 186 187def process_file(args: 'Arguments') -> None: 188 filemap: T.Dict[str, str] = {} 189 190 with open(args.input, "r") as infile: 191 copy_file = preprocess_file( 192 args, infile, os.path.dirname(args.output), filemap) 193 194 cmd_list = [args.glslang] 195 196 if args.Olib: 197 cmd_list.append("--keep-uncalled") 198 199 if args.vn is not None: 200 cmd_list.extend(["--variable-name", args.vn]) 201 202 if args.extra is not None: 203 cmd_list.append(args.extra) 204 205 if args.create_entry is not None: 206 cmd_list.extend(["--entry-point", args.create_entry]) 207 208 if args.depfile is not None: 209 cmd_list.extend(['--depfile', args.depfile]) 210 211 for f in args.includes: 212 cmd_list.append('-I' + f) 213 for d in args.defines: 214 cmd_list.append('-D' + d) 215 216 cmd_list.extend([ 217 '-V', 218 '-o', args.output, 219 '-S', args.stage, 220 copy_file, 221 ]) 222 223 ret = subprocess.run(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30) 224 if ret.returncode != 0: 225 print(ret.stdout) 226 print(ret.stderr, file=sys.stderr) 227 sys.exit(1) 228 229 if args.vn is not None: 230 postprocess_file(args) 231 232 if args.create_entry is not None: 233 os.remove(copy_file) 234 235 if args.depfile is not None: 236 fixup_depfile(args.depfile, filemap) 237 238 239def main() -> None: 240 args = get_args() 241 process_file(args) 242 243 244if __name__ == "__main__": 245 main() 246