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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
+18
@@ -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
|
||||
+33
@@ -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()
|
||||
Reference in new issue
Block a user