feat(bench): automate B7 shader profiles, wire B12b trials, settle 45s
- gen_shader_profiles.py + shader-profiles/: Iris per-pack option files resolved from each pack's profile.* lines (Iris has no profile key) - bench.py: flag rows shader_not_loaded when Iris falls back to vanilla rendering (analyze.py treats it as invalid) - matrix: shader_options session key, trial jar globs pinned to the downloaded Modrinth builds, post-join settle 45 s (B0 v1 showed rep 1 still warming at 10 s)
This commit is contained in:
1 parent
a15c912ad0
commit
4fa9a6ab81
13 files changed
+552
-31
No files matched your search
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Iris per-pack option files that reproduce a shaderpack's built-in profile.
|
||||
|
||||
Iris 1.8.x reads `shaderpacks/<pack name>.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())
|
||||
Reference in new issue
Block a user