| 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) |