Benchmark: a 1080p set next to 720p
perf_bench ran every game in a 1280x720 window only. It now runs the standard set at 1280x720 and 1920x1080 (--sizes picks others), each size its own reports: <label>-rd<N>.json at 720p as before, <label>-1080p-rd<N>.json at 1080p. Horizon takes vanilla's --width and --height for its window's starting size (1280x720 by default); perf_bench passes them to every game, and to Horizon only off 720p, so builds before this one still run the 720p set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -267,6 +267,9 @@ struct Options {
|
||||
/// Render into a texture instead of the window (`--offscreen`), so a
|
||||
/// GPU the display cannot present from can still be benchmarked.
|
||||
offscreen: bool,
|
||||
/// `--width` and `--height` (vanilla's own arguments): the window's
|
||||
/// starting size, 1280x720 by default.
|
||||
window_size: [u32; 2],
|
||||
/// Multiplayer: `--server HOST[:PORT]` joins at start; `--name` is the offline player name.
|
||||
server: Option<String>,
|
||||
/// `--name`, `HORIZON_PLAYER_NAME` or the signed-in account's name; else
|
||||
@@ -326,6 +329,7 @@ impl Options {
|
||||
no_pause: false,
|
||||
control: None,
|
||||
offscreen: false,
|
||||
window_size: [1280, 720],
|
||||
spikes: std::env::var("HORIZON_SPIKES")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok()),
|
||||
@@ -348,7 +352,7 @@ impl Options {
|
||||
match arg.as_str() {
|
||||
"--help" | "-h" => {
|
||||
println!(
|
||||
"Horizon — Minecraft Java Edition {} in Rust\n\n --resources PATH Prepared resources (horizon-prepare)\n --render-distance N Chunks, 2..64 (default 12)\n --world NAME World folder under saves/ (default \"New World\")\n --seed N World seed for new worlds (default 0)\n --creative Creative mode (default survival)\n --test-terrain Simple test terrain instead of vanilla generation\n --frames N Exit after N frames (testing)\n --world-frames N Exit after N frames drawn in the world (testing)\n --smoke-report PATH Write a JSON report on exit\n --benchmark SCENARIO Run a benchmark (standard, village, forest, ocean, mountain), write its report to benchmarks/ and quit\n --control Accept horizon-ctl commands on a local socket\n --control-socket PATH Control socket path (default $XDG_RUNTIME_DIR/horizon-control.sock)\n --server HOST[:PORT] Join a multiplayer server (offline mode) at start\n --name NAME Player name for multiplayer (default: generated once, kept in horizon-player.txt)\n --session-file PATH Signed-in account from the launcher (read once, then deleted)\n --dev-mod PATH A Horizon mod (.hmod or folder) to develop: always on, reloaded when rebuilt\n --no-server-thread Run single player's world in the frame, not on its server thread\n\nControls: mouse look, WASD move, Space jump, Left Shift sneak, Left Ctrl sprint,\nleft click break, right click place/use, 1-9/scroll hotbar, E inventory,\nQ drop, F3 debug, Esc pause.",
|
||||
"Horizon — Minecraft Java Edition {} in Rust\n\n --resources PATH Prepared resources (horizon-prepare)\n --render-distance N Chunks, 2..64 (default 12)\n --world NAME World folder under saves/ (default \"New World\")\n --seed N World seed for new worlds (default 0)\n --width N, --height N Window size (default 1280 x 720)\n --creative Creative mode (default survival)\n --test-terrain Simple test terrain instead of vanilla generation\n --frames N Exit after N frames (testing)\n --world-frames N Exit after N frames drawn in the world (testing)\n --smoke-report PATH Write a JSON report on exit\n --benchmark SCENARIO Run a benchmark (standard, village, forest, ocean, mountain), write its report to benchmarks/ and quit\n --control Accept horizon-ctl commands on a local socket\n --control-socket PATH Control socket path (default $XDG_RUNTIME_DIR/horizon-control.sock)\n --server HOST[:PORT] Join a multiplayer server (offline mode) at start\n --name NAME Player name for multiplayer (default: generated once, kept in horizon-player.txt)\n --session-file PATH Signed-in account from the launcher (read once, then deleted)\n --dev-mod PATH A Horizon mod (.hmod or folder) to develop: always on, reloaded when rebuilt\n --no-server-thread Run single player's world in the frame, not on its server thread\n\nControls: mouse look, WASD move, Space jump, Left Shift sneak, Left Ctrl sprint,\nleft click break, right click place/use, 1-9/scroll hotbar, E inventory,\nQ drop, F3 debug, Esc pause.",
|
||||
horizon_core::VANILLA_VERSION
|
||||
);
|
||||
return Ok(None);
|
||||
@@ -397,6 +401,8 @@ impl Options {
|
||||
"--timeout-seconds" => o.timeout = Duration::from_secs(value.parse()?),
|
||||
"--smoke-report" => o.report = Some(value.into()),
|
||||
"--screenshot-after" => o.screenshot_after = Some(value.parse()?),
|
||||
"--width" => o.window_size[0] = value.parse::<u32>()?.max(1),
|
||||
"--height" => o.window_size[1] = value.parse::<u32>()?.max(1),
|
||||
"--world" => o.world = Some(value),
|
||||
"--dev-mod" => o.dev_mods.push(std::path::absolute(&value)?),
|
||||
"--benchmark" => {
|
||||
@@ -10574,6 +10580,7 @@ impl ApplicationHandler for Game<'_> {
|
||||
}
|
||||
// The Window constructor: created fullscreen when options.txt says so.
|
||||
let fullscreen = self.settings.fullscreen;
|
||||
let size = self.options.window_size.map(f64::from);
|
||||
self.fullscreen = horizon_client::window::FullscreenState::new(fullscreen);
|
||||
let attributes = Window::default_attributes()
|
||||
.with_title("Horizon")
|
||||
@@ -10585,7 +10592,7 @@ impl ApplicationHandler for Game<'_> {
|
||||
);
|
||||
let window = match event_loop.create_window(
|
||||
attributes
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0))
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(size[0], size[1]))
|
||||
.with_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None))),
|
||||
) {
|
||||
Ok(w) => Arc::new(w),
|
||||
@@ -12678,6 +12685,11 @@ impl Loading {
|
||||
}
|
||||
// The Window constructor: created fullscreen when options.txt says so.
|
||||
let fullscreen = self.settings.fullscreen;
|
||||
let size = self
|
||||
.options
|
||||
.as_ref()
|
||||
.map_or([1280, 720], |o| o.window_size)
|
||||
.map(f64::from);
|
||||
self.fullscreen = horizon_client::window::FullscreenState::new(fullscreen);
|
||||
let attributes = Window::default_attributes()
|
||||
.with_title("Horizon")
|
||||
@@ -12689,7 +12701,7 @@ impl Loading {
|
||||
);
|
||||
let window = match event_loop.create_window(
|
||||
attributes
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0))
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(size[0], size[1]))
|
||||
.with_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None))),
|
||||
) {
|
||||
Ok(w) => Arc::new(w),
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
## What is measured
|
||||
|
||||
One game is started per render distance (8, 16 and 32 chunks by default), each from a fresh world, so nothing generated by an earlier run is reused. In each game:
|
||||
One game is started per window size (1280x720 and 1920x1080 by default) and render distance (8, 16 and 32 chunks by default), each from a fresh world, so nothing generated by an earlier run is reused. In each game:
|
||||
|
||||
1. **Start to the title screen.** Wall-clock time from starting the process to the first frame of the title screen with no loading overlay.
|
||||
2. **Create the world.** A creative world named `PerfBench`, seed 1234, default world type and difficulty, created the way the game's own Create World does. **Start to in-world** is the time from starting the process until the player has spawned and every chunk around the spawn is loaded and drawn.
|
||||
@@ -16,7 +16,7 @@ One game is started per render distance (8, 16 and 32 chunks by default), each f
|
||||
|
||||
- **Same machine, same run conditions.** Games run one at a time; the load average before and after each run and the machine (CPU, RAM, OS) are stored with the results. Runs on a machine doing other work are marked as such and not published.
|
||||
- **Same settings, one file.** Every game gets the same `options.txt`, written by the driver: render distance, simulation distance 12, unlimited frame rate (`maxFps:260`), VSync off, FOV 70, automatic GUI scale, windowed, no pause on lost focus, and `inactivityFpsLimit:minimized` (both games otherwise drop to 30 FPS after a minute without input). Everything else stays at vanilla's defaults: Horizon reads vanilla's `options.txt` keys and its defaults match vanilla's (the options harness checks it), and Horizon's own settings (`horizon-graphics.txt`) are left at their defaults, which render as vanilla does.
|
||||
- **Same resolution.** Both windows are 1280x720 (Horizon's default window; vanilla gets `--width 1280 --height 720`). The size each game actually renders at is stored with the results.
|
||||
- **Same resolution.** Both games get the same window size, `--width` and `--height` (vanilla's own launch arguments, which Horizon takes too): 1280x720, the size results were first published at, then 1920x1080, the commonest screen; `--sizes` picks others. The size each game actually renders at is stored with the results: a compositor or screen smaller than the window can shrink it.
|
||||
- **Same world and path.** Same seed and world type; same paths, yaw and pitch, same flight speed; the same steps in the same order. Vanilla is driven by a small dev-only Fabric mod (`mods/horizon/src/bench`), which only calls the game's own code: it creates the world with `WorldOpenFlows.createFreshLevel` as the Create World screen does, teleports with `/tp`, sets the time with `/time set`, and places the player on the path each frame as Horizon's `horizon-ctl fly` does.
|
||||
- **Warm-up excluded.** Each scene's flight starts only after its chunks are loaded and drawn and 2 seconds have passed; the load itself is reported separately. JIT warm-up of the JVM during the first scene is part of vanilla's real behaviour and is not removed.
|
||||
- **The same statistics code.** Horizon returns its raw frame times (`horizon-ctl fly ... raw`) and the vanilla mod writes its own; `stats()` in the driver computes every figure for both.
|
||||
@@ -55,7 +55,7 @@ python3 tools/perf_bench.py --game vanilla-mods --label mods-mine
|
||||
python3 tools/perf_bench.py --table horizon-mine vanilla-mine mods-mine
|
||||
```
|
||||
|
||||
Options: `--distances 8,16`, `--scenes village,ocean`, `--seconds 20`, `--repeats 5`, `--heap 4G` (vanilla), `--offscreen` (Horizon, for locked or headless sessions). Results go to `benchmarks/perf/<label>-rd<N>.json`, one [benchmark report](benchmark-report.md) per render distance (the same format the game's `--benchmark` and the launcher write), with the load average before each game; each game's log, and the vanilla driver's event log, stay in `.local/perf-bench/`. Keep the machine otherwise idle, plugged in, and the window on a screen.
|
||||
Options: `--distances 8,16`, `--sizes 1920x1080`, `--scenes village,ocean`, `--seconds 20`, `--repeats 5`, `--heap 4G` (vanilla), `--offscreen` (Horizon, for locked or headless sessions). Results go to `benchmarks/perf/<label>-rd<N>.json` at 1280x720 and `<label>-1080p-rd<N>.json` at 1920x1080 (other sizes `<label>-<W>x<H>-rd<N>.json`; `--table` takes those labels), one [benchmark report](benchmark-report.md) per window size and render distance (the same format the game's `--benchmark` and the launcher write), with the load average before each game; each game's log, and the vanilla driver's event log, stay in `.local/perf-bench/`. Keep the machine otherwise idle, plugged in, and the window on a screen.
|
||||
|
||||
## Results
|
||||
|
||||
|
||||
@@ -38,6 +38,14 @@ class PerfBenchStatsTests(unittest.TestCase):
|
||||
self.assertIsNone(frames("x", 0, []))
|
||||
json.dumps(s)
|
||||
|
||||
def test_window_sizes_name_their_reports(self):
|
||||
from tools.perf_bench import SIZES, parse_sizes, size_label
|
||||
self.assertEqual(parse_sizes("1280x720,1920X1080"), [(1280, 720), (1920, 1080)])
|
||||
self.assertEqual(SIZES, [(1280, 720), (1920, 1080)])
|
||||
self.assertEqual(size_label("horizon-m4", 1280, 720), "horizon-m4")
|
||||
self.assertEqual(size_label("horizon-m4", 1920, 1080), "horizon-m4-1080p")
|
||||
self.assertEqual(size_label("horizon-m4", 2560, 1080), "horizon-m4-2560x1080")
|
||||
|
||||
def test_one_options_file_sets_the_render_distance(self):
|
||||
self.assertEqual(OPTIONS["renderDistance"].format(distance=16), "16")
|
||||
self.assertEqual(OPTIONS["graphicsPreset"], '"custom"')
|
||||
|
||||
+36
-13
@@ -256,6 +256,23 @@ def write_options(game_dir, distance, version=None):
|
||||
return text
|
||||
|
||||
|
||||
# Window sizes of the standard set (every game gets --width/--height): 720p,
|
||||
# the size results were first published at, and 1080p, the commonest screen.
|
||||
SIZES = [(1280, 720), (1920, 1080)]
|
||||
|
||||
|
||||
def parse_sizes(text):
|
||||
""""1280x720,1920x1080" as [(1280, 720), (1920, 1080)]."""
|
||||
return [tuple(int(v) for v in size.lower().split("x")) for size in text.split(",") if size]
|
||||
|
||||
|
||||
def size_label(label, width, height):
|
||||
"""Reports at 1280x720 keep the plain label; other sizes add theirs: horizon-m4-1080p, or -2560x1080."""
|
||||
if (width, height) == SIZES[0]:
|
||||
return label
|
||||
return f"{label}-{height}p" if width * 9 == height * 16 else f"{label}-{width}x{height}"
|
||||
|
||||
|
||||
def scene_list(args):
|
||||
picked = args.scenes.split(",") if args.scenes else [s[0] for s in SCENES]
|
||||
return [s for s in SCENES if s[0] in picked]
|
||||
@@ -375,6 +392,9 @@ def run_horizon(args, distance):
|
||||
client = Path(args.build).resolve() / "horizon-client"
|
||||
command = [str(client), "--resources", str(Path(args.resources).resolve()),
|
||||
"--control-socket", str(sock), "--no-pause", "--weather", "clear", "--time", "6000"]
|
||||
if (args.width, args.height) != SIZES[0]:
|
||||
# 1280x720 is Horizon's own window; builds before --width open only that one.
|
||||
command += ["--width", str(args.width), "--height", str(args.height)]
|
||||
if args.offscreen:
|
||||
command.append("--offscreen")
|
||||
for carry in dev_mods(args):
|
||||
@@ -796,8 +816,8 @@ def main():
|
||||
p.add_argument("--seconds", type=float, default=12.0, help="flight duration per scene")
|
||||
p.add_argument("--settle", type=float, default=2.0, help="seconds held at the start before each flight")
|
||||
p.add_argument("--timeout", type=float, default=300.0, help="seconds allowed for each load")
|
||||
p.add_argument("--width", type=int, default=1280, help="vanilla window width (Horizon's is 1280x720)")
|
||||
p.add_argument("--height", type=int, default=720)
|
||||
p.add_argument("--sizes", default=",".join(f"{w}x{h}" for w, h in SIZES),
|
||||
help="window sizes, each a set of reports (default 1280x720 and 1920x1080)")
|
||||
p.add_argument("--heap", default="2G", help="vanilla -Xmx (the official launcher's default is 2G)")
|
||||
p.add_argument("--offscreen", action="store_true",
|
||||
help="Horizon renders into a texture and does not wait for the window (locked macOS sessions)")
|
||||
@@ -817,17 +837,20 @@ def main():
|
||||
args.label = args.label or f"{args.game}-{platform.node().split('.')[0]}"
|
||||
WORK.mkdir(parents=True, exist_ok=True)
|
||||
run = {"horizon": run_horizon, "neoforge": run_neoforge}.get(args.game, run_vanilla)
|
||||
for d in [int(x) for x in args.distances.split(",")]:
|
||||
runs = []
|
||||
for _ in range(args.repeats):
|
||||
load_before = os.getloadavg()
|
||||
result = run(args, d)
|
||||
result["load_before"], result["load_after"] = load_before, os.getloadavg()
|
||||
runs.append(result)
|
||||
out = ROOT / "benchmarks/perf" / f"{args.label}-rd{d}.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(report(args, d, runs, app(args)), indent=1) + "\n")
|
||||
print(out)
|
||||
label = args.label
|
||||
for args.width, args.height in parse_sizes(args.sizes):
|
||||
args.label = size_label(label, args.width, args.height)
|
||||
for d in [int(x) for x in args.distances.split(",")]:
|
||||
runs = []
|
||||
for _ in range(args.repeats):
|
||||
load_before = os.getloadavg()
|
||||
result = run(args, d)
|
||||
result["load_before"], result["load_after"] = load_before, os.getloadavg()
|
||||
runs.append(result)
|
||||
out = ROOT / "benchmarks/perf" / f"{args.label}-rd{d}.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(report(args, d, runs, app(args)), indent=1) + "\n")
|
||||
print(out)
|
||||
|
||||
|
||||
def app(args):
|
||||
|
||||
Reference in New Issue
Block a user