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:
logan committed 2026-10-16 23:34:52 -04:00
1 parent a15c912ad0
commit 4fa9a6ab81
13 files changed
+552 -31

No files matched your search

+5 -2
View File
@@ -119,8 +119,11 @@ join seconds, block-atlas size, `level.dat` hash.
only reaches Xwayland windows, `windowactivate --sync` hangs under kwin, focus-steal only reaches Xwayland windows, `windowactivate --sync` hangs under kwin, focus-steal
prevention can drop keystrokes, and typed chat lands in whatever has focus. Hence no prevention can drop keystrokes, and typed chat lands in whatever has focus. Hence no
input injection at all in v2. input injection at all in v2.
- Still manual: **S6** (JEI/GUI overlay needs a key press), producing B7 shader option - B7 shader profiles are automated: `gen_shader_profiles.py` resolves each pack's
profiles (export once from Iris' GUI, then pass `--shader-options`), checks needing F3 `profile.NAME` (shaders.properties, Iris ProfileSet semantics) into
`shader-profiles/<pack-stem>.<profile>.txt`; matrix.toml passes it as `shader_options`
-> `--shader-options`. Re-run the generator (or `--check`) after a shaderpack update.
- Still manual: **S6** (JEI/GUI overlay needs a key press), checks needing F3
reads, `/sparkc` heap/GC reads (B1/B4). reads, `/sparkc` heap/GC reads (B1/B4).
- MangoHud's `log_duration=1800` in `~/.local/bin/mangohud-local` caps one CSV at 30 min; - MangoHud's `log_duration=1800` in `~/.local/bin/mangohud-local` caps one CSV at 30 min;
`session` warns when a plan is longer (B5's 45-min decay run needs a split or a larger `session` warns when a plan is longer (B5's 45-min decay run needs a split or a larger
+2 -1
View File
@@ -22,7 +22,8 @@ import statistics
import sys import sys
from pathlib import Path from pathlib import Path
INVALID = {"crashed", "timeout", "no_data", "no_markers", "low_n", "vsync_suspect"} INVALID = {"crashed", "timeout", "no_data", "no_markers", "low_n", "vsync_suspect",
"shader_not_loaded"}
DEFAULT_FILE = Path.home() / "onion-bench" / "results" / "session.jsonl" DEFAULT_FILE = Path.home() / "onion-bench" / "results" / "session.jsonl"
+16 -1
View File
@@ -1066,6 +1066,21 @@ def options_values(game, keys):
return vals return vals
SHADER_FAIL_RE = re.compile(
r"Falling back to normal rendering without shaders|Failed to compile shaders|"
r"Failed to create shader rendering pipeline|Failed to load the shaderpack|"
r"Shaders are disabled because|is not valid! Can't load it")
def shader_flags(shader, lines):
"""['shader_not_loaded'] when a shader was requested but Iris didn't load it."""
if not shader or shader == "off":
return []
ok = any("Using shaderpack: " in ln for ln in lines)
bad = any(SHADER_FAIL_RE.search(ln) for ln in lines)
return ["shader_not_loaded"] if bad or not ok else []
def env_flags(gpus, server_running): def env_flags(gpus, server_running):
"""Warn flags for uncontrolled noise sources.""" """Warn flags for uncontrolled noise sources."""
flags = [] flags = []
@@ -1311,7 +1326,7 @@ def _record_session(a, sid, plan, tail, t_launch, outcome, detail, jvm_flags, cf
"join_s": round(ev["joined_t"] - t_launch, 2) if ev["joined_t"] else None, "join_s": round(ev["joined_t"] - t_launch, 2) if ev["joined_t"] else None,
"atlas": ev["atlas"], "log": log_path, **meta, "mem_available_after_mb": mem_after, "atlas": ev["atlas"], "log": log_path, **meta, "mem_available_after_mb": mem_after,
} }
noise = env_flags(meta["gpus"], meta["server_running"]) noise = env_flags(meta["gpus"], meta["server_running"]) + shader_flags(a.shader, lines)
if restore_failed: if restore_failed:
noise.append("restore_failed") noise.append("restore_failed")
n_ok = 0 n_ok = 0
@@ -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())
+28 -8
View File
@@ -38,13 +38,13 @@ DISPLAYS = {"desktop", "gamescope", "gamescope-headless"}
PARAM_RE = re.compile(r"\$\{(\w+)\}") PARAM_RE = re.compile(r"\$\{(\w+)\}")
TOP_KEYS = {"defaults", "worlds", "trials", "missing_variants", "block"} TOP_KEYS = {"defaults", "worlds", "trials", "missing_variants", "block"}
DEFAULT_KEYS = {"reps", "warmup", "capture", "display", "res", "world", "shader", "dh", DEFAULT_KEYS = {"reps", "warmup", "capture", "settle", "display", "res", "world", "shader", "dh",
"launch_overhead_s"} "launch_overhead_s"}
BLOCK_KEYS = {"id", "title", "rule", "proxy_note", "manual_checks", "sessions", "params", BLOCK_KEYS = {"id", "title", "rule", "proxy_note", "manual_checks", "sessions", "params",
"deferred", "manual", "interleave"} "deferred", "manual", "interleave"}
SESSION_KEYS = {"variant", "plan", "reps", "repeat", "tag", "jvm", "heap", "shader", SESSION_KEYS = {"variant", "plan", "reps", "repeat", "tag", "jvm", "heap", "shader",
"shader_profile", "dh", "disable_mods", "add_mods", "world", "warmup", "shader_profile", "shader_options", "dh", "disable_mods", "add_mods", "world", "warmup",
"capture", "manual", "note", "display", "res"} "capture", "settle", "manual", "note", "display", "res"}
MISSING_KEYS = {"id", "blocks", "contents"} MISSING_KEYS = {"id", "blocks", "contents"}
# Flags `bench.py session` accepts; bench_argv must emit only these. # Flags `bench.py session` accepts; bench_argv must emit only these.
BENCH_SESSION_FLAGS = { BENCH_SESSION_FLAGS = {
@@ -173,7 +173,7 @@ def validate(m, out_dir=OUT_DIR):
def _check_common(s, where, worlds, trials, err): def _check_common(s, where, worlds, trials, err):
for k in ("reps", "warmup", "capture"): for k in ("reps", "warmup", "capture", "settle"):
if k in s and not _pos_int(s[k]): if k in s and not _pos_int(s[k]):
err.append(f"{where}: {k} must be a positive int") err.append(f"{where}: {k} must be a positive int")
if "display" in s and s["display"] not in DISPLAYS: if "display" in s and s["display"] not in DISPLAYS:
@@ -184,6 +184,8 @@ def _check_common(s, where, worlds, trials, err):
err.append(f"{where}: dh must be on/off") err.append(f"{where}: dh must be on/off")
if "world" in s and s["world"] not in worlds: if "world" in s and s["world"] not in worlds:
err.append(f"{where}: world {s['world']!r} not in [worlds]") err.append(f"{where}: world {s['world']!r} not in [worlds]")
if "shader_options" in s and s.get("shader", "off") == "off":
err.append(f"{where}: shader_options needs shader = <pack>")
def block_map(m): def block_map(m):
@@ -247,12 +249,18 @@ def resolve_path(p):
return hits[-1] if hits else None return hits[-1] if hits else None
def shader_options_path(p):
"""shader_options are harness-relative unless absolute / ~."""
p = Path(os.path.expanduser(p))
return p if p.is_absolute() else HERE / p
def bench_argv(m, sess, live_world=False): def bench_argv(m, sess, live_world=False):
"""`bench.py session` argv for one session, plus a list of missing inputs.""" """`bench.py session` argv for one session, plus a list of missing inputs."""
missing = [] missing = []
a = ["session", "--variant", sess["variant"], "--plan", ",".join(sess["plan"]), a = ["session", "--variant", sess["variant"], "--plan", ",".join(sess["plan"]),
"--reps", str(sess.get("reps", 3)), "--label", sess["label"]] "--reps", str(sess.get("reps", 3)), "--label", sess["label"]]
for k in ("warmup", "capture"): for k in ("warmup", "capture", "settle"):
if k in sess: if k in sess:
a += [f"--{k}", str(sess[k])] a += [f"--{k}", str(sess[k])]
for k in ("display", "res", "shader", "dh", "jvm"): for k in ("display", "res", "shader", "dh", "jvm"):
@@ -260,6 +268,11 @@ def bench_argv(m, sess, live_world=False):
a += [f"--{k}", str(sess[k])] a += [f"--{k}", str(sess[k])]
if sess.get("heap"): if sess.get("heap"):
a += ["--heap", str(sess["heap"])] a += ["--heap", str(sess["heap"])]
if sess.get("shader_options"):
so = shader_options_path(sess["shader_options"])
if not so.is_file():
missing.append(f"shader options {so} (run gen_shader_profiles.py)")
a += ["--shader-options", str(so)]
if sess.get("disable_mods"): if sess.get("disable_mods"):
a += ["--disable-mods", ",".join(sess["disable_mods"])] a += ["--disable-mods", ",".join(sess["disable_mods"])]
if sess.get("add_mods"): if sess.get("add_mods"):
@@ -289,11 +302,16 @@ def variant_missing(sess, out_dir=OUT_DIR):
def session_seconds(sess, overhead): def session_seconds(sess, overhead):
reps, w, c = sess.get("reps", 3), sess.get("warmup", 15), sess.get("capture", 30) reps, w, c = sess.get("reps", 3), sess.get("warmup", 15), sess.get("capture", 30)
scenes = sum(S5_SECONDS if p == "s5" else w + c for p in sess["plan"]) * reps scenes = sum(S5_SECONDS if p == "s5" else w + c for p in sess["plan"]) * reps
return overhead + scenes return overhead + sess.get("settle", 10) + scenes
def spec_hash(argv, sess): def spec_hash(argv, sess):
blob = json.dumps({"argv": argv, "manual": bool(sess.get("manual"))}, sort_keys=True) spec = {"argv": argv, "manual": bool(sess.get("manual"))}
if sess.get("shader_options"): # a regenerated profile file means a new spec
so = shader_options_path(sess["shader_options"])
spec["shader_options_sha256"] = (hashlib.sha256(so.read_bytes()).hexdigest()
if so.is_file() else None)
blob = json.dumps(spec, sort_keys=True)
return hashlib.sha256(blob.encode()).hexdigest()[:16] return hashlib.sha256(blob.encode()).hexdigest()[:16]
@@ -501,8 +519,10 @@ def cmd_plan(m, a):
argv, miss = bench_argv(m, s, a.live_world) argv, miss = bench_argv(m, s, a.live_world)
pre = "# MANUAL: " if s.get("manual") else "" pre = "# MANUAL: " if s.get("manual") else ""
print(f"{pre}{fmt_cmd(argv)}") print(f"{pre}{fmt_cmd(argv)}")
if s.get("shader_profile"): if s.get("shader_profile") and not s.get("shader_options"):
print(f"# set Iris profile {s['shader_profile']} for {s['shader']} first") print(f"# set Iris profile {s['shader_profile']} for {s['shader']} first")
elif s.get("shader_profile"):
print(f"# Iris profile {s['shader_profile']} via shader options file")
if s.get("note"): if s.get("note"):
print(f"# note: {s['note']}") print(f"# note: {s['note']}")
if unresolved(s): if unresolved(s):
+22 -18
View File
@@ -11,7 +11,9 @@
# tag distinguishes sessions that share a variant but differ by flags; the # tag distinguishes sessions that share a variant but differ by flags; the
# bench label becomes "<block>.<tag>" so analyze.py groups them apart # bench label becomes "<block>.<tag>" so analyze.py groups them apart
# jvm/heap BENCH-MATRIX JVM set / MaxMemAlloc MB # jvm/heap BENCH-MATRIX JVM set / MaxMemAlloc MB
# shader shaderpack file name or "off"; shader_profile = Iris profile (manual today) # shader shaderpack file name or "off"; shader_profile = the pack's profile name
# shader_options Iris per-pack options file (harness-relative), copied to
# shaderpacks/<pack>.txt; gen_shader_profiles.py builds them from the zips
# dh "on" | "off" (dh-off row of B3) # dh "on" | "off" (dh-off row of B3)
# disable_mods globs matched in the instance mods/ (B12a) # disable_mods globs matched in the instance mods/ (B12a)
# add_mods jar paths/globs added for the session only (B12b) # add_mods jar paths/globs added for the session only (B12b)
@@ -25,30 +27,32 @@
reps = 3 reps = 3
warmup = 15 warmup = 15
capture = 30 capture = 30
settle = 45 # post-join settle; B0 v1 showed rep 1 still warming at settle 10
display = "gamescope-headless" display = "gamescope-headless"
res = "1920x1080" res = "1920x1080"
world = "v1" world = "v1"
shader = "off" # pin: a shader/DH state must never leak from one session to the next shader = "off" # pin: a shader/DH state must never leak from one session to the next
dh = "on" dh = "on"
launch_overhead_s = 60 # launch->join (~28 s measured) + settle 10 s + teardown/restore launch_overhead_s = 60 # launch->join (~31 s measured) + world-zip restore + teardown
[worlds] [worlds]
# BENCH-MATRIX §2. None of these exist yet: S4 (airships) is unbuilt. Until they do, # BENCH-MATRIX §2. Built 2026-10-16 (sha256 in .pi/journal.md). `run --live-world`
# `run --live-world` uses the instance's saves/onionbench as-is (no restore). # uses the instance's saves/onionbench as-is (no restore).
v1 = "~/onion-bench/worlds/bench-world-v1.zip" v1 = "~/onion-bench/worlds/bench-world-v1.zip"
s4-0 = "~/onion-bench/worlds/bench-world-v1-s4-0.zip" s4-0 = "~/onion-bench/worlds/bench-world-v1-s4-0.zip"
s4-1 = "~/onion-bench/worlds/bench-world-v1-s4-1.zip" s4-1 = "~/onion-bench/worlds/bench-world-v1-s4-1.zip"
s4-4 = "~/onion-bench/worlds/bench-world-v1.zip" # the main zip carries all 4 ships s4-4 = "~/onion-bench/worlds/bench-world-v1.zip" # the main zip carries all 4 ships
[trials] [trials]
# B12b trial jars: download the NeoForge 1.21.1 build from Modrinth into this directory. # B12b trial jars (NeoForge 1.21.1, Modrinth, sha512-verified 2026-10-16; see /tmp/pi-trials.md
gnetum = "~/onion-bench/trials/gnetum-*.jar" # or the journal). Each glob matches exactly one file in ~/onion-bench/trials/.
better_block_entities = "~/onion-bench/trials/betterblockentities-*.jar" gnetum = "~/onion-bench/trials/gnetum-3.3.6.jar"
flerovium = "~/onion-bench/trials/flerovium-*.jar" better_block_entities = "~/onion-bench/trials/bbe-neoforge-1.3.4+mc1.21.1.jar"
super_resolution = "~/onion-bench/trials/superresolution-*.jar" flerovium = "~/onion-bench/trials/flerovium-neoforge-1.21.1-1.1.2-all.jar"
ixeris = "~/onion-bench/trials/ixeris-*.jar" super_resolution = "~/onion-bench/trials/super_resolution-neoforge-*-0.9.2-alpha.2+opengl.jar"
iris_flywheel_compat = "~/onion-bench/trials/irisflw-*.jar" ixeris = "~/onion-bench/trials/Ixeris-4.6.8+1.21.1-neoforge.jar"
cull_leaves = "~/onion-bench/trials/cull-leaves-*.jar" iris_flywheel_compat = "~/onion-bench/trials/iris-flywheel-compat-NeoForge-2.4.0.jar"
cull_leaves = "~/onion-bench/trials/cullleaves-neoforge-4.1.1+1.21.1.jar"
# Variants a block needs that build.py does not generate. Sessions using them are # Variants a block needs that build.py does not generate. Sessions using them are
# reported as blocked until they exist in proposed-configs/out/. (C1-fa, C1-fa-ext, # reported as blocked until they exist in proposed-configs/out/. (C1-fa, C1-fa-ext,
@@ -180,14 +184,14 @@ sessions = [
id = "B7" id = "B7"
title = "shaders" title = "shaders"
rule = "Data for the owner's shader policy, no auto-decision. Report the FPS cost per profile and whether ship corruption appears with shadow profiles" rule = "Data for the owner's shader policy, no auto-decision. Report the FPS cost per profile and whether ship corruption appears with shadow profiles"
proxy_note = "Iris profiles are set in Iris' GUI; bench.py --shader only selects the pack, so profile rows stay manual until a profile mechanism exists." proxy_note = "Profiles are reproduced by shader-profiles/*.txt (gen_shader_profiles.py resolves the pack's profile.NAME into an Iris options file); rows run unattended. Visual checks stay manual."
manual_checks = ["Flywheel backend line in F3 (expect off with shaders)", "visual check for Iris #3164 on ships"] manual_checks = ["Flywheel backend line in F3 (expect off with shaders)", "visual check for Iris #3164 on ships (shadow profiles: makeup-medium, compl-medium)", "confirm Iris' shader screen shows the intended profile once per pack"]
sessions = [ sessions = [
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "off", tag = "noshader" }, { variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "off", tag = "noshader" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", shader_profile = "shadowless_medium", tag = "makeup-shadowless", manual = true }, { variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", shader_profile = "shadowless_medium", shader_options = "shader-profiles/MakeUp-UltraFast-9.5f.shadowless_medium.txt", tag = "makeup-shadowless" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", shader_profile = "medium", tag = "makeup-medium", manual = true }, { variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", shader_profile = "medium", shader_options = "shader-profiles/MakeUp-UltraFast-9.5f.medium.txt", tag = "makeup-medium" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "ComplementaryReimagined_r5.9.3.zip", shader_profile = "LOW", tag = "compl-low", manual = true }, { variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "ComplementaryReimagined_r5.9.3.zip", shader_profile = "LOW", shader_options = "shader-profiles/ComplementaryReimagined_r5.9.3.LOW.txt", tag = "compl-low" },
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "ComplementaryReimagined_r5.9.3.zip", shader_profile = "MEDIUM", tag = "compl-medium", manual = true }, { variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "ComplementaryReimagined_r5.9.3.zip", shader_profile = "MEDIUM", shader_options = "shader-profiles/ComplementaryReimagined_r5.9.3.MEDIUM.txt", tag = "compl-medium" },
] ]
[[block]] [[block]]
@@ -0,0 +1,18 @@
# Iris options reproducing profile.LOW of ComplementaryReimagined_r5.9.3.zip
# pack sha256 fed6c879e732cf7fc7bc6570ebb1def43235aa7e96f62859fbdbdbfa0b484b38
# 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)
SHADOW_QUALITY=0
shadowDistance=96.0
WATER_REFLECT_QUALITY=2
BLOCK_REFLECT_QUALITY=1
LIGHTSHAFT_QUALI_DEFINE=0
SSAO_QUALI_DEFINE=2
FXAA_DEFINE=-1
DETAIL_QUALITY=2
CLOUD_QUALITY=2
ANISOTROPIC_FILTER=0
COLORED_LIGHTING=0
WORLD_SPACE_REFLECTIONS=-1
ENTITY_SHADOW=-1
RAIN_PUDDLES=0
@@ -0,0 +1,18 @@
# Iris options reproducing profile.MEDIUM of ComplementaryReimagined_r5.9.3.zip
# pack sha256 fed6c879e732cf7fc7bc6570ebb1def43235aa7e96f62859fbdbdbfa0b484b38
# 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)
SHADOW_QUALITY=2
shadowDistance=128.0
WATER_REFLECT_QUALITY=2
BLOCK_REFLECT_QUALITY=1
LIGHTSHAFT_QUALI_DEFINE=1
SSAO_QUALI_DEFINE=2
FXAA_DEFINE=1
DETAIL_QUALITY=2
CLOUD_QUALITY=2
ANISOTROPIC_FILTER=0
COLORED_LIGHTING=0
WORLD_SPACE_REFLECTIONS=-1
ENTITY_SHADOW=-1
RAIN_PUDDLES=0
@@ -0,0 +1,33 @@
# Iris options reproducing profile.medium of MakeUp-UltraFast-9.5f.zip
# pack sha256 afbf622b983d29ffa5fe5f15197f321d3921915bd44584d85f14bc11b5e0e5e8
# 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)
SHADOW_CASTING=true
SHADOW_QTY_SLIDER=2
SHADOW_DISTANCE_SLIDER=1
REFLECTION_SLIDER=2
DOF=false
DOF_STRENGTH=0.09
AO=1
AOSTEPS=5
AO_STRENGTH=0.70
AA_TYPE=2
MOTION_BLUR=false
REFRACTION=1
SUN_REFLECTION=1
WAVING=1
V_CLOUDS=1
CLOUD_STEPS_AVG=10
CLOUD_VOL_STYLE=0
BLOOM_SAMPLES=3.0
BLOOM=true
CHROMA_ABER=0
VOL_LIGHT=1
CLOUD_REFLECTION=true
WATER_ABSORPTION=0.10
COLOR_SCHEME=1
VANILLA_WATER=false
WATER_COLOR_SOURCE=0
COLORED_SHADOW=true
MOTION_BLUR_SAMPLES=4
MATERIAL_GLOSS=true
@@ -0,0 +1,33 @@
# Iris options reproducing profile.shadowless_medium of MakeUp-UltraFast-9.5f.zip
# pack sha256 afbf622b983d29ffa5fe5f15197f321d3921915bd44584d85f14bc11b5e0e5e8
# 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)
SHADOW_CASTING=false
SHADOW_QTY_SLIDER=1
SHADOW_DISTANCE_SLIDER=0
REFLECTION_SLIDER=1
DOF=false
DOF_STRENGTH=0.09
AO=1
AOSTEPS=5
AO_STRENGTH=0.70
AA_TYPE=0
MOTION_BLUR=false
REFRACTION=1
SUN_REFLECTION=1
WAVING=1
V_CLOUDS=1
CLOUD_STEPS_AVG=10
CLOUD_VOL_STYLE=0
BLOOM_SAMPLES=3.0
BLOOM=false
CHROMA_ABER=0
VOL_LIGHT=0
CLOUD_REFLECTION=false
WATER_ABSORPTION=0.10
COLOR_SCHEME=1
VANILLA_WATER=false
WATER_COLOR_SOURCE=0
COLORED_SHADOW=false
MOTION_BLUR_SAMPLES=4
MATERIAL_GLOSS=true
@@ -271,3 +271,13 @@ class OptionsAndFlags(unittest.TestCase):
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
class ShaderLoaded(unittest.TestCase):
def test_flags(self):
ok = ["[Render thread/INFO] [Iris/]: Using shaderpack: MakeUp-UltraFast-9.5f.zip"]
bad = ok + ["[Render thread/ERROR] [Iris/]: Failed to compile shaders"]
self.assertEqual(bench.shader_flags("off", []), [])
self.assertEqual(bench.shader_flags("X.zip", ok), [])
self.assertEqual(bench.shader_flags("X.zip", []), ["shader_not_loaded"])
self.assertEqual(bench.shader_flags("X.zip", bad), ["shader_not_loaded"])
@@ -238,7 +238,69 @@ class Commands(Fixture):
def test_seconds(self): def test_seconds(self):
b3 = matrix.expand(self.m, matrix.block_map(self.m)["B3"]) b3 = matrix.expand(self.m, matrix.block_map(self.m)["B3"])
self.assertEqual(matrix.session_seconds(b3[0], 60), 60 + 3 * 45 + 3 * 130) self.assertEqual(matrix.session_seconds(b3[0], 60), 60 + 10 + 3 * 45 + 3 * 130)
class ShaderOptions(Fixture):
def b7(self, **extra):
d = tomllib.loads(MINI.format(tmp=self.tmp))
sess = {"variant": "C1", "plan": "s1,s4", "shader": "Pack.zip",
"shader_profile": "low", "tag": "p-low", **extra}
d["block"].append({"id": "B7", "rule": "shaders", "sessions": [sess]})
return d
def test_emitted(self):
f = self.tmp / "Pack.low.txt"
f.write_text("A=1\n")
d = self.b7(shader_options=str(f))
self.assertEqual(matrix.validate(d, self.out), [])
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
argv, miss = matrix.bench_argv(d, s)
self.assertEqual(miss, [])
self.assertEqual(argv[argv.index("--shader") + 1], "Pack.zip")
self.assertEqual(argv[argv.index("--shader-options") + 1], str(f))
self.assertIn("--shader-options", matrix.BENCH_SESSION_FLAGS)
def test_missing_file_and_relative_path(self):
d = self.b7(shader_options="shader-profiles/does-not-exist.txt")
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
argv, miss = matrix.bench_argv(d, s)
self.assertEqual(argv[argv.index("--shader-options") + 1],
str(matrix.HERE / "shader-profiles/does-not-exist.txt"))
self.assertTrue(any("shader options" in x for x in miss))
def test_needs_shader(self):
d = self.b7(shader_options="x.txt", shader="off")
self.assertIn("shader_options needs shader", "\n".join(matrix.validate(d, self.out)))
def test_spec_hash_tracks_file_content(self):
f = self.tmp / "Pack.low.txt"
f.write_text("A=1\n")
d = self.b7(shader_options=str(f))
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
argv, _ = matrix.bench_argv(d, s)
h1 = matrix.spec_hash(argv, s)
f.write_text("A=2\n")
self.assertNotEqual(h1, matrix.spec_hash(argv, s))
# sessions without shader_options keep the original hash formula (resume state)
b0 = matrix.expand(self.m, matrix.block_map(self.m)["B0"])[0]
a0, _ = matrix.bench_argv(self.m, b0)
blob = json.dumps({"argv": a0, "manual": False}, sort_keys=True)
import hashlib
self.assertEqual(matrix.spec_hash(a0, b0),
hashlib.sha256(blob.encode()).hexdigest()[:16])
def test_real_b7_automated(self):
m = matrix.load()
rows = matrix.expand(m, matrix.block_map(m)["B7"])
shaded = [s for s in rows if s["shader"] != "off"]
self.assertEqual(len(shaded), 4)
for s in rows:
self.assertFalse(s.get("manual"), s["label"])
for s in shaded:
argv, miss = matrix.bench_argv(m, s)
self.assertEqual([x for x in miss if "shader options" in x], [], s["label"])
self.assertTrue(Path(argv[argv.index("--shader-options") + 1]).is_file())
class Resume(Fixture): class Resume(Fixture):
@@ -0,0 +1,100 @@
"""Offline tests for gen_shader_profiles.py (synthetic packs; no game)."""
import contextlib
import io
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
import gen_shader_profiles as g # noqa: E402
PROPS = (
"# Profiles\r\n"
" profile.BASE = A=1 !B C D:4 \\\r\n"
" E=x\r\n"
"profile.LOW = profile.BASE A=2 !C\r\n"
"profile.TOP = profile.LOW B E=y\r\n"
"screen=<profile> A B\r\n"
"#if A == 1\r\n"
" program.world0/shadow.enabled=false\r\n"
"#endif\r\n"
)
class Parse(unittest.TestCase):
def test_tokens_and_continuation(self):
p = g.parse_profiles(PROPS)
self.assertEqual(sorted(p), ["BASE", "LOW", "TOP"])
self.assertEqual(p["BASE"], ["A=1", "!B", "C", "D:4", "E=x"])
def test_resolve_semantics(self):
p = g.parse_profiles(PROPS)
opts, prog = g.resolve("BASE", p)
self.assertEqual(opts, {"A": "1", "B": "false", "C": "true", "D": "4", "E": "x"})
self.assertEqual(prog, [])
opts, _ = g.resolve("TOP", p) # includes recurse; later tokens win
self.assertEqual(opts, {"D": "4", "A": "2", "C": "false", "B": "true", "E": "y"})
def test_errors(self):
p = {"X": ["profile.Y"], "Y": ["profile.X"]}
with self.assertRaisesRegex(g.ProfileError, "cycle"):
g.resolve("X", p)
with self.assertRaisesRegex(g.ProfileError, "unknown profile"):
g.resolve("NOPE", g.parse_profiles(PROPS))
with self.assertRaisesRegex(g.ProfileError, "conditional"):
g.parse_profiles("#ifdef MC_OS_MAC\nprofile.M = A=1\n#endif\n")
with self.assertRaisesRegex(g.ProfileError, "escaping"):
g.render({"A": "has space"})
def test_programs_collected(self):
_, prog = g.resolve("P", {"P": ["!program.world0/shadow", "A"]})
self.assertEqual(prog, ["world0/shadow"])
class Generate(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
self.zip = self.tmp / "Pack_1.0.zip"
with zipfile.ZipFile(self.zip, "w") as z:
z.writestr("shaders/shaders.properties", PROPS)
z.writestr("shaders/lib/settings.glsl",
"#define A 1 // [1 2]\n//#define B\n#define C\nconst int D = 4; // [4 8]\n")
def test_file_and_unknowns(self):
text, n, unknown = g.generate(self.zip, "LOW")
self.assertEqual(n, 5)
self.assertEqual(unknown, ["E"])
body = [ln for ln in text.splitlines() if not ln.startswith("#")]
self.assertEqual(body, ["B=false", "D=4", "E=x", "A=2", "C=false"])
self.assertIn("profile.LOW of Pack_1.0.zip", text)
self.assertEqual(g.out_name("Pack_1.0.zip", "LOW"), "Pack_1.0.LOW.txt")
def test_program_disabling_profile_rejected(self):
z2 = self.tmp / "P2.zip"
with zipfile.ZipFile(z2, "w") as z:
z.writestr("shaders/shaders.properties", "profile.X = !program.composite A\n")
with self.assertRaisesRegex(g.ProfileError, "disables programs"):
g.generate(z2, "X")
def test_cli_write_then_check(self):
out = self.tmp / "out"
args = ["--packs", str(self.tmp), "--out", str(out), "--target", "Pack_1.0.zip:TOP"]
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(g.main(args), 0)
self.assertTrue((out / "Pack_1.0.TOP.txt").is_file())
self.assertEqual(g.main(args + ["--check"]), 0)
(out / "Pack_1.0.TOP.txt").write_text("stale\n")
self.assertEqual(g.main(args + ["--check"]), 1)
def test_matrix_targets(self):
t = g.matrix_targets()
self.assertIn(("MakeUp-UltraFast-9.5f.zip", "medium"), t)
self.assertIn(("ComplementaryReimagined_r5.9.3.zip", "LOW"), t)
if __name__ == "__main__":
unittest.main()