feat(perf): add one-step tier setup script for existing players
Default Options only seeds configs when absent, so existing players never
got the tuned settings. build.py --ship now also writes the Deck/Mid/High
tier files to config/onion-perf/<tier>/, and onion-perf-setup.{sh,ps1,bat}
(installed into the instance folder) backs up and replaces the tier's
config files and merges only the tuned video keys into options.txt,
keeping keybinds, audio and vsync/max-FPS choices.
PLAYER-TUNING: point players at the script; bench B4 GC verdict (G1 for
everyone; ZGC/Shenandoah tested worse); 16 GB PCs 5-6 GB heap.
This commit is contained in:
1 parent
cf7ea5617e
commit
f949c169a2
34 files changed
+4348
-22
No files matched your search
@@ -1,7 +1,7 @@
|
||||
# Onion MC: performance settings for players
|
||||
|
||||
These are things the pack can't set for you, plus the settings to change for your machine.
|
||||
Values marked *(provisional)* are waiting on Phase 2 benchmarks and may change.
|
||||
All values below are final (benchmarked in Phase 2; see `RESULTS-proxy.md` in this folder).
|
||||
|
||||
## 1. Which tier are you?
|
||||
|
||||
@@ -12,24 +12,34 @@ Values marked *(provisional)* are waiting on Phase 2 benchmarks and may change.
|
||||
| **High** | 8 GB+ VRAM card | GTX 1080 Ti, RTX 3060+ |
|
||||
|
||||
The pack's defaults are the **mid** tier. They're applied only when the config file doesn't
|
||||
exist yet. **If you played before the defaults shipped**, close the game and delete these
|
||||
files from the instance folder to pick them up on the next launch. This resets those mods'
|
||||
settings.
|
||||
exist yet. **If you played before the defaults shipped**, close Minecraft and run the
|
||||
tier setup script in the instance folder to apply a tier over your current settings:
|
||||
|
||||
- **Windows:** double-click `onion-perf-setup.bat` in the instance folder.
|
||||
- **Linux / Steam Deck / macOS:** run `sh onion-perf-setup.sh` in the instance folder.
|
||||
|
||||
Pick your tier when prompted. The script backs up everything it changes (to
|
||||
`onion-perf-backup/<date-time>/`), replaces these 7 config files (the **deck** tier also
|
||||
ships `config/sodium-options.json`; **high** has the same 7 as mid), and merges the tuned
|
||||
video keys into `options.txt` — your keybinds, audio and every other setting stay as
|
||||
they are. Nothing else is touched.
|
||||
|
||||
Manual fallback: copy the files below from `config/onion-perf/<tier>/` in the instance
|
||||
folder over your own configs, and set the `options.txt` values from the table below by
|
||||
hand (don't copy a tier's `options.txt` over yours — that resets all your video, audio
|
||||
and control settings, keybinds included):
|
||||
|
||||
```
|
||||
config/DistantHorizons.toml
|
||||
config/sodium-options.json
|
||||
config/entity_model_features.json
|
||||
config/flywheel-client.toml
|
||||
config/lambdynlights.toml
|
||||
config/ssrd-common.toml
|
||||
config/sound_physics_remastered/soundphysics.properties
|
||||
config/sound_physics_remastered/sound_rates.properties
|
||||
```
|
||||
|
||||
Don't delete `options.txt`: that resets all your video, audio and control settings,
|
||||
keybinds included. Set the video values in the table below by hand instead.
|
||||
|
||||
Then change these for your tier *(provisional)*:
|
||||
The whole tier table below is applied by the script:
|
||||
|
||||
| Setting | Where | Deck | Mid | High |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -55,25 +65,23 @@ each GC pause longer. Bench (B4h): **5 GB is enough; 4 GB is measurably worse**
|
||||
| Your machine | Max memory (`-Xmx`) | Min memory |
|
||||
| --- | --- | --- |
|
||||
| Steam Deck (16 GB shared with the GPU) | **5 GB**, never above 6 | leave low (512 MB) |
|
||||
| 16 GB PC | **6 GB** | leave low |
|
||||
| 16 GB PC | **5–6 GB** | leave low |
|
||||
| 32 GB PC | **6–8 GB** | may equal max |
|
||||
| PC with little free RAM (other apps/containers running) | min(6 GB, free RAM − 2 GB) | leave low |
|
||||
|
||||
**JVM arguments** *(provisional; Phase 2 block B4 picks the winner)*. Paste one line into
|
||||
*Java arguments* (Prism adds `-Xmx` itself from the memory box):
|
||||
**JVM arguments** — bench block B4 settled this: the client-tuned G1 line below and the
|
||||
phase-0 G1 set tied (S5 tour 1 % low 226.8 vs 228.1 fps), while Generational ZGC and
|
||||
Shenandoah were measurably worse on the tour scene (1 % low 216.6 and 173.4 vs 226.8 fps).
|
||||
So: **the G1 line below, for everyone.** Paste it into *Java arguments* (Prism adds
|
||||
`-Xmx` itself from the memory box):
|
||||
|
||||
- **Default (all machines):** client-tuned G1
|
||||
```
|
||||
-XX:+UseG1GC -XX:MaxGCPauseMillis=37 -XX:G1HeapRegionSize=16M -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=23 -XX:G1ReservePercent=20 -XX:G1MixedGCCountTarget=3 -XX:G1HeapWastePercent=20 -XX:InitiatingHeapOccupancyPercent=10 -XX:G1RSetUpdatingPauseTimePercent=0 -XX:MaxTenuringThreshold=1 -XX:GCTimeRatio=99 -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem
|
||||
```
|
||||
- **32 GB machines, if you still get stutter:** Generational ZGC. Distant Horizons
|
||||
recommends it in its log warning about G1.
|
||||
```
|
||||
-XX:+UseZGC -XX:+ZGenerational -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem
|
||||
```
|
||||
`-XX:+ZGenerational` is required: on Java 21, `-XX:+UseZGC` alone selects the older
|
||||
non-generational ZGC. Avoid ZGC on the Deck or on low-RAM PCs. It needs spare memory and
|
||||
CPU those machines don't have.
|
||||
- Don't switch to Generational ZGC — not even on 32 GB machines. Distant Horizons
|
||||
recommends it in a log warning about G1, but here it **tested worse** (tour 1 % low
|
||||
216.6 fps vs 226.8 for G1; Shenandoah was worse still at 173.4).
|
||||
|
||||
Don't add `-XX:+AlwaysPreTouch` on the Deck or on low-RAM PCs. Don't copy
|
||||
`-XX:ShenandoahGCMode=iu` from older guides: Java 21 refuses to start with it.
|
||||
|
||||
@@ -11,7 +11,8 @@ pinned jar's source (see README.md, "Key provenance").
|
||||
Output: out/<variant>/<path relative to the game dir>, plus out/MANIFEST.md.
|
||||
|
||||
python3 build.py # from this directory; stdlib only
|
||||
python3 build.py --ship # also copy T-default into the repo (see SHIP), then
|
||||
python3 build.py --ship # also copy T-default into the repo (see SHIP) and the
|
||||
# player tiers into config/onion-perf/ (see PERF), then
|
||||
# run `packwiz refresh` from the repo root
|
||||
"""
|
||||
import json
|
||||
@@ -48,6 +49,15 @@ SHIP = {
|
||||
"config/modernfix-mixins.properties": "config/modernfix-mixins.properties",
|
||||
}
|
||||
|
||||
# Player tiers `--ship` also writes to config/onion-perf/<tier>/ (paths relative to
|
||||
# the game dir) for onion-perf-setup.sh/.ps1, which existing players run to apply a
|
||||
# tier over their configs. A tier carries only the files its layers tune, so
|
||||
# untuned files (sable-client.toml; sodium-options.json except on deck) keep the
|
||||
# player's own settings. modernfix-mixins.properties is enforced by packwiz already.
|
||||
PERF = "config/onion-perf"
|
||||
PERF_TIERS = {"deck": "T-deck", "mid": "T-default", "high": "T-high"} # mid == pack default
|
||||
PERF_SKIP = {"config/modernfix-mixins.properties"}
|
||||
|
||||
# game-dir path -> (base file, format, may_append_new_keys)
|
||||
FILES = {
|
||||
"options.txt": (TEMPLATES / "options.txt", "options", False),
|
||||
@@ -384,6 +394,15 @@ def main():
|
||||
(REPO / dst).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(OUT / "T-default" / src, REPO / dst)
|
||||
print(f"shipped {len(SHIP)} files to {REPO / DO}; now run `packwiz refresh`")
|
||||
shutil.rmtree(REPO / PERF, ignore_errors=True)
|
||||
for tier, vid in PERF_TIERS.items():
|
||||
layers, _ = VARIANTS[vid]
|
||||
names = sorted({n for layer in layers for n in layer} - PERF_SKIP)
|
||||
for name in names:
|
||||
dst = REPO / PERF / tier / name
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(OUT / vid / name, dst)
|
||||
print(f"shipped tier {tier} ({vid}): {len(names)} files to {REPO / PERF / tier}")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,237 @@
|
||||
"""Tests for onion-perf-setup.sh (and .ps1 when pwsh exists) against temp instance dirs.
|
||||
|
||||
Needs `python3 build.py --ship` to have generated config/onion-perf/ in the repo.
|
||||
Run: python3 -m unittest discover -s analysis/client-perf/proposed-configs/tests
|
||||
"""
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[4]
|
||||
PERF = REPO / "config" / "onion-perf"
|
||||
SCRIPTS = ["onion-perf-setup.sh", "onion-perf-setup.ps1", "onion-perf-setup.bat"]
|
||||
|
||||
OLD_OPTIONS = (
|
||||
"version:3955\n"
|
||||
"maxFps:144\n"
|
||||
"enableVsync:false\n"
|
||||
"renderDistance:12\n"
|
||||
"mipmapLevels:4\n"
|
||||
"soundCategory_master:0.5\n"
|
||||
"key_key.attack:key.mouse.left\n"
|
||||
"key_key.jump:key.keyboard.space\n"
|
||||
'resourcePacks:["vanilla","file/MyPack.zip"]\n'
|
||||
)
|
||||
|
||||
|
||||
def runners():
|
||||
"""(name, argv prefix) for every available POSIX-sh implementation, plus pwsh."""
|
||||
out = [("sh", ["sh"])]
|
||||
if shutil.which("dash"):
|
||||
out.append(("dash", ["dash"]))
|
||||
if shutil.which("zsh"):
|
||||
out.append(("zsh-emulate-sh", ["zsh", "--emulate", "sh"]))
|
||||
return out
|
||||
|
||||
|
||||
PWSH = shutil.which("pwsh")
|
||||
|
||||
|
||||
def tier_options(tier):
|
||||
kv = {}
|
||||
for line in (PERF / tier / "options.txt").read_text().splitlines():
|
||||
k, _, v = line.partition(":")
|
||||
kv[k] = v
|
||||
return kv
|
||||
|
||||
|
||||
class Base:
|
||||
"""Mixin; subclasses set self.cmd(script, args) -> argv."""
|
||||
|
||||
def setUp(self):
|
||||
if not (PERF / "mid").is_dir():
|
||||
self.skipTest("config/onion-perf not generated (run build.py --ship)")
|
||||
self.tmp = Path(tempfile.mkdtemp())
|
||||
self.game = self.tmp / "instance"
|
||||
(self.game / "config").mkdir(parents=True)
|
||||
shutil.copytree(PERF, self.game / "config" / "onion-perf")
|
||||
for s in SCRIPTS:
|
||||
shutil.copy(REPO / s, self.game / s)
|
||||
(self.game / "config" / "DistantHorizons.toml").write_text("old dh\n")
|
||||
(self.game / "config" / "unrelated.toml").write_text("keep me\n")
|
||||
(self.game / "options.txt").write_text(OLD_OPTIONS)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def run_setup(self, *args, cwd=None):
|
||||
return subprocess.run(self.cmd(*args), cwd=cwd or self.tmp, stdin=subprocess.DEVNULL,
|
||||
capture_output=True, text=True, timeout=60)
|
||||
|
||||
def backups(self):
|
||||
d = self.game / "onion-perf-backup"
|
||||
return sorted(d.iterdir()) if d.is_dir() else []
|
||||
|
||||
def test_tier_copy_backup_and_merge(self):
|
||||
for tier in ("deck", "mid", "high"):
|
||||
with self.subTest(tier=tier):
|
||||
if tier != "deck":
|
||||
self.tearDown()
|
||||
self.setUp()
|
||||
r = self.run_setup(tier, "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||
src = PERF / tier
|
||||
for f in (src / "config").rglob("*"):
|
||||
if f.is_file():
|
||||
rel = f.relative_to(src)
|
||||
self.assertEqual((self.game / rel).read_bytes(), f.read_bytes(), rel)
|
||||
self.assertEqual((self.game / "config/unrelated.toml").read_text(), "keep me\n")
|
||||
# backup holds the pre-run files
|
||||
(b,) = self.backups()
|
||||
self.assertEqual((b / "config/DistantHorizons.toml").read_text(), "old dh\n")
|
||||
self.assertEqual((b / "options.txt").read_text(), OLD_OPTIONS)
|
||||
self.assertFalse((b / "config/unrelated.toml").exists())
|
||||
# options merged key by key
|
||||
want = tier_options(tier)
|
||||
lines = (self.game / "options.txt").read_text().splitlines()
|
||||
kv = dict(line.partition(":")[::2] for line in lines)
|
||||
for k in ("version", "maxFps", "enableVsync"):
|
||||
self.assertEqual(kv[k], dict(x.partition(":")[::2]
|
||||
for x in OLD_OPTIONS.splitlines())[k], k)
|
||||
for k, v in want.items():
|
||||
if k not in ("version", "maxFps", "enableVsync"):
|
||||
self.assertEqual(kv[k], v, k)
|
||||
for keep in ("soundCategory_master:0.5", "key_key.attack:key.mouse.left",
|
||||
"key_key.jump:key.keyboard.space",
|
||||
'resourcePacks:["vanilla","file/MyPack.zip"]'):
|
||||
self.assertIn(keep, lines)
|
||||
self.assertEqual(len(lines), len(set(line.partition(":")[0] for line in lines)))
|
||||
# existing lines keep their positions; appended keys go at the end
|
||||
self.assertEqual(lines[0], "version:3955")
|
||||
self.assertTrue(lines[3].startswith("renderDistance:"))
|
||||
if want["mipmapLevels"] != "4":
|
||||
self.assertIn(f"mipmapLevels: 4 -> {want['mipmapLevels']}", r.stdout)
|
||||
else:
|
||||
self.assertNotIn("mipmapLevels:", r.stdout)
|
||||
self.assertIn("(unset) ->", r.stdout)
|
||||
|
||||
def test_value_with_colon_replaced_whole(self):
|
||||
# split on the FIRST ':' only: a tier value containing ':' replaces as a whole
|
||||
tier_opt = self.game / "config/onion-perf/high/options.txt"
|
||||
tier_opt.write_text(tier_opt.read_text() + "onionProbe:part1:part2\n")
|
||||
(self.game / "options.txt").write_text("version:3955\nonionProbe:wrong\n")
|
||||
r = self.run_setup("high", "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
lines = (self.game / "options.txt").read_text().splitlines()
|
||||
self.assertIn("onionProbe:part1:part2", lines)
|
||||
self.assertNotIn("onionProbe:wrong", lines)
|
||||
self.assertIn("onionProbe: wrong -> part1:part2", r.stdout)
|
||||
|
||||
def test_idempotent(self):
|
||||
self.assertEqual(self.run_setup("deck", "--yes").returncode, 0)
|
||||
snap = {p: p.read_bytes() for p in self.game.rglob("*") if p.is_file()
|
||||
and "onion-perf-backup" not in p.parts}
|
||||
r = self.run_setup("deck", "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("nothing changed", r.stdout)
|
||||
self.assertEqual(len(self.backups()), 1)
|
||||
for p, data in snap.items():
|
||||
self.assertEqual(p.read_bytes(), data, p)
|
||||
|
||||
def test_default_tier_is_mid_when_noninteractive(self):
|
||||
r = self.run_setup("--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("tier 'mid'", r.stdout)
|
||||
self.assertEqual((self.game / "config/DistantHorizons.toml").read_bytes(),
|
||||
(PERF / "mid/config/DistantHorizons.toml").read_bytes())
|
||||
|
||||
def test_missing_options(self):
|
||||
(self.game / "options.txt").unlink()
|
||||
r = self.run_setup("high", "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("options.txt not found", r.stdout)
|
||||
self.assertFalse((self.game / "options.txt").exists())
|
||||
|
||||
def test_crlf_options_preserved(self):
|
||||
(self.game / "options.txt").write_bytes(OLD_OPTIONS.replace("\n", "\r\n").encode())
|
||||
r = self.run_setup("mid", "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
data = (self.game / "options.txt").read_bytes()
|
||||
self.assertNotIn(b"\r\r", data)
|
||||
self.assertEqual(data.count(b"\n"), data.count(b"\r\n"))
|
||||
self.assertIn(b"key_key.attack:key.mouse.left\r\n", data)
|
||||
self.assertIn(b"biomeBlendRadius:1\r\n", data)
|
||||
|
||||
def test_lf_options_preserved(self):
|
||||
self.run_setup("mid", "--yes")
|
||||
self.assertNotIn(b"\r", (self.game / "options.txt").read_bytes())
|
||||
|
||||
def test_bad_tier(self):
|
||||
r = self.run_setup("ultra", "--yes")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertEqual((self.game / "config/DistantHorizons.toml").read_text(), "old dh\n")
|
||||
|
||||
def test_missing_onion_perf(self):
|
||||
shutil.rmtree(self.game / "config" / "onion-perf")
|
||||
r = self.run_setup("mid", "--yes")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertIn("Update the modpack", r.stderr)
|
||||
self.assertEqual(self.backups(), [])
|
||||
|
||||
def test_needs_yes_when_noninteractive(self):
|
||||
r = self.run_setup("mid")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertIn("--yes", r.stderr)
|
||||
self.assertEqual((self.game / "config/DistantHorizons.toml").read_text(), "old dh\n")
|
||||
|
||||
|
||||
def make_sh_case(name, prefix):
|
||||
class Case(Base, unittest.TestCase):
|
||||
def cmd(self, *args):
|
||||
return [*prefix, str(self.game / "onion-perf-setup.sh"), *args]
|
||||
|
||||
def test_run_from_game_dir_with_relative_path(self):
|
||||
r = subprocess.run([*prefix, "onion-perf-setup.sh", "mid", "--yes"], cwd=self.game,
|
||||
stdin=subprocess.DEVNULL, capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
|
||||
Case.__name__ = Case.__qualname__ = f"Sh_{name.replace('-', '_')}"
|
||||
return Case
|
||||
|
||||
|
||||
for _n, _p in runners():
|
||||
globals()[f"Sh_{_n.replace('-', '_')}"] = make_sh_case(_n, _p)
|
||||
|
||||
|
||||
@unittest.skipUnless(PWSH, "pwsh not installed: onion-perf-setup.ps1 untested here")
|
||||
class Pwsh(Base, unittest.TestCase):
|
||||
def cmd(self, *args):
|
||||
return [PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File",
|
||||
str(self.game / "onion-perf-setup.ps1"), *args]
|
||||
|
||||
|
||||
class Packaging(unittest.TestCase):
|
||||
def test_bat_is_crlf_and_calls_ps1(self):
|
||||
data = (REPO / "onion-perf-setup.bat").read_bytes()
|
||||
self.assertEqual(data.count(b"\n"), data.count(b"\r\n"))
|
||||
self.assertIn(b'-File "%~dp0onion-perf-setup.ps1" %*', data)
|
||||
|
||||
def test_scripts_ascii(self):
|
||||
for s in SCRIPTS:
|
||||
(REPO / s).read_bytes().decode("ascii")
|
||||
|
||||
def test_mid_matches_default_options(self):
|
||||
do = REPO / "config" / "defaultoptions"
|
||||
self.assertEqual((PERF / "mid/options.txt").read_bytes(),
|
||||
(do / "options.txt").read_bytes())
|
||||
for f in (PERF / "mid/config").rglob("*"):
|
||||
if f.is_file():
|
||||
rel = f.relative_to(PERF / "mid")
|
||||
self.assertEqual(f.read_bytes(), (do / "extra" / rel).read_bytes(), rel)
|
||||
self.assertFalse((PERF / "mid/config/modernfix-mixins.properties").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user