summaryrefslogtreecommitdiffstats
path: root/tools/glsl_preproc
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tools/glsl_preproc/macros.py105
-rwxr-xr-xtools/glsl_preproc/main.py17
-rw-r--r--tools/glsl_preproc/meson.build13
-rw-r--r--tools/glsl_preproc/statement.py301
-rw-r--r--tools/glsl_preproc/templates.py14
-rw-r--r--tools/glsl_preproc/templates/call.c.j219
-rw-r--r--tools/glsl_preproc/templates/function.c.j219
-rw-r--r--tools/glsl_preproc/templates/glsl_block.c.j217
-rw-r--r--tools/glsl_preproc/templates/struct.c.j25
-rw-r--r--tools/glsl_preproc/variables.py79
10 files changed, 589 insertions, 0 deletions
diff --git a/tools/glsl_preproc/macros.py b/tools/glsl_preproc/macros.py
new file mode 100644
index 0000000..2ba7e21
--- /dev/null
+++ b/tools/glsl_preproc/macros.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+
+import re
+
+from variables import Var
+from templates import *
+from statement import *
+
+PATTERN_PRAGMA = re.compile(flags=re.VERBOSE, pattern=r'''
+\s*\#\s*pragma\s+ # '#pragma'
+(?P<pragma>(?: # pragma name
+ GLSL[PHF]?
+))\s*
+(?P<rest>.*)$ # rest of line (pragma body)
+''')
+
+# Represents a single #pragma macro
+class Macro(object):
+ PRAGMAS = {
+ 'GLSL': 'SH_BUF_BODY',
+ 'GLSLP': 'SH_BUF_PRELUDE',
+ 'GLSLH': 'SH_BUF_HEADER',
+ 'GLSLF': 'SH_BUF_FOOTER',
+ }
+
+ def __init__(self, linenr=0, type='GLSL'):
+ self.linenr = linenr
+ self.buf = Macro.PRAGMAS[type]
+ self.name = '_glsl_' + str(linenr)
+ self.body = [] # list of statements
+ self.last = None # previous GLSLBlock (if unterminated)
+ self.vars = VarSet()
+
+ def needs_single_line(self):
+ if not self.body:
+ return False
+ prev = self.body[-1]
+ return isinstance(prev, BlockStart) and not prev.multiline
+
+ def push_line(self, line):
+ self.vars.merge(line.vars)
+
+ if isinstance(line, GLSLLine):
+ if self.last:
+ self.last.append(line)
+ elif self.needs_single_line():
+ self.body.append(GLSLBlock(line))
+ else:
+ # start new GLSL block
+ self.last = GLSLBlock(line)
+ self.body.append(self.last)
+ else:
+ self.body.append(line)
+ self.last = None
+
+ def render_struct(self):
+ return STRUCT_TEMPLATE.render(macro=self)
+
+ def render_call(self):
+ return CALL_TEMPLATE.render(macro=self)
+
+ def render_fun(self):
+ return FUNCTION_TEMPLATE.render(macro=self, Var=Var)
+
+ # yields output lines
+ @staticmethod
+ def process_file(lines, strip=False):
+ macro = None
+ macros = []
+
+ for linenr, line_orig in enumerate(lines, start=1):
+ line = line_orig.rstrip()
+
+ # Strip leading spaces, due to C indent. Skip first pragma line.
+ if macro and leading_spaces is None:
+ leading_spaces = len(line) - len(line.lstrip())
+
+ # check for start of macro
+ if not macro:
+ leading_spaces = None
+ if result := re.match(PATTERN_PRAGMA, line):
+ macro = Macro(linenr, type=result['pragma'])
+ line = result['rest'] # strip pragma prefix
+
+ if macro:
+ if leading_spaces:
+ line = re.sub(f'^\s{{1,{leading_spaces}}}', '', line)
+ if more_lines := line.endswith('\\'):
+ line = line[:-1]
+ if statement := Statement.parse(line, strip=strip, linenr=linenr):
+ macro.push_line(statement)
+ if more_lines:
+ continue # stay in macro
+ else:
+ yield macro.render_call()
+ yield '#line {}\n'.format(linenr + 1)
+ macros.append(macro)
+ macro = None
+ else:
+ yield line_orig
+
+ if macros:
+ yield '\n// Auto-generated template functions:'
+ for macro in macros:
+ yield macro.render_fun()
diff --git a/tools/glsl_preproc/main.py b/tools/glsl_preproc/main.py
new file mode 100755
index 0000000..fcfea1f
--- /dev/null
+++ b/tools/glsl_preproc/main.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+
+import sys
+import argparse
+
+from macros import Macro
+
+parser = argparse.ArgumentParser()
+parser.add_argument('input')
+parser.add_argument('output')
+parser.add_argument('-s', '--strip', default=False, action='store_true')
+args = parser.parse_args()
+
+with open(args.input) as infile:
+ with open(args.output, 'w') as outfile:
+ for line in Macro.process_file(infile, strip=args.strip):
+ outfile.write(line)
diff --git a/tools/glsl_preproc/meson.build b/tools/glsl_preproc/meson.build
new file mode 100644
index 0000000..677ef7c
--- /dev/null
+++ b/tools/glsl_preproc/meson.build
@@ -0,0 +1,13 @@
+strip_arg = get_option('debug') ? [] : [ '--strip' ]
+glsl_preproc = [ python, join_paths(meson.current_source_dir(), 'main.py') ] + \
+ strip_arg + [ '@INPUT@', '@OUTPUT@' ]
+glsl_deps = files(
+ 'macros.py',
+ 'statement.py',
+ 'templates.py',
+ 'templates/call.c.j2',
+ 'templates/function.c.j2',
+ 'templates/glsl_block.c.j2',
+ 'templates/struct.c.j2',
+ 'variables.py',
+)
diff --git a/tools/glsl_preproc/statement.py b/tools/glsl_preproc/statement.py
new file mode 100644
index 0000000..8641e94
--- /dev/null
+++ b/tools/glsl_preproc/statement.py
@@ -0,0 +1,301 @@
+import re
+
+from templates import GLSL_BLOCK_TEMPLATE
+from variables import VarSet, slugify
+
+VAR_PATTERN = re.compile(flags=re.VERBOSE, pattern=r'''
+ # long form ${ ... } syntax
+ \${ (?:\s*(?P<type>(?: # optional type prefix
+ ident # identifiers (always dynamic)
+ | (?:(?:const|dynamic)\s+)? # optional const/dynamic modifiers
+ (?:float|u?int) # base type
+ | swizzle # swizzle mask
+ | (?:i|u)?vecType # vector type (for mask)
+ )):)?
+ (?P<expr>[^{}]+)
+ }
+| \$(?P<name>\w+) # reference to captured variable
+| @(?P<var>\w+) # reference to locally defined var
+''')
+
+class FmtSpec(object):
+ def __init__(self, ctype='ident_t', fmtstr='_%hx',
+ wrap_expr=lambda name, expr: expr,
+ fmt_expr=lambda name: name):
+ self.ctype = ctype
+ self.fmtstr = fmtstr
+ self.wrap_expr = wrap_expr
+ self.fmt_expr = fmt_expr
+
+ @staticmethod
+ def wrap_var(type, dynamic=False):
+ if dynamic:
+ return lambda name, expr: f'sh_var_{type}(sh, "{name}", {expr}, true)'
+ else:
+ return lambda name, expr: f'sh_const_{type}(sh, "{name}", {expr})'
+
+ @staticmethod
+ def wrap_fn(fn):
+ return lambda name: f'{fn}({name})'
+
+VAR_TYPES = {
+ # identifiers: get mapped as-is
+ 'ident': FmtSpec(),
+
+ # normal variables: get mapped as shader constants
+ 'int': FmtSpec(wrap_expr=FmtSpec.wrap_var('int')),
+ 'uint': FmtSpec(wrap_expr=FmtSpec.wrap_var('uint')),
+ 'float': FmtSpec(wrap_expr=FmtSpec.wrap_var('float')),
+
+ # constant variables: get printed directly into the source code
+ 'const int': FmtSpec(ctype='int', fmtstr='%d'),
+ 'const uint': FmtSpec(ctype='unsigned', fmtstr='uint(%u)'),
+ 'const float': FmtSpec(ctype='float', fmtstr='float(%f)'),
+
+ # dynamic variables: get loaded as shader variables
+ 'dynamic int': FmtSpec(wrap_expr=FmtSpec.wrap_var('int', dynamic=True)),
+ 'dynamic uint': FmtSpec(wrap_expr=FmtSpec.wrap_var('uint', dynamic=True)),
+ 'dynamic float': FmtSpec(wrap_expr=FmtSpec.wrap_var('float', dynamic=True)),
+
+ # component mask types
+ 'swizzle': FmtSpec(ctype='uint8_t', fmtstr='%s', fmt_expr=FmtSpec.wrap_fn('sh_swizzle')),
+ 'ivecType': FmtSpec(ctype='uint8_t', fmtstr='%s', fmt_expr=FmtSpec.wrap_fn('sh_float_type')),
+ 'uvecType': FmtSpec(ctype='uint8_t', fmtstr='%s', fmt_expr=FmtSpec.wrap_fn('sh_float_type')),
+ 'vecType': FmtSpec(ctype='uint8_t', fmtstr='%s', fmt_expr=FmtSpec.wrap_fn('sh_float_type')),
+}
+
+def stringify(value, strip):
+ end = '\\n"'
+ if strip:
+ end = '"'
+ value = re.sub(r'(?:\/\*[^\*]*\*\/|\/\/[^\n]+|^\s*)', '', value)
+ return '"' + value.replace('\\', '\\\\').replace('"', '\\"') + end
+
+def commentify(value, strip):
+ if strip:
+ return ''
+ return '/*' + value.replace('/*', '[[').replace('*/', ']]') + '*/'
+
+# Represents a statement + its enclosed variables
+class Statement(object):
+ def __init__(self, linenr=0):
+ super().__init__()
+ self.linenr = linenr
+ self.vars = VarSet()
+
+ def add_var(self, ctype, expr, name=None):
+ return self.vars.add_var(ctype, expr, name, self.linenr)
+
+ def render(self):
+ raise NotImplementedError
+
+ @staticmethod
+ def parse(text_orig, **kwargs):
+ raise NotImplementedError
+
+# Represents a single line of GLSL
+class GLSLLine(Statement):
+ class GLSLVar(object): # variable reference
+ def __init__(self, fmt, var):
+ self.fmt = fmt
+ self.var = var
+
+ def __init__(self, text, strip=False, **kwargs):
+ super().__init__(**kwargs)
+ self.refs = []
+ self.strip = strip
+
+ # produce two versions of line, one for printf() and one for append()
+ text = text.rstrip()
+ self.rawstr = stringify(text, strip)
+ self.fmtstr = stringify(re.sub(VAR_PATTERN, self.handle_var, text.replace('%', '%%')), strip)
+
+ def handle_var(self, match):
+ # local @var
+ if match['var']:
+ self.refs.append(match['var'])
+ return '%d'
+
+ # captured $var
+ type = match['type']
+ name = match['name']
+ expr = match['expr'] or name
+ name = name or slugify(expr)
+
+ fmt = VAR_TYPES[type or 'ident']
+ self.refs.append(fmt.fmt_expr(self.add_var(
+ ctype = fmt.ctype,
+ expr = fmt.wrap_expr(name, expr),
+ name = name,
+ )))
+
+ if fmt.ctype == 'ident_t':
+ return commentify(name, self.strip) + fmt.fmtstr
+ else:
+ return fmt.fmtstr
+
+# Represents an entire GLSL block
+class GLSLBlock(Statement):
+ def __init__(self, line):
+ super().__init__(linenr=line.linenr)
+ self.lines = []
+ self.refs = []
+ self.append(line)
+
+ def append(self, line):
+ assert isinstance(line, GLSLLine)
+ self.lines.append(line)
+ self.refs += line.refs
+ self.vars.merge(line.vars)
+
+ def render(self):
+ return GLSL_BLOCK_TEMPLATE.render(block=self)
+
+# Represents a statement which can either take a single line or a block
+class BlockStart(Statement):
+ def __init__(self, multiline=False, **kwargs):
+ super().__init__(**kwargs)
+ self.multiline = multiline
+
+ def add_brace(self, text):
+ if self.multiline:
+ text += ' {'
+ return text
+
+# Represents an @if
+class IfCond(BlockStart):
+ def __init__(self, cond, inner=False, **kwargs):
+ super().__init__(**kwargs)
+ self.cond = cond if inner else self.add_var('bool', expr=cond)
+
+ def render(self):
+ return self.add_brace(f'if ({self.cond})')
+
+# Represents an @else
+class Else(BlockStart):
+ def __init__(self, closing, **kwargs):
+ super().__init__(**kwargs)
+ self.closing = closing
+
+ def render(self):
+ text = '} else' if self.closing else 'else'
+ return self.add_brace(text)
+
+# Represents a normal (integer) @for loop, or an (unsigned 8-bit) bitmask loop
+class ForLoop(BlockStart):
+ def __init__(self, var, op, bound, **kwargs):
+ super().__init__(**kwargs)
+ self.comps = op == ':'
+ self.bound = self.add_var('uint8_t' if self.comps else 'int', expr=bound)
+ self.var = var
+ self.op = op
+
+ def render(self):
+ if self.comps:
+ loopstart = f'uint8_t _mask = {self.bound}, {self.var}'
+ loopcond = f'_mask && ({self.var} = __builtin_ctz(_mask), 1)'
+ loopstep = f'_mask &= ~(1u << {self.var})'
+ else:
+ loopstart = f'int {self.var} = 0'
+ loopcond = f'{self.var} {self.op} {self.bound}'
+ loopstep = f'{self.var}++'
+
+ return self.add_brace(f'for ({loopstart}; {loopcond}; {loopstep})')
+
+# Represents a @switch block
+class Switch(Statement):
+ def __init__(self, expr, **kwargs):
+ super().__init__(**kwargs)
+ self.expr = self.add_var('unsigned', expr=expr)
+
+ def render(self):
+ return f'switch ({self.expr}) {{'
+
+# Represents a @case label
+class Case(Statement):
+ def __init__(self, label, **kwargs):
+ super().__init__(**kwargs)
+ self.label = label
+
+ def render(self):
+ return f'case {self.label}:'
+
+# Represents a @default line
+class Default(Statement):
+ def render(self):
+ return 'default:'
+
+# Represents a @break line
+class Break(Statement):
+ def render(self):
+ return 'break;'
+
+# Represents a single closing brace
+class EndBrace(Statement):
+ def render(self):
+ return '}'
+
+# Shitty regex-based statement parser
+PATTERN_IF = re.compile(flags=re.VERBOSE, pattern=r'''
+@\s*if\s* # '@if'
+(?P<inner>@)? # optional leading @
+\((?P<cond>.+)\)\s* # (condition)
+(?P<multiline>{)?\s* # optional trailing {
+$''')
+
+PATTERN_ELSE = re.compile(flags=re.VERBOSE, pattern=r'''
+@\s*(?P<closing>})?\s* # optional leading }
+else\s* # 'else'
+(?P<multiline>{)?\s* # optional trailing {
+$''')
+
+PATTERN_FOR = re.compile(flags=re.VERBOSE, pattern=r'''
+@\s*for\s+\( # '@for' (
+(?P<var>\w+)\s* # loop variable name
+(?P<op>(?:\<=?|:))(?=[\w\s])\s* # '<', '<=' or ':', followed by \s or \w
+(?P<bound>[^\s].*)\s* # loop boundary expression
+\)\s*(?P<multiline>{)?\s* # ) and optional trailing {
+$''')
+
+PATTERN_SWITCH = re.compile(flags=re.VERBOSE, pattern=r'''
+@\s*switch\s* # '@switch'
+\((?P<expr>.+)\)\s*{ # switch expression
+$''')
+
+PATTERN_CASE = re.compile(flags=re.VERBOSE, pattern=r'''
+@\s*case\s* # '@case'
+(?P<label>[^:]+):? # case label, optionally followed by :
+$''')
+
+PATTERN_BREAK = r'@\s*break;?\s*$'
+PATTERN_DEFAULT = r'@\s*default:?\s*$'
+PATTERN_BRACE = r'@\s*}\s*$'
+
+PARSERS = {
+ PATTERN_IF: lambda r, **kw: IfCond(r['cond'], inner=r['inner'], multiline=r['multiline'], **kw),
+ PATTERN_ELSE: lambda r, **kw: Else(closing=r['closing'], multiline=r['multiline'], **kw),
+ PATTERN_FOR: lambda r, **kw: ForLoop(r['var'], r['op'], r['bound'], multiline=r['multiline'], **kw),
+ PATTERN_SWITCH: lambda r, **kw: Switch(r['expr'], **kw),
+ PATTERN_CASE: lambda r, **kw: Case(r['label'], **kw),
+ PATTERN_BREAK: lambda _, **kw: Break(**kw),
+ PATTERN_DEFAULT: lambda _, **kw: Default(**kw),
+ PATTERN_BRACE: lambda _, **kw: EndBrace(**kw),
+}
+
+def parse_line(text_orig, strip, **kwargs):
+ # skip empty lines
+ text = text_orig.strip()
+ if not text:
+ return None
+ if text.lstrip().startswith('@'):
+ # try parsing as statement
+ for pat, fun in PARSERS.items():
+ if res := re.match(pat, text):
+ return fun(res, **kwargs)
+ # return generic error for unrecognized statements
+ raise SyntaxError('Syntax error in directive: ' + text.lstrip())
+ else:
+ # default to literal GLSL line
+ return GLSLLine(text_orig, strip, **kwargs)
+
+Statement.parse = parse_line
diff --git a/tools/glsl_preproc/templates.py b/tools/glsl_preproc/templates.py
new file mode 100644
index 0000000..b3b6c44
--- /dev/null
+++ b/tools/glsl_preproc/templates.py
@@ -0,0 +1,14 @@
+import jinja2
+import os.path
+
+TEMPLATEDIR = os.path.dirname(__file__) + '/templates'
+TEMPLATES = jinja2.Environment(
+ loader = jinja2.FileSystemLoader(searchpath=TEMPLATEDIR),
+ lstrip_blocks = True,
+ trim_blocks = True,
+)
+
+GLSL_BLOCK_TEMPLATE = TEMPLATES.get_template('glsl_block.c.j2')
+FUNCTION_TEMPLATE = TEMPLATES.get_template('function.c.j2')
+CALL_TEMPLATE = TEMPLATES.get_template('call.c.j2')
+STRUCT_TEMPLATE = TEMPLATES.get_template('struct.c.j2')
diff --git a/tools/glsl_preproc/templates/call.c.j2 b/tools/glsl_preproc/templates/call.c.j2
new file mode 100644
index 0000000..61ee6c0
--- /dev/null
+++ b/tools/glsl_preproc/templates/call.c.j2
@@ -0,0 +1,19 @@
+{
+{% if macro.vars %}
+ const {{ macro.render_struct() }} {{ macro.name }}_args = {
+ {% for var in macro.vars %}
+#line {{ var.linenr }}
+ .{{ var.name }} = {{ var.expr }},
+ {% endfor %}
+ };
+#line {{ macro.linenr }}
+{% endif %}
+ size_t {{ macro.name }}_fn(void *, pl_str *, const uint8_t *);
+{% if macro.vars %}
+ pl_str_builder_append(sh->buffers[{{ macro.buf }}], {{ macro.name }}_fn,
+ &{{ macro.name }}_args, sizeof({{ macro.name }}_args));
+{% else %}
+ pl_str_builder_append(sh->buffers[{{ macro.buf }}], {{ macro.name }}_fn, NULL, 0);
+{% endif %}
+}
+
diff --git a/tools/glsl_preproc/templates/function.c.j2 b/tools/glsl_preproc/templates/function.c.j2
new file mode 100644
index 0000000..9216472
--- /dev/null
+++ b/tools/glsl_preproc/templates/function.c.j2
@@ -0,0 +1,19 @@
+
+size_t {{ macro.name }}_fn(void *alloc, pl_str *buf, const uint8_t *ptr);
+size_t {{ macro.name }}_fn(void *alloc, pl_str *buf, const uint8_t *ptr)
+{
+{% if macro.vars %}
+{{ macro.render_struct() }} {{ Var.STRUCT_NAME }};
+memcpy(&{{ Var.STRUCT_NAME }}, ptr, sizeof({{ Var.STRUCT_NAME }}));
+{% endif %}
+
+{% for statement in macro.body %}
+{{ statement.render() }}
+{% endfor %}
+
+{% if macro.vars %}
+return sizeof({{ Var.STRUCT_NAME }});
+{% else %}
+return 0;
+{% endif %}
+}
diff --git a/tools/glsl_preproc/templates/glsl_block.c.j2 b/tools/glsl_preproc/templates/glsl_block.c.j2
new file mode 100644
index 0000000..aa8372d
--- /dev/null
+++ b/tools/glsl_preproc/templates/glsl_block.c.j2
@@ -0,0 +1,17 @@
+#line {{ block.linenr }}
+{% if block.refs %}
+ pl_str_append_asprintf_c(alloc, buf,
+ {% for line in block.lines %}
+ {{ line.fmtstr }}{{ ',' if loop.last }}
+ {% endfor %}
+ {% for ref in block.refs %}
+ {{ ref }}{{ ',' if not loop.last }}
+ {% endfor %}
+ );
+{% else %}
+ pl_str_append(alloc, buf, pl_str0(
+ {% for line in block.lines %}
+ {{ line.rawstr }}
+ {% endfor %}
+ ));
+{% endif %}
diff --git a/tools/glsl_preproc/templates/struct.c.j2 b/tools/glsl_preproc/templates/struct.c.j2
new file mode 100644
index 0000000..6a6a8fb
--- /dev/null
+++ b/tools/glsl_preproc/templates/struct.c.j2
@@ -0,0 +1,5 @@
+struct __attribute__((__packed__)) {
+{% for var in macro.vars %}
+ {{ var.ctype }} {{ var.name }};
+{% endfor %}
+}
diff --git a/tools/glsl_preproc/variables.py b/tools/glsl_preproc/variables.py
new file mode 100644
index 0000000..187fd79
--- /dev/null
+++ b/tools/glsl_preproc/variables.py
@@ -0,0 +1,79 @@
+import re
+
+def slugify(value):
+ value = re.sub(r'[^\w]+', '_', value.lower()).strip('_')
+ if value[:1].isdigit():
+ value = '_' + value
+ return value
+
+# A single variable (enclosed by the template)
+class Var(object):
+ STRUCT_NAME = 'vars'
+ CSIZES = {
+ # This array doesn't have to be exact, it's only used for sorting
+ # struct members to save a few bytes of memory here and there
+ 'int': 4,
+ 'unsigned': 4,
+ 'float': 4,
+ 'ident_t': 2,
+ 'uint8_t': 1,
+ 'bool': 1,
+ }
+
+ def __init__(self, ctype, expr, name, csize=0, linenr=0):
+ self.ctype = ctype
+ self.csize = csize or Var.CSIZES[ctype]
+ self.expr = expr
+ self.name = name
+ self.linenr = linenr
+
+ def __str__(self):
+ return f'{Var.STRUCT_NAME}.{self.name}'
+
+def is_literal(expr):
+ return expr.isnumeric() or expr in ['true', 'false']
+
+# A (deduplicated) set of variables
+class VarSet(object):
+ def __init__(self):
+ self.varmap = {} # expr -> cvar
+
+ def __iter__(self):
+ # Sort from largest to smallest variable to optimize struct padding
+ yield from sorted(self.varmap.values(),
+ reverse=True,
+ key=lambda v: v.csize,
+ )
+
+ def __bool__(self):
+ return True if self.varmap else False
+
+ def add_var_raw(self, var):
+ # Re-use existing entry for identical expression/type pairs
+ if old := self.varmap.get(var.expr):
+ if var.ctype != old.ctype:
+ raise SyntaxError(f'Conflicting types for expression {var.expr}, '
+ f'got {var.ctype}, expected {old.ctype}')
+ assert old.name == var.name
+ return old
+
+ names = [ v.name for v in self.varmap.values() ]
+ while var.name in names:
+ var.name += '_'
+ self.varmap[var.expr] = var
+ return var
+
+ # Returns the added variable
+ def add_var(self, ctype, expr, name=None, linenr=0):
+ assert expr
+ expr = expr.strip()
+ if is_literal(expr):
+ return expr
+ name = name or slugify(expr)
+
+ var = Var(ctype, expr=expr, name=name, linenr=linenr)
+ return self.add_var_raw(var)
+
+ def merge(self, other):
+ for var in other:
+ self.add_var_raw(var)