| import argparse | |
| import glob | |
| import io | |
| import os | |
| import re | |
| import sys | |
| from collections import defaultdict | |
| parser = argparse.ArgumentParser(description=""" | |
| Process a batch of .glsl files. Minify and export them to C++ strings. | |
| Performs the following transformations: | |
| * Strip comments. | |
| * Strip whitespace. | |
| * Strip unused #defines. | |
| * Rename stpq and rgba swizzles to xyzw. | |
| * Rename variables. | |
| - No new name begins with the '_' character, so internal code can begin names with '_' | |
| without fear of renaming collisions. | |
| - GLSL keywords and builtins are not renamed. | |
| - Tokens beginning with '@' have their new name exported to a header file. | |
| - Tokens beginning with '$' are not renamed, with the exception of removing the leading '$'. | |
| "file.glsl" gets exported to: | |
| * "outdir/file.exports.h", with #defines for the rewritten names of each identifier that began | |
| with '@' in the original shader. | |
| * "outdir/file.glsl.cpp", with a global `const char file_glsl[]` in the rive::glsl | |
| namespace that contains the minified shader code. This variable is intentionally declared as a | |
| global in order to generate a link error if the user includes the string more than once in | |
| the build process. | |
| * "outdir/file.minified.glsl" for offline compiling, with all variables renamed except for | |
| exported #defines names (since the offline compiling process will set those defines). | |
| """) | |
| parser.add_argument("files", type=str, nargs="+", help="a list of glsl files") | |
| parser.add_argument("-o", "--outdir", required=True, | |
| help="OUTPUT directory to store the header files") | |
| parser.add_argument("-H", "--human-readable", action='store_true', | |
| help="don't rename or strip out comments or whitespace") | |
| parser.add_argument("-p", "--ply-path", type=str, help="path to ply module") | |
| parser.add_argument("-m", "--msvc", action="store_true", help="set this to generate MSVC-compatible headers") | |
| args = parser.parse_args() | |
| if args.ply_path: | |
| # --ply-path was specified, so add it to the sys path so we can locate the module. | |
| # (if it was not specified we'll assume that it's already reachable via the path) | |
| # Convert posix path to windows | |
| convertedPath = args.ply_path | |
| if sys.platform.startswith('win32') and args.ply_path[:2] == '/c': | |
| convertedPath = 'C:\\' + args.ply_path[2:] | |
| print('Using ply path:' + convertedPath) | |
| sys.path.append(convertedPath) | |
| import ply.lex as lex | |
| # tokens used by PLY to run lexical analysis on our glsl files. | |
| tokens = [ | |
| "DEFINE", | |
| "IFDEF", | |
| "DEFINED_ID", | |
| "TOKEN_PASTE", | |
| "DIRECTIVE", | |
| "LINE_COMMENT", | |
| "BLOCK_COMMENT", | |
| "WHITESPACE", | |
| "OP", | |
| "FLOAT", | |
| "HEX", | |
| "INT", | |
| "ID", | |
| "UNKNOWN", | |
| ] | |
| # tracks which exported identifiers (identifiers whose @name begins with '@') are used as switches | |
| # inside an #ifdef, #if defined(), etc. | |
| exported_switches = set() | |
| # counts the number of times each ID is seen, to prioritize which IDs get the shortest names | |
| all_id_counts = defaultdict(int); | |
| all_id_reference_counts = defaultdict(int); | |
| def parse_id(name, exports, *, is_reference): | |
| all_id_counts[name] += 1 | |
| if is_reference: | |
| all_id_reference_counts[name] += 1 | |
| # identifiers that begin with '@' get exported to C++ through #defines. | |
| if name[0] == '@': | |
| exports.add(name) | |
| # lexing functions used by PLY. | |
| def t_DEFINE(tok): | |
| r"\#[ \t]*define[ \t]+(?P<id>[\@\$]?[A-Za-z_][A-Za-z0-9_]*)(?P<arglist>\((\n|[^\)])*\))?(?P<val>(((\\\n|.)(?!\/[\/\*]))*))?" | |
| tok.define_id = re.match(t_DEFINE.__doc__, tok.value)['id'] | |
| arglist = re.match(t_DEFINE.__doc__, tok.value)['arglist'] | |
| tok.define_arglist = Minifier(arglist, "", tok.lexer.exports) if arglist else None | |
| val = re.match(t_DEFINE.__doc__, tok.value)['val'] | |
| tok.define_val = Minifier(val, "", tok.lexer.exports) if val else None | |
| parse_id(tok.define_id, tok.lexer.exports, is_reference=False) | |
| tok.lexer.lineno += tok.value.count('\n') | |
| return tok | |
| def t_IFDEF(tok): | |
| r"\#[ \t]*(?P<tag>ifn?def)[ \t]+(?P<ifdef_id>[\@\$]?[A-Za-z_][A-Za-z0-9_]*)" | |
| tok.ifdef_tag = re.match(t_IFDEF.__doc__, tok.value)['tag'] | |
| tok.ifdef_id = re.match(t_IFDEF.__doc__, tok.value)['ifdef_id'] | |
| if tok.ifdef_id[0] == '@': | |
| exported_switches.add(tok.ifdef_id) | |
| parse_id(tok.ifdef_id, tok.lexer.exports, is_reference=True) | |
| return tok | |
| def t_DEFINED_ID(tok): | |
| r"defined\((?P<defined_id>[\@\$]?[A-Za-z_][A-Za-z0-9_]*)\)" | |
| tok.defined_id = re.match(t_DEFINED_ID.__doc__, tok.value)['defined_id'] | |
| if tok.defined_id[0] == '@': | |
| exported_switches.add(tok.defined_id) | |
| parse_id(tok.defined_id, tok.lexer.exports, is_reference=True) | |
| return tok | |
| def t_TOKEN_PASTE(tok): | |
| r"\#\#" | |
| return tok | |
| def t_DIRECTIVE(tok): | |
| r"\#[ \t]*(?P<val>(((\\\n|.)(?!\/[\/\*]))*))" | |
| val = re.match(t_DIRECTIVE.__doc__, tok.value)['val'] | |
| tok.directive_val = Minifier(val, "", tok.lexer.exports) if val else None | |
| tok.lexer.lineno += tok.value.count('\n') | |
| return tok | |
| def t_LINE_COMMENT(tok): | |
| r"//(\\\n|.)*" | |
| tok.lexer.lineno += tok.value.count('\n') | |
| return tok | |
| def t_BLOCK_COMMENT(tok): | |
| r"\/\*(\*(?!\/)|[^*])*\*\/" | |
| tok.lexer.lineno += tok.value.count('\n') | |
| return tok | |
| def t_WHITESPACE(tok): | |
| r"(\s|\\\r?\n)+" | |
| tok.lexer.lineno += tok.value.count('\n') | |
| return tok | |
| def t_OP(tok): | |
| r"[~!%^&\*\(\)\-=+\/\[\]{}\?:\<\>\.\,|;]" | |
| return tok | |
| def t_FLOAT(tok): | |
| r"([0-9]*\.[0-9]+|[0-9]+\.)([eE][+\-]?[0-9]+)?|([0-9]+[eE][+\-]?[0-9]+)" | |
| return tok | |
| def t_HEX(tok): | |
| r"0x[0-9a-fA-F]+u?" | |
| return tok | |
| def t_INT(tok): | |
| r"[0-9]+u?" | |
| return tok | |
| def t_ID(tok): | |
| r"[\@\$]?[A-Za-z_][A-Za-z0-9_]*" | |
| parse_id(tok.value, tok.lexer.exports, is_reference=True) | |
| return tok | |
| def t_UNKNOWN(tok): | |
| r"." | |
| return tok | |
| def t_error(tok): | |
| raise Exception("Illegal character '%s' at line %d" % (tok.value[0], tok.lexer.lineno)) | |
| # identifier names that cannot be renamed | |
| glsl_reserved = { | |
| 'EmitStreamVertex', 'EmitVertex', 'EndPrimitive', 'EndStreamPrimitive', | |
| '__VERSION__', '__pixel_localEXT', '__pixel_local_inEXT', | |
| '__pixel_local_outEXT', 'abs', 'acos', 'acosh', 'all', 'allInvocations', | |
| 'allInvocationsEqual', 'any', 'anyInvocation', 'asin', 'asinh', 'atan', | |
| 'atanh', 'atomicAdd', 'atomicAnd', 'atomicCompSwap', 'atomicCounter', | |
| 'atomicCounterAdd', 'atomicCounterAnd', 'atomicCounterCompSwap', | |
| 'atomicCounterDecrement', 'atomicCounterExchange', 'atomicCounterIncrement', | |
| 'atomicCounterMax', 'atomicCounterMin', 'atomicCounterOr', | |
| 'atomicCounterSubtract', 'atomicCounterXor', 'atomicExchange', 'atomicMax', | |
| 'atomicMin', 'atomicOr', 'atomicXor', 'barrier', | |
| 'beginFragmentShaderOrderingINTEL', 'beginInvocationInterlockARB', | |
| 'beginInvocationInterlockNV', 'binding', 'bitCount', 'bitfieldExtract', | |
| 'bitfieldInsert', 'bitfieldReverse', 'blend_support_all_equations', | |
| 'blend_support_colorburn', 'blend_support_colordodge', | |
| 'blend_support_darken', 'blend_support_difference', | |
| 'blend_support_exclusion', 'blend_support_hardlight', | |
| 'blend_support_lighten', 'blend_support_multiply', 'blend_support_overlay', | |
| 'blend_support_screen', 'blend_support_softlight', 'bool', 'break', | |
| 'buffer', 'bvec2', 'bvec3', 'bvec4', 'case', 'ceil', 'centroid', 'clamp', | |
| 'coherent', 'const', 'constant_id', 'continue', 'cos', 'cosh', 'cross', | |
| 'dFdx', 'dFdxCoarse', 'dFdxFine', 'dFdy', 'dFdyCoarse', 'dFdyFine', | |
| 'default', 'defined', 'degrees', 'determinant', 'discard', 'distance', 'do', | |
| 'dot', 'elif', 'else', 'enable', 'endInvocationInterlockARB', | |
| 'endInvocationInterlockNV', 'endif', 'equal', 'exp', 'exp2', 'extension', | |
| 'faceforward', 'false', 'findLSB', 'findMSB', 'flat', 'float', | |
| 'floatBitsToInt', 'floatBitsToUint', 'floor', 'fma', 'for', 'fract', | |
| 'frexp', 'ftransform', 'fwidth', 'fwidthCoarse', 'fwidthFine', | |
| 'greaterThan', 'greaterThanEqual', 'groupMemoryBarrier', 'highp', 'if', | |
| 'iimage2D', 'image2D', 'imageAtomicAdd', 'imageAtomicAnd', | |
| 'imageAtomicCompSwap', 'imageAtomicExchange', 'imageAtomicMax', | |
| 'imageAtomicMin', 'imageAtomicOr', 'imageAtomicXor', 'imageLoad', | |
| 'imageSamples', 'imageSize', 'imageStore', 'imulExtended', 'in', 'inout', | |
| 'input_attachment_index', 'int', 'intBitsToFloat', 'interpolateAtCentroid', | |
| 'interpolateAtOffset', 'interpolateAtSample', 'invariant', 'inverse', | |
| 'inversesqrt', 'isampler2D', 'isampler2DArray', 'isampler3D', | |
| 'isamplerCube', 'isinf', 'isnan', 'ivec2', 'ivec3', 'ivec4', 'layout', | |
| 'ldexp', 'length', 'lessThan', 'lessThanEqual', 'location', 'log', 'log2', | |
| 'lowp', 'main', 'mat2', 'mat2x2', 'mat2x3', 'mat2x4', 'mat3', 'mat3x2', | |
| 'mat3x3', 'mat3x4', 'mat4', 'mat4x2', 'mat4x3', 'mat4x4', 'matrixCompMult', | |
| 'max', 'mediump', 'memoryBarrier', 'memoryBarrierAtomicCounter', | |
| 'memoryBarrierBuffer', 'memoryBarrierImage', 'memoryBarrierShared', 'min', | |
| 'mix', 'mod', 'modf', 'noise1', 'noise2', 'noise3', 'noise4', | |
| 'noperspective', 'normalize', 'not', 'notEqual', 'out', 'outerProduct', | |
| 'packDouble2x32', 'packHalf2x16', 'packSnorm2x16', 'packSnorm4x8', | |
| 'packUnorm2x16', 'packUnorm4x8', 'pixelLocalLoadANGLE', | |
| 'pixelLocalStoreANGLE', 'pow', 'pragma', 'precision', 'push_constant', | |
| 'r16f', 'r32f', 'r32i', 'r32ui', 'radians', 'readonly', 'reflect', | |
| 'refract', 'require', 'return', 'rg16f', 'rgb10_a2', 'rgb_2_yuv', 'rgba8', | |
| 'rgba8i', 'rgba8ui', 'round', 'roundEven', 'sampler', 'sampler2D', | |
| 'sampler2DArray', 'sampler2DArrayShadow', 'sampler2DShadow', 'sampler3D', | |
| 'samplerCube', 'samplerCubeShadow', 'set', 'shadow1D', 'shadow1DLod', | |
| 'shadow1DProj', 'shadow1DProjLod', 'shadow2D', 'shadow2DEXT', 'shadow2DLod', | |
| 'shadow2DProj', 'shadow2DProjEXT', 'shadow2DProjLod', 'sign', 'sin', 'sinh', | |
| 'smooth', 'smoothstep', 'sqrt', 'std140', 'std430', 'step', 'struct', | |
| 'subpassInput', 'subpassInputMS', 'subpassLoad', 'switch', 'tan', 'tanh', | |
| 'texelFetch', 'texelFetchOffset', 'texture', 'texture1D', 'texture1DLod', | |
| 'texture1DProj', 'texture1DProjLod', 'texture2D', 'texture2DGradEXT', | |
| 'texture2DLod', 'texture2DLodEXT', 'texture2DProj', 'texture2DProjGradEXT', | |
| 'texture2DProjLod', 'texture2DProjLodEXT', 'texture2DRect', | |
| 'texture2DRectProj', 'texture3D', 'texture3DLod', 'texture3DProj', | |
| 'texture3DProjLod', 'textureCube', 'textureCubeGradEXT', 'textureCubeLod', | |
| 'textureCubeLodEXT', 'textureGather', 'textureGatherOffset', | |
| 'textureGatherOffsets', 'textureGrad', 'textureGradOffset', 'textureLod', | |
| 'textureLodOffset', 'textureOffset', 'textureProj', 'textureProjGrad', | |
| 'textureProjGradOffset', 'textureProjLod', 'textureProjLodOffset', | |
| 'textureProjOffset', 'textureQueryLevels', 'textureQueryLod', | |
| 'textureSamples', 'textureSize', 'textureVideoWEBGL', 'transpose', 'true', | |
| 'trunc', 'uaddCarry', 'uimage2D', 'uint', 'uintBitsToFloat', 'umulExtended', | |
| 'undef', 'uniform', 'unpackDouble2x32', 'unpackHalf2x16', 'unpackSnorm2x16', | |
| 'unpackSnorm4x8', 'unpackUnorm2x16', 'unpackUnorm4x8', 'usampler2D', | |
| 'usampler2DArray', 'usampler3D', 'usamplerCube', 'usubBorrow', | |
| 'usubpassInput', 'utexture2D', 'uvec2', 'uvec3', 'uvec4', 'vec2', 'vec3', | |
| 'vec4', 'void', 'volatile', 'while', 'yuv_2_rgb' | |
| } | |
| # rgba and stpq get rewritten to xyzw, so we only need to check xyzw here. This way we can keep | |
| # renaming to names like, e.g., "rg". | |
| xyzw_pattern = re.compile(r"^[xyzw]{1,4}$") | |
| # HLSL registers base names can't be overwritten by macro arguments if token pasting (e.g. t##IDX). | |
| hlsl_register_base_names = ['t', 's', 'u', 'b'] | |
| # HLSL registers (e.g., t0, u1) can't be overwritten by a #define. | |
| hlsl_register_pattern = re.compile(r"^[tsub]\d+$") | |
| # can we rename to or from 'name'? | |
| def is_reserved_keyword(name): | |
| return name in glsl_reserved\ | |
| or xyzw_pattern.match(name)\ | |
| or name in hlsl_register_base_names\ | |
| or hlsl_register_pattern.match(name)\ | |
| or name.startswith("$")\ | |
| or name.startswith("gl_")\ | |
| or name.startswith("GL_")\ | |
| or name.startswith("__pixel_local")\ | |
| or name.endswith("ANGLE") | |
| def remove_leading_annotation(name): | |
| if name[0] == '@': | |
| # A leading '@' indicates identifier names that should be exported. Rename '@my_var' to | |
| # '_EXPORTED_my_var' to enforce that '@my_var' and 'my_var' are not interchangeable. | |
| return 'EXPORTED_' + name[1:] | |
| if name[0] == '$': | |
| # A leading '$' indicates identifier names that should not be renamed. | |
| return name[1:] | |
| return name | |
| # Generates new identifier names to rewrite our variables. | |
| class NameGenerator: | |
| def __init__(self, first_letter_chars, additional_letter_chars): | |
| self.first_letter_chars = first_letter_chars | |
| self.additional_letter_chars = additional_letter_chars | |
| self.name_index = 0 | |
| def next_name(self): | |
| i = self.name_index | |
| # Generate the first character from 'self.first_letter_chars' | |
| name = self.first_letter_chars[i % len(self.first_letter_chars)] | |
| i = i // len(self.first_letter_chars) | |
| while i > 0: | |
| # Generate the remaining characters from 'self.additional_letter_chars' | |
| name += self.additional_letter_chars[i % len(self.additional_letter_chars)] | |
| i = i // len(self.additional_letter_chars) | |
| self.name_index = self.name_index + 1 | |
| return name | |
| # Exported variables only use upper case letters in their names. HLSL semantics are not case | |
| # sensitive and may also assign special meaning to numbers. | |
| upper_case_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" | |
| upper_case_name_generator = NameGenerator(upper_case_chars, upper_case_chars + "_") | |
| # Don't begin new names with the the '_' character. Internal code can begin names with '_' without | |
| # fear of renaming collisions. | |
| lower_and_upper_chars = "abcdefghijklmnopqrstuvwxyz" + upper_case_chars | |
| general_name_generator = NameGenerator(lower_and_upper_chars, "_0123456789" + lower_and_upper_chars) | |
| used_new_names = set() | |
| def generate_new_name(*, force_upper_case): | |
| name_generator = upper_case_name_generator if force_upper_case else general_name_generator | |
| while True: | |
| name = name_generator.next_name() | |
| if not is_reserved_keyword(name) and not name in used_new_names: | |
| used_new_names.add(name) | |
| return name | |
| # mapping from original identifiers to new names. | |
| new_names = {} | |
| def generate_new_names(): | |
| for name,count in sorted(all_id_counts.items(), key=lambda x:x[1], reverse=True): | |
| new_name = (remove_leading_annotation(name) | |
| if args.human_readable or is_reserved_keyword(name) | |
| # HLSL semantics are not case sensitive and can assign special meaning to | |
| # numbers. Make all exported names upper case with no numbers. | |
| else generate_new_name(force_upper_case=name[0] == '@')) | |
| new_names[name] = new_name | |
| # used to rewrite rgba and stpq swizzles to xyzw. | |
| rgba_stpq_pattern = re.compile(r"^([rgba]{1,4}|[stpq]{1,4})$") | |
| rgba_stpq_remap = {'r':'x', 'g':'y', 'b':'z', 'a':'w', | |
| 's':'x', 't':'y', 'p':'z', 'q':'w',} | |
| # minifies a single GLSL file. | |
| class Minifier: | |
| def __init__(self, code, basename, exports=set()): | |
| # parse tokens. | |
| lexer = lex.lex() | |
| lexer.exports = exports | |
| lexer.input(code) | |
| self.tokens = [tok for tok in lexer]; | |
| self.exports = exports | |
| self.basename = basename | |
| # Strips unneeded code from the tokens. Called after all Minifiers have been parsed. | |
| def strip_tokens(self): | |
| assert(not args.human_readable) | |
| # strip comments. | |
| self.tokens = \ | |
| [tok for tok in self.tokens if "COMMENT" not in tok.type] | |
| # strip unused defines. | |
| self.tokens = [tok for tok in self.tokens if tok.type != "DEFINE"\ | |
| or all_id_reference_counts[tok.define_id] > 0] | |
| # merge whitespace. | |
| unmerged,self.tokens = self.tokens,[] | |
| for tok in unmerged: | |
| if tok.type == "DEFINE": | |
| if tok.define_arglist != None: | |
| tok.define_arglist.strip_tokens() | |
| if tok.define_val != None: | |
| tok.define_val.strip_tokens() | |
| if tok.type == "DIRECTIVE": | |
| if tok.directive_val != None: | |
| tok.directive_val.strip_tokens() | |
| if (tok.type == "WHITESPACE" | |
| and len(self.tokens) > 0 | |
| and self.tokens[-1].type == "WHITESPACE"): | |
| self.tokens[-1].value += tok.value | |
| else: | |
| self.tokens.append(tok) | |
| # generates rewritten glsl from our tokens. | |
| def emit_tokens_to_rewritten_glsl(self, out, *, preserve_exported_switches, calling_token_type:str=None): | |
| # stand-in for a null token. | |
| lasttoken = lambda : None | |
| lasttoken.type = "" | |
| lasttoken.value = "" | |
| lasttoken_needs_whitespace = False | |
| is_newline = True | |
| for tok in self.tokens: | |
| if tok.type == "WHITESPACE": | |
| if args.human_readable: | |
| out.write(tok.value) | |
| is_newline = tok.value[-1] == '\n' | |
| lasttoken_needs_whitespace = False | |
| continue | |
| is_directive = tok.type in ["DEFINE", "IFDEF", "DIRECTIVE"] | |
| needs_whitespace = tok.type in ["FLOAT", "INT", "HEX", "ID", "DEFINED_ID"] | |
| # Adding this calling_token_type != 'DEFINE' prevents us from adding a new line to stringify macros | |
| if is_directive and not is_newline and calling_token_type != 'DEFINE': | |
| out.write('\n') | |
| elif needs_whitespace and lasttoken_needs_whitespace: | |
| out.write(' ') | |
| # is_newline will be false once we output the token (unless this value otherwise gets | |
| # updated). | |
| is_newline = False | |
| if tok.type == "ID": | |
| if (rgba_stpq_pattern.match(tok.value) | |
| and lasttoken.type == "OP" | |
| and lasttoken.value == "."): | |
| # convert rgba and stpq to xyzw. | |
| out.write(''.join([rgba_stpq_remap[ch] for ch in tok.value])) | |
| else: | |
| self.write_identifier(out, tok.value, preserve_exported_switches) | |
| elif tok.type == "DEFINE": | |
| out.write("#define ") | |
| self.write_identifier(out, tok.define_id, preserve_exported_switches) | |
| if tok.define_arglist != None: | |
| is_newline = tok.define_arglist.emit_tokens_to_rewritten_glsl(\ | |
| out,\ | |
| preserve_exported_switches=preserve_exported_switches,\ | |
| calling_token_type=tok.type) | |
| assert(not is_newline) | |
| if tok.define_val != None: | |
| out.write(' ') | |
| is_newline = tok.define_val.emit_tokens_to_rewritten_glsl(\ | |
| out,\ | |
| preserve_exported_switches=preserve_exported_switches,\ | |
| calling_token_type=tok.type) | |
| elif tok.type == "IFDEF": | |
| out.write('#') | |
| out.write(tok.ifdef_tag) | |
| out.write(' ') | |
| self.write_identifier(out, tok.ifdef_id, preserve_exported_switches) | |
| elif tok.type == "DEFINED_ID": | |
| out.write('defined(') | |
| self.write_identifier(out, tok.defined_id, preserve_exported_switches) | |
| out.write(')') | |
| elif tok.type == "DIRECTIVE": | |
| out.write("#") | |
| if tok.directive_val != None: | |
| is_newline = tok.directive_val.emit_tokens_to_rewritten_glsl(\ | |
| out,\ | |
| preserve_exported_switches=preserve_exported_switches,\ | |
| calling_token_type=tok.type) | |
| else: | |
| out.write(tok.value) | |
| # Since we preserve whitespace in 'human_readable' mode, the newline after a | |
| # preprocessor directive will happen for us automatically unless 'human_readable' is | |
| # false. | |
| if not args.human_readable and is_directive and not is_newline: | |
| out.write('\n') | |
| is_newline = True | |
| lasttoken = tok | |
| lasttoken_needs_whitespace = needs_whitespace | |
| return is_newline | |
| def write_identifier(self, out, identifier, preserve_exported_switches): | |
| if preserve_exported_switches and identifier in exported_switches: | |
| assert(identifier[0] == '@') | |
| out.write(identifier[1:]) | |
| else: | |
| out.write(new_names[identifier]) | |
| def write_exports(self, outdir): | |
| output_path = os.path.join(outdir, f"{self.basename}.exports.h") | |
| print(f"Exporting {output_path} <- {self.basename}") | |
| out = open(output_path, "w", newline='\n') | |
| out.write('#pragma once\n\n') | |
| for exp in sorted(self.exports): | |
| out.write('#define GLSL_%s "%s"\n' % (exp[1:], new_names[exp])) | |
| out.write('#define GLSL_%s_raw %s\n' % (exp[1:], new_names[exp])) | |
| out.close() | |
| def write_embedded_glsl(self, outdir): | |
| output_path = os.path.join(outdir, self.basename + ".hpp") | |
| print("Embedding %s <- %s" % (output_path, self.basename)) | |
| out = open(output_path, "w", newline='\n') | |
| out.write("#pragma once\n\n") | |
| out.write(f'#include "{self.basename}.exports.h"\n\n') | |
| # emit shader code. | |
| root, ext = os.path.splitext(self.basename) | |
| cpp_name = root | |
| if ext != '.glsl': | |
| cpp_name = f"{root}_{ext[1:]}" | |
| out.write("namespace rive {\n") | |
| out.write("namespace gpu {\n") | |
| out.write("namespace glsl {\n") | |
| if args.msvc: | |
| # MSVC cannot compile raw shaders as strings because of an internal | |
| # string length limit. There is, however, no limit on arrays so | |
| # instead write it out as an array of individual characters. | |
| out.write(f'const char {cpp_name}[] = {{\n ') | |
| codeIO = io.StringIO() | |
| self.emit_tokens_to_rewritten_glsl(codeIO, preserve_exported_switches=False) | |
| out_ch_count = 0 | |
| for ch in codeIO.getvalue(): | |
| # use `repr` to escape the characters (but we need to handle single | |
| # quote manually, since it will represent that as "'" - which will | |
| # definitely not compile) | |
| out.write(f" {repr(ch)},") if ch != "'" else out.write(" '\\'',") | |
| out_ch_count = out_ch_count + 1 | |
| CHAR_COUNT_PER_LINE = 12 | |
| if (out_ch_count % CHAR_COUNT_PER_LINE) == 0: | |
| out.write('\n ') | |
| # Null-terminate so the array is a valid C-string when passed to | |
| # APIs like ostream::operator<<(const char*), which read until '\0'. | |
| out.write(" '\\0'\n};\n") | |
| else: | |
| # For non-MSVC outputs just output as a raw string | |
| out.write(f'const char {cpp_name}[] = R"===(') | |
| is_newline = self.emit_tokens_to_rewritten_glsl(out, preserve_exported_switches=False) | |
| if not is_newline: | |
| out.write('\n') | |
| out.write(')===";\n') | |
| out.write("} // namespace glsl\n") | |
| out.write("} // namespace gpu\n") | |
| out.write("} // namespace rive") | |
| out.close() | |
| def write_offline_glsl(self, outdir): | |
| root, ext = os.path.splitext(self.basename) | |
| output_path = os.path.join(outdir, f"{root}.minified{ext}") | |
| print(f"Minifying f{output_path} <- {self.basename}") | |
| out = open(output_path, "w", newline='\n') | |
| self.emit_tokens_to_rewritten_glsl(out, preserve_exported_switches=True) | |
| out.close() | |
| # parse all GLSL files before renaming. This keeps the renaming consistent across files. | |
| minifiers = [Minifier(open(f).read(), os.path.basename(f)) for f in args.files] | |
| generate_new_names() | |
| # minify all GLSL files. | |
| if not os.path.exists(args.outdir): | |
| os.makedirs(args.outdir) | |
| for m in minifiers: | |
| if not args.human_readable: | |
| m.strip_tokens() | |
| m.write_exports(args.outdir) | |
| m.write_embedded_glsl(args.outdir) | |
| m.write_offline_glsl(args.outdir) |