summaryrefslogtreecommitdiff
path: root/subprojects/pixpat/pixpat-native/codegen
diff options
context:
space:
mode:
authorTomi Valkeinen <tomi.valkeinen@ideasonboard.com>2026-05-08 17:22:58 +0300
committerTomi Valkeinen <tomi.valkeinen@ideasonboard.com>2026-05-08 17:22:58 +0300
commit4e2b291a4acdc2cbd39f005c88bda363bc06bd34 (patch)
treee90048d5973ad1164b109d575cf577af7daf50be /subprojects/pixpat/pixpat-native/codegen
parent8f94b39040e79eccd9312ed1e467fe8ebfab8860 (diff)
parente0b7d30fd437292c88141fb08d60681870b86c6e (diff)
Merge commit 'e0b7d30fd437292c88141fb08d60681870b86c6e' as 'subprojects/pixpat'
Diffstat (limited to 'subprojects/pixpat/pixpat-native/codegen')
-rw-r--r--subprojects/pixpat/pixpat-native/codegen/gen_pixpat.py267
1 files changed, 267 insertions, 0 deletions
diff --git a/subprojects/pixpat/pixpat-native/codegen/gen_pixpat.py b/subprojects/pixpat/pixpat-native/codegen/gen_pixpat.py
new file mode 100644
index 0000000..36fac3b
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/codegen/gen_pixpat.py
@@ -0,0 +1,267 @@
+#!/usr/bin/env python3
+"""pixpat code generator.
+
+Reads a small user TOML config plus the hand-written catalogs
+(format_catalog.h, pattern_catalog.h — both X-macro lists) and emits
+the per-build capability tables consumed by pixpat.cpp /
+pixpat_convert.cpp / pixpat_pattern.cpp.
+
+Outputs:
+ pixpat_config.h — PIXPAT_FEATURE_PATTERN / _CONVERT defines
+ pixpat_caps.inc — s_format_caps[] indexed by FormatId
+ s_pattern_caps[] indexed by PatternId
+
+The convert dispatch (dispatch_dst_convert / dispatch_src_convert /
+dispatch_convert) and pattern dispatch (try_pattern / try_default
+arms) are hand-written and consume the capability arrays via
+`if constexpr`.
+
+A --query mode prints 0/1 to stdout for use from meson.
+"""
+
+import argparse
+import re
+import sys
+import tomllib
+from pathlib import Path
+
+
+# === Catalog parsers =============================================================
+#
+# Both catalogs are hand-written X-macro lists; we parse them here so
+# that adding a format/pattern is a single-file edit. The macro form
+# is rigid enough that a simple regex over the body works.
+
+
+def _parse_x_macro(path: Path, macro_name: str, row_re: re.Pattern) -> list:
+ """Find `#define <macro_name>(X) ...` and return row_re's captures.
+
+ The macro body extends from the `#define` line through the first
+ line that does not end with a backslash.
+ """
+ lines = path.read_text().splitlines()
+ header_re = re.compile(rf'#define\s+{macro_name}\s*\(\s*X\s*\)')
+ i = 0
+ while i < len(lines) and not header_re.search(lines[i]):
+ i += 1
+ if i == len(lines):
+ raise SystemExit(f'{macro_name} not found in {path}')
+ body_lines: list[str] = []
+ while True:
+ body_lines.append(lines[i])
+ if not lines[i].rstrip().endswith('\\'):
+ break
+ i += 1
+ if i == len(lines):
+ break
+ rows = row_re.findall('\n'.join(body_lines))
+ if not rows:
+ raise SystemExit(f'{macro_name} is empty in {path}')
+ return rows
+
+
+_FORMAT_ROW = re.compile(r'X\(\s*(\w+)\s*\)')
+_PATTERN_ROW = re.compile(r'X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*"([^"]+)"\s*\)')
+
+
+def parse_format_catalog(path: Path) -> list[str]:
+ return _parse_x_macro(path, 'PIXPAT_FORMAT_LIST', _FORMAT_ROW)
+
+
+def parse_pattern_catalog(path: Path) -> list[tuple[str, str, str, str]]:
+ """Return [(label, rgb_type, yuv_type, name), …] from pattern_catalog.h.
+
+ `label` is the C++ identifier doubling as the PatternId enum value
+ and the s_pattern_caps[] index. `rgb_type` and `yuv_type` are the
+ C++ classes implementing the pattern in each color kind, or the
+ sentinel `void` if the pattern has no variant in that kind.
+ `name` is the lowercase string exposed via the C ABI.
+ """
+ return _parse_x_macro(path, 'PIXPAT_PATTERN_LIST', _PATTERN_ROW)
+
+
+VALID_CAPS = {'rw', 'r', 'w', 'off'}
+
+
+# === Resolution =================================================================
+
+
+def resolve(
+ cfg: dict, format_catalog: list[str], pattern_catalog: list[tuple[str, str, str, str]]
+) -> dict:
+ """Combine catalogs + user config; return concrete settings."""
+ features = cfg.get('features', {})
+ have_pattern = bool(features.get('pattern', True))
+ have_convert = bool(features.get('convert', True))
+ default_caps = features.get('default_format_caps', 'rw')
+ if default_caps not in VALID_CAPS:
+ raise SystemExit(f'invalid default_format_caps: {default_caps!r}')
+
+ catalog_names = set(format_catalog)
+ overrides = cfg.get('formats', {}) or {}
+ for name in overrides:
+ if name not in catalog_names:
+ raise SystemExit(f'[formats] override for unknown format: {name!r}')
+ if overrides[name] not in VALID_CAPS:
+ raise SystemExit(f'invalid caps for {name!r}: {overrides[name]!r}')
+
+ formats = []
+ for name in format_catalog:
+ caps = overrides.get(name, default_caps)
+ read = 'r' in caps and caps != 'off'
+ write = 'w' in caps and caps != 'off'
+ # Reading only matters for the convert path; if convert is off
+ # we suppress all reads so unpack_for<Read=false, ...> is the
+ # only instantiation seen and unpack_to_norm never gets
+ # referenced.
+ if not have_convert:
+ read = False
+ formats.append(
+ {
+ 'name': name,
+ 'read': read,
+ 'write': write,
+ }
+ )
+
+ enabled_names = {f['name'] for f in formats if f['read'] or f['write']}
+ hot_pivots = cfg.get('hot_pivots', []) or []
+ for p in hot_pivots:
+ if p not in enabled_names:
+ raise SystemExit(
+ f'hot_pivot {p!r} is not an enabled format (must have read or write enabled)'
+ )
+
+ catalog_names = {n for (_lbl, _rgb, _yuv, n) in pattern_catalog}
+ patterns = cfg.get('patterns', []) or []
+ for p in patterns:
+ if p not in catalog_names:
+ raise SystemExit(f'unknown pattern: {p!r} (known: {sorted(catalog_names)})')
+
+ # An empty pattern list collapses pattern feature to 'off'.
+ have_pattern = have_pattern and bool(patterns)
+
+ return {
+ 'have_pattern': have_pattern,
+ 'have_convert': have_convert,
+ 'hot_pivots': hot_pivots,
+ 'patterns': patterns,
+ 'formats': formats,
+ 'pattern_catalog': pattern_catalog,
+ }
+
+
+# === Emitters ===================================================================
+
+HEADER = '// Auto-generated by gen_pixpat.py. Do not edit by hand.'
+
+
+def emit_config_h(r: dict) -> str:
+ return '\n'.join(
+ [
+ HEADER,
+ '#pragma once',
+ '',
+ f'#define PIXPAT_FEATURE_PATTERN {1 if r["have_pattern"] else 0}',
+ f'#define PIXPAT_FEATURE_CONVERT {1 if r["have_convert"] else 0}',
+ '',
+ ]
+ )
+
+
+def _caps_row(f: dict, hot: set[str]) -> str:
+ cells = [
+ 'true,' if f['read'] else 'false,',
+ 'true,' if f['write'] else 'false,',
+ 'true,' if (f['name'] in hot) and f['read'] else 'false,',
+ 'true' if (f['name'] in hot) and f['write'] else 'false',
+ ]
+ return f'\t{{ {cells[0]:<7}{cells[1]:<7}{cells[2]:<7}{cells[3]:<6}}}, // {f["name"]}'
+
+
+def emit_caps_inc(r: dict) -> str:
+ out = [HEADER, '']
+
+ # Per-format build capabilities, one row per FormatId (catalog
+ # order). The FormatCaps schema and the size sanity-check live in
+ # static source (pixpat_internal.h / pixpat.cpp); this file is pure
+ # data. Consumers pull individual bools via .readable / .writable /
+ # .hot_src / .hot_dst — member accesses on a constexpr array
+ # element are themselves constant expressions, so they work as
+ # `if constexpr` conditions and as non-type template arguments.
+ out += [
+ 'inline constexpr FormatCaps s_format_caps[] = {',
+ '\t// readable writable hot_src hot_dst',
+ ]
+ hot = set(r['hot_pivots'])
+ for f in r['formats']:
+ out.append(_caps_row(f, hot))
+ out.append('};')
+ out.append('')
+
+ # Per-pattern build capabilities, indexed by PatternId. An unknown
+ # or disabled pattern name in pixpat_draw_pattern is an error, so
+ # there's no default-fallback row.
+ user_patterns = set(r['patterns'])
+ out += [
+ 'inline constexpr PatternCaps s_pattern_caps[] = {',
+ '\t// enabled',
+ ]
+ for _label, _rgb, _yuv, str_name in r['pattern_catalog']:
+ enabled = 'true' if str_name in user_patterns else 'false'
+ out.append(f'\t{{ {enabled:<5} }}, // {str_name}')
+ out.append('};')
+ out.append('')
+ return '\n'.join(out)
+
+
+# === CLI ========================================================================
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--config', type=Path, required=True)
+ ap.add_argument('--format-catalog', type=Path, required=True, help='Path to format_catalog.h.')
+ ap.add_argument(
+ '--pattern-catalog', type=Path, required=True, help='Path to pattern_catalog.h.'
+ )
+ ap.add_argument('--out-config-h', type=Path)
+ ap.add_argument('--out-caps-inc', type=Path)
+ ap.add_argument(
+ '--query',
+ type=str,
+ default=None,
+ help='Print 0/1 for: feature_pattern, feature_convert, have_both (= pattern && convert).',
+ )
+ args = ap.parse_args()
+
+ format_catalog = parse_format_catalog(args.format_catalog)
+ pattern_catalog = parse_pattern_catalog(args.pattern_catalog)
+ with args.config.open('rb') as fh:
+ cfg = tomllib.load(fh)
+ r = resolve(cfg, format_catalog, pattern_catalog)
+
+ if args.query:
+ flag = {
+ 'feature_pattern': r['have_pattern'],
+ 'feature_convert': r['have_convert'],
+ 'have_both': r['have_pattern'] and r['have_convert'],
+ }.get(args.query)
+ if flag is None:
+ raise SystemExit(f'unknown query: {args.query!r}')
+ print(1 if flag else 0)
+ return 0
+
+ outs = [
+ (args.out_config_h, emit_config_h(r)),
+ (args.out_caps_inc, emit_caps_inc(r)),
+ ]
+ if any(o[0] is None for o in outs):
+ raise SystemExit('all --out-* flags are required when not in --query mode')
+ for path, content in outs:
+ path.write_text(content)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())