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
prevention can drop keystrokes, and typed chat lands in whatever has focus. Hence no
input injection at all in v2.
- Still manual: **S6** (JEI/GUI overlay needs a key press), producing B7 shader option
profiles (export once from Iris' GUI, then pass `--shader-options`), checks needing F3
- B7 shader profiles are automated: `gen_shader_profiles.py` resolves each pack's
`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).
- 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
+2 -1
View File
@@ -22,7 +22,8 @@ import statistics
import sys
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"
+16 -1
View File
@@ -1066,6 +1066,21 @@ def options_values(game, keys):
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):
"""Warn flags for uncontrolled noise sources."""
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,
"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:
noise.append("restore_failed")
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+)\}")
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"}
BLOCK_KEYS = {"id", "title", "rule", "proxy_note", "manual_checks", "sessions", "params",
"deferred", "manual", "interleave"}
SESSION_KEYS = {"variant", "plan", "reps", "repeat", "tag", "jvm", "heap", "shader",
"shader_profile", "dh", "disable_mods", "add_mods", "world", "warmup",
"capture", "manual", "note", "display", "res"}
"shader_profile", "shader_options", "dh", "disable_mods", "add_mods", "world", "warmup",
"capture", "settle", "manual", "note", "display", "res"}
MISSING_KEYS = {"id", "blocks", "contents"}
# Flags `bench.py session` accepts; bench_argv must emit only these.
BENCH_SESSION_FLAGS = {
@@ -173,7 +173,7 @@ def validate(m, out_dir=OUT_DIR):
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]):
err.append(f"{where}: {k} must be a positive int")
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")
if "world" in s and s["world"] 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):
@@ -247,12 +249,18 @@ def resolve_path(p):
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):
"""`bench.py session` argv for one session, plus a list of missing inputs."""
missing = []
a = ["session", "--variant", sess["variant"], "--plan", ",".join(sess["plan"]),
"--reps", str(sess.get("reps", 3)), "--label", sess["label"]]
for k in ("warmup", "capture"):
for k in ("warmup", "capture", "settle"):
if k in sess:
a += [f"--{k}", str(sess[k])]
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])]
if sess.get("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"):
a += ["--disable-mods", ",".join(sess["disable_mods"])]
if sess.get("add_mods"):
@@ -289,11 +302,16 @@ def variant_missing(sess, out_dir=OUT_DIR):
def session_seconds(sess, overhead):
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
return overhead + scenes
return overhead + sess.get("settle", 10) + scenes
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]
@@ -501,8 +519,10 @@ def cmd_plan(m, a):
argv, miss = bench_argv(m, s, a.live_world)
pre = "# MANUAL: " if s.get("manual") else ""
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")
elif s.get("shader_profile"):
print(f"# Iris profile {s['shader_profile']} via shader options file")
if s.get("note"):
print(f"# note: {s['note']}")
if unresolved(s):
+22 -18
View File
@@ -11,7 +11,9 @@
# tag distinguishes sessions that share a variant but differ by flags; the
# bench label becomes "<block>.<tag>" so analyze.py groups them apart
# 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)
# disable_mods globs matched in the instance mods/ (B12a)
# add_mods jar paths/globs added for the session only (B12b)
@@ -25,30 +27,32 @@
reps = 3
warmup = 15
capture = 30
settle = 45 # post-join settle; B0 v1 showed rep 1 still warming at settle 10
display = "gamescope-headless"
res = "1920x1080"
world = "v1"
shader = "off" # pin: a shader/DH state must never leak from one session to the next
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]
# BENCH-MATRIX §2. None of these exist yet: S4 (airships) is unbuilt. Until they do,
# `run --live-world` uses the instance's saves/onionbench as-is (no restore).
# BENCH-MATRIX §2. Built 2026-10-16 (sha256 in .pi/journal.md). `run --live-world`
# uses the instance's saves/onionbench as-is (no restore).
v1 = "~/onion-bench/worlds/bench-world-v1.zip"
s4-0 = "~/onion-bench/worlds/bench-world-v1-s4-0.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
[trials]
# B12b trial jars: download the NeoForge 1.21.1 build from Modrinth into this directory.
gnetum = "~/onion-bench/trials/gnetum-*.jar"
better_block_entities = "~/onion-bench/trials/betterblockentities-*.jar"
flerovium = "~/onion-bench/trials/flerovium-*.jar"
super_resolution = "~/onion-bench/trials/superresolution-*.jar"
ixeris = "~/onion-bench/trials/ixeris-*.jar"
iris_flywheel_compat = "~/onion-bench/trials/irisflw-*.jar"
cull_leaves = "~/onion-bench/trials/cull-leaves-*.jar"
# B12b trial jars (NeoForge 1.21.1, Modrinth, sha512-verified 2026-10-16; see /tmp/pi-trials.md
# or the journal). Each glob matches exactly one file in ~/onion-bench/trials/.
gnetum = "~/onion-bench/trials/gnetum-3.3.6.jar"
better_block_entities = "~/onion-bench/trials/bbe-neoforge-1.3.4+mc1.21.1.jar"
flerovium = "~/onion-bench/trials/flerovium-neoforge-1.21.1-1.1.2-all.jar"
super_resolution = "~/onion-bench/trials/super_resolution-neoforge-*-0.9.2-alpha.2+opengl.jar"
ixeris = "~/onion-bench/trials/Ixeris-4.6.8+1.21.1-neoforge.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
# reported as blocked until they exist in proposed-configs/out/. (C1-fa, C1-fa-ext,
@@ -180,14 +184,14 @@ sessions = [
id = "B7"
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"
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."
manual_checks = ["Flywheel backend line in F3 (expect off with shaders)", "visual check for Iris #3164 on ships"]
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 (shadow profiles: makeup-medium, compl-medium)", "confirm Iris' shader screen shows the intended profile once per pack"]
sessions = [
{ 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 = "medium", tag = "makeup-medium", manual = true },
{ 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 = "MEDIUM", tag = "compl-medium", 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", 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", 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", shader_options = "shader-profiles/ComplementaryReimagined_r5.9.3.MEDIUM.txt", tag = "compl-medium" },
]
[[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__":
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):
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):
@@ -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()