diff --git a/tools/cg2xml.py b/tools/cg2xml.py new file mode 100755 index 0000000000..fc25e09f1f --- /dev/null +++ b/tools/cg2xml.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 + +""" +Python 3 script which converts simple RetroArch Cg shaders to modern XML/GLSL format. +Author: Hans-Kristian Arntzen (Themaister) +License: Public domain +""" + +import sys +import os +import errno +import subprocess + +batch_mode = False + +def log(*arg): + if not batch_mode: + print(*arg) + +def remove_comments(source_lines): + ret = [] + killed_comments = [line.split('//')[0] for line in source_lines] + for i in filter(lambda line: len(line) > 0, killed_comments): + ret.append(i) + return ret + +def keep_line_if(func, lines): + ret = [] + for i in filter(func, lines): + ret.append(i) + return ret + +def replace_global_vertex(source): + replace_table = [ + ('POSITION', 'rubyVertexCoord'), + ('TEXCOORD0', 'rubyTexCoord'), + ('TEXCOORD', 'rubyTexCoord'), + ('uniform vec4 _modelViewProj1[4]', 'uniform mat4 rubyMVPMatrix'), + ('_modelViewProj1', 'rubyMVPMatrix'), + ('rubyMVPMatrix[0]', 'rubyMVPMatrix_[0]'), + ('rubyMVPMatrix[1]', 'rubyMVPMatrix_[1]'), + ('rubyMVPMatrix[2]', 'rubyMVPMatrix_[2]'), + ('rubyMVPMatrix[3]', 'rubyMVPMatrix_[3]'), + ('_IN1._video_size', 'rubyInputSize'), + ('_IN1._texture_size', 'rubyTextureSize'), + ('_IN1._output_size', 'rubyOutputSize'), + ('input', 'input_dummy'), # 'input' is reserved in GLSL. + ('output', 'output_dummy'), # 'output' is reserved in GLSL. + ] + + for replacement in replace_table: + source = source.replace(replacement[0], replacement[1]) + + return source + +def translate_varyings(varyings, source): + dictionary = {} + for varying in varyings: + for line in source: + if (varying in line) and ('//var' in line): + log('Found line for', varying + ':', line) + dictionary[varying] = line.split(':')[0].split('.')[-1].strip() + break + + return dictionary + +def destructify_varyings(source): + # We have to change varying structs that Cg support to single varyings for GL. Varying structs aren't supported until later versions + # of GLSL. + struct_types = [] + for line in source: + if 'struct' in line: + struct_types.append(line.split(' ')[1]) + + log('Struct types:', struct_types) + + last_struct_decl_line = 0 + varyings = [] + varyings_name = [] + # Find all varyings in structs and make them "global" varyings. + for struct in struct_types: + for i, line in enumerate(source): + if 'struct ' + struct in line: + j = i + 1 + while (j < len(source)) and ('};' not in source[j]): + j += 1 + varyings.extend(['varying ' + string for string in source[i + 1 : j]]) + names = [string.strip().split(' ')[1].split(';')[0].strip() for string in source[i + 1 : j]] + varyings_name.extend(names) + log('Found elements in struct', struct + ':', names) + last_struct_decl_line = j + + varyings_tmp = varyings + varyings = [] + variables = [] + + # Don't include useless varyings like IN.video_size, IN.texture_size, etc as they are not actual varyings ... + for i in filter(lambda v: ('_video_size' not in v) and ('_texture_size' not in v) and '_output_size' not in v, varyings_tmp): + varyings.append(i) + + # Find any global variable struct that is supposed to be the output varying, and redirect all references to it to + # the actual varyings we just declared ... + # Globals only come before main() ... + # Make sure to only look after all struct declarations as there might be overlap. + for line in source[last_struct_decl_line:]: + if 'void main()' in line: + break + + for struct in struct_types: + if struct in line: + variable = line.split(' ')[1].split(';')[0] + log('Found struct variable for', struct + ':', variable) + variables.append(variable) + + varyings_dict = translate_varyings(varyings_name, source) + log('Varyings dict:', varyings_dict) + + # Append all varyings. Keep the structs as they might be used as regular values. + for varying in varyings: + source.insert(1, varying) + + # Replace struct access with global access, e.g. (_co1._c00 => _c00) + # Also replace mangled Cg name with 'real' name. + for index, _ in enumerate(source): + for variable in variables: + source[index] = source[index].replace(variable + '.', ''); + + for index, _ in enumerate(source): + for varying_name in varyings_name: + if varying_name in varyings_dict: + source[index] = source[index].replace(varying_name, varyings_dict[varying_name]) + + return source + + +def hack_source_vertex(source): + transpose_index = 2 + code_index = 0 + for index, line in enumerate(source): + if 'void main()' in line: + source.insert(index + 2, ' mat4 rubyMVPMatrix_ = transpose_(rubyMVPMatrix);') # transpose() is GLSL 1.20+, doesn't exist in GLSL ES 1.0 + source.insert(index, 'uniform vec2 rubyInputSize;') + source.insert(index, 'uniform vec2 rubyTextureSize;') + source.insert(index, 'uniform vec2 rubyOutputSize;') + source.insert(index, """ + mat4 transpose_(mat4 matrix) + { + mat4 ret; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + ret[i][j] = matrix[j][i]; + + return ret; + } + """) + break + + source = destructify_varyings(source) + return source + +def replace_global_fragment(source): + replace_table = [ + ('_IN1._video_size', 'rubyInputSize'), + ('_IN1._texture_size', 'rubyTextureSize'), + ('_IN1._output_size', 'rubyOutputSize'), + ('input', 'input_dummy'), + ('output', 'output_dummy'), # 'output' is reserved in GLSL. + ] + + for replacement in replace_table: + source = source.replace(replacement[0], replacement[1]) + + return source + +def hack_source_fragment(source): + for index, line in enumerate(source): + if 'void main()' in line: + source.insert(index, 'uniform vec2 rubyInputSize;') + source.insert(index, 'uniform vec2 rubyTextureSize;') + source.insert(index, 'uniform vec2 rubyOutputSize;') + break + + for line in source: + if ('TEXUNIT0' in line) and ('semantic' not in line): + sampler = line.split(':')[2].split(' ')[1] + log('Fragment: Sampler:', sampler) + break + + ret = [] + for line in source: + ret.append(line.replace(sampler, 'rubyTexture')) + + ret = destructify_varyings(ret) + return ret + +def validate_shader(source, target): + command = ['cgc', '-noentry', '-ogles'] + p = subprocess.Popen(command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) + stdout_ret, stderr_ret = p.communicate(source.encode()) + + log('Shader:') + log('===') + log(source) + log('===') + log('CGC:', stderr_ret.decode()) + + return p.returncode == 0 + +def convert(source, dest): + vert_cmd = ['cgc', '-profile', 'glesv', '-entry', 'main_vertex', source] + p = subprocess.Popen(vert_cmd, stderr = subprocess.PIPE, stdout = subprocess.PIPE) + vertex_source, stderr_ret = p.communicate() + log(stderr_ret.decode()) + vertex_source = vertex_source.decode() + + if p.returncode != 0: + log('Vertex compilation failed ...') + return 1 + + frag_cmd = ['cgc', '-profile', 'glesf', '-entry', 'main_fragment', source] + p = subprocess.Popen(frag_cmd, stderr = subprocess.PIPE, stdout = subprocess.PIPE) + fragment_source, stderr_ret = p.communicate() + log(stderr_ret.decode()) + fragment_source = fragment_source.decode() + + if p.returncode != 0: + log('Vertex compilation failed ...') + return 1 + + vertex_source = replace_global_vertex(vertex_source) + fragment_source = replace_global_fragment(fragment_source) + + vertex_source = vertex_source.split('\n') + fragment_source = fragment_source.split('\n') + + # Cg think we're using row-major matrices, but we're using column major. + # Also, Cg tends to compile matrix multiplications as dot products in GLSL. + # Hack in a fix for this. + vertex_source = hack_source_vertex(vertex_source) + fragment_source = hack_source_fragment(fragment_source) + + # We compile to GLES, but we really just want modern GL ... + vertex_source = keep_line_if(lambda line: 'precision' not in line, vertex_source) + fragment_source = keep_line_if(lambda line: 'precision' not in line, fragment_source) + + # Kill all comments. Cg adds lots of useless comments. + # Remove first line. It contains the name of the cg program. + vertex_source = remove_comments(vertex_source[1:]) + fragment_source = remove_comments(fragment_source[1:]) + + out_vertex = '\n'.join(vertex_source) + out_fragment = '\n'.join(['#ifdef GL_ES', 'precision mediump float;', '#endif'] + fragment_source) + + if not validate_shader(out_vertex, 'glesv'): + log('Vertex shader does not compile ...') + return 1 + + if not validate_shader(out_fragment, 'glesf'): + log('Fragment shader does not compile ...') + return 1 + + with open(dest, 'w') as f: + f.write('\n') + f.write('\n') + f.write('\n') + f.write(' \n') + + f.write(' \n') + f.write('\n') + + return 0 + +def main(): + if len(sys.argv) != 3: + print('Usage: {} prog.cg prog.shader'.format(sys.argv[0])) + return 1 + + if os.path.isdir(sys.argv[1]): + global batch_mode + batch_mode = True + try: + os.makedirs(sys.argv[2]) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + failed_cnt = 0 + success_cnt = 0 + failed_files = [] + for dirname, _, filenames in os.walk(sys.argv[1]): + for source in filter(lambda path: 'cg' == path.split('.')[-1], [os.path.join(dirname, filename) for filename in filenames]): + + dest = os.path.join(sys.argv[2], source.replace(sys.argv[1], '')[1:]).replace('.cg', '.shader') + dirpath = os.path.split(dest)[0] + print('Dirpath:', dirpath) + if not os.path.isdir(dirpath): + try: + os.makedirs(dirpath) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + try: + ret = convert(source, dest) + print(source, '->', dest, '...', 'suceeded!' if ret == 0 else 'failed!') + + if ret == 0: + success_cnt += 1 + else: + failed_cnt += 1 + failed_files.append(source) + except Exception as e: + print(e) + failed_files.append(source) + failed_cnt += 1 + + print(success_cnt, 'shaders converted successfully.') + print(failed_cnt, 'shaders failed.') + if failed_cnt > 0: + print('Failed shaders:') + for path in failed_files: + print(path) + + else: + source = sys.argv[1] + dest = sys.argv[2] + sys.exit(convert(source, dest)) + +if __name__ == '__main__': + sys.exit(main()) +