#!/usr/bin/env python3 """Generate Iris per-pack option files that reproduce a shaderpack's built-in profile. Iris 1.8.x reads `shaderpacks/.txt` (java.util.Properties) when it loads a pack and applies the key=value pairs as option overrides; there is no "selected profile" key - the GUI's profile button just writes the profile's option values, and the profile shown is re-derived by matching values (ProfileSet.scan). So a profile is reproduced by writing its resolved option list. bench.py --shader-options copies the file into place for one session. Profile syntax (shaders/shaders.properties, mirrored from Iris ProfileSet.parse): profile.NAME = tok tok ... whitespace-separated, applied in order, later wins profile.OTHER include another profile !program.X disable a program (cannot be expressed in the options file -> error) !opt opt=false opt=val/opt:val opt=val opt opt=true python3 gen_shader_profiles.py # (re)write every B7 profile in matrix.toml python3 gen_shader_profiles.py --check # exit 1 if any file is missing/stale """ import argparse import hashlib import re import sys import tomllib import zipfile from pathlib import Path HERE = Path(__file__).resolve().parent OUT = HERE / "shader-profiles" PACKS = Path.home() / "onion-bench" / "prism" / "instances" / "OnionBench" / ".minecraft" / "shaderpacks" MATRIX = HERE / "matrix.toml" IF_RE = re.compile(r"^\s*#\s*if(n?def)?\b") ENDIF_RE = re.compile(r"^\s*#\s*endif\b") NAME_RE = re.compile(r"^[A-Za-z0-9_.\-]+$") SRC_EXT = (".glsl", ".fsh", ".vsh", ".gsh", ".csh", ".tcs", ".tes", ".inc") DEFINE_RE = re.compile(r"^\s*(?://\s*)?#define\s+(\w+)", re.M) CONST_RE = re.compile(r"\bconst\s+\w+\s+(\w+)\s*=") class ProfileError(Exception): pass def logical_lines(text): """Java-properties logical lines: CRLF normalised, trailing-backslash continuation.""" out, buf = [], "" for raw in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"): line = raw if not buf else raw.lstrip() if line.endswith("\\") and not line.endswith("\\\\"): buf += line[:-1] continue out.append(buf + line) buf = "" if buf: out.append(buf) return out def parse_profiles(text): """{name: [tokens]} for every profile.NAME line. Profiles inside preprocessor conditionals can't be evaluated without the pack's macro environment -> error.""" profiles, depth = {}, 0 for line in logical_lines(text): s = line.strip() if IF_RE.match(s): depth += 1 continue if ENDIF_RE.match(s): depth = max(0, depth - 1) continue if not s or s[0] in "#!": continue m = re.match(r"([^=:\s]+)\s*[=:]\s*(.*)$", s) if not m or not m.group(1).startswith("profile."): continue name = m.group(1)[len("profile."):] if depth: raise ProfileError(f"profile.{name} is inside a preprocessor conditional") profiles[name] = m.group(2).split() return profiles def resolve(name, profiles, _stack=()): """(options dict in application order, disabled programs) for one profile.""" if name not in profiles: raise ProfileError(f"unknown profile {name!r} (have {sorted(profiles)})") if name in _stack: raise ProfileError("profile cycle: " + " -> ".join((*_stack, name))) opts, programs = {}, [] for tok in profiles[name]: if tok.startswith("!program."): programs.append(tok[len("!program."):]) elif tok.startswith("profile."): sub, subprog = resolve(tok[len("profile."):], profiles, (*_stack, name)) for k, v in sub.items(): opts.pop(k, None) opts[k] = v programs += subprog else: if tok.startswith("!"): k, v = tok[1:], "false" elif "=" in tok: k, v = tok.split("=", 1) elif ":" in tok: k, v = tok.split(":", 1) else: k, v = tok, "true" opts.pop(k, None) opts[k] = v return opts, programs def render(opts, header=()): lines = [f"# {h}" for h in header] for k, v in opts.items(): if not NAME_RE.match(k) or not v or any(c.isspace() for c in v) or "\\" in v: raise ProfileError(f"option {k}={v!r} would need properties escaping") lines.append(f"{k}={v}") return "\n".join(lines) + "\n" def pack_sources(zf): props = [n for n in zf.namelist() if n.endswith("shaders/shaders.properties")] if len(props) != 1: raise ProfileError(f"expected one shaders/shaders.properties, found {props}") return zf.read(props[0]).decode("utf-8", "replace") def known_options(zf): names = set() for n in zf.namelist(): if n.endswith(SRC_EXT): t = zf.read(n).decode("utf-8", "replace") names |= set(DEFINE_RE.findall(t)) | set(CONST_RE.findall(t)) return names def out_name(pack, profile): stem = pack[:-4] if pack.endswith(".zip") else pack return f"{stem}.{profile}.txt" def generate(pack_path, profile): """(file text, option count, unknown option names) for one pack/profile.""" pack_path = Path(pack_path) with zipfile.ZipFile(pack_path) as zf: opts, programs = resolve(profile, parse_profiles(pack_sources(zf))) if programs: raise ProfileError(f"{profile} disables programs {programs}; an options file " "cannot express that") unknown = sorted(set(opts) - known_options(zf)) sha = hashlib.sha256(pack_path.read_bytes()).hexdigest() header = [f"Iris options reproducing profile.{profile} of {pack_path.name}", f"pack sha256 {sha}", "generated by gen_shader_profiles.py - do not edit; Iris rewrites the live", "copy keeping only non-default values (bench.py journals and restores it)"] return render(opts, header), len(opts), unknown def matrix_targets(path=MATRIX): """(pack, profile) for every session in matrix.toml with a shader_profile.""" with open(path, "rb") as f: m = tomllib.load(f) seen = [] for b in m.get("block", []): for s in b.get("sessions", []): if s.get("shader_profile"): t = (s["shader"], s["shader_profile"]) if t not in seen: seen.append(t) return seen def main(argv=None): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--packs", type=Path, default=PACKS, help="shaderpacks directory") ap.add_argument("--out", type=Path, default=OUT) ap.add_argument("--target", action="append", metavar="PACK:PROFILE", help="instead of the matrix.toml B7 rows") ap.add_argument("--check", action="store_true", help="verify files are current, write nothing") a = ap.parse_args(argv) targets = ([tuple(t.rsplit(":", 1)) for t in a.target] if a.target else matrix_targets()) rc = 0 for pack, profile in targets: text, n, unknown = generate(a.packs / pack, profile) dst = a.out / out_name(pack, profile) note = f" (not found in pack sources: {', '.join(unknown)})" if unknown else "" if a.check: ok = dst.is_file() and dst.read_text() == text print(f"{'ok ' if ok else 'STALE'} {dst.name}: {n} options{note}") rc |= 0 if ok else 1 else: a.out.mkdir(parents=True, exist_ok=True) dst.write_text(text) print(f"wrote {dst.relative_to(HERE) if dst.is_relative_to(HERE) else dst}: " f"{n} options{note}") return rc if __name__ == "__main__": sys.exit(main())