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