Compare commits

..
Author SHA1 Message Date
Ludovic MarieandClaude Opus 5.5 5bb7be3765 Merge main into feat/lua-mods (Cargo.toml members one per line; stability.md server-mods row from main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:04:24 +01:00
Ludovic MarieandClaude Opus 5.5 bad95e4f6a horizon-mod-lua: string.rep and table.concat keep bytes that are not UTF-8
Lua strings are byte strings. Both converted their arguments with
to_str_lossy, so a byte such as \255 (binary values a mod keeps in
horizon.storage, for one) came back as U+FFFD's three bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:33:52 +01:00
Ludovic Marie b03d1411d1 Merge remote-tracking branch 'origin/main' into feat/mods-lua
# Conflicts:
#	Cargo.toml
2026-10-27 02:24:19 +01:00
Ludovic Marie a24a6fa26c Merge remote-tracking branch 'origin/main' into feat/mods-lua
# Conflicts:
#	Cargo.toml
2026-10-26 23:40:35 +01:00
Ludovic Marie 6d9bb375c5 Merge remote-tracking branch 'origin/main' into feat/mods-lua
# Conflicts:
#	Cargo.toml
2026-10-26 21:43:18 +01:00
Ludovic Marie 10c9d8440c Merge remote-tracking branch 'origin/main' into feat/mods-lua 2026-10-26 19:13:17 +01:00
Ludovic MarieandClaude Opus 5.5 7f6d556e68 docs: Lua mods' page goes to horizon-site with the rest of website/
docs/mods.md keeps the reference, plus what a capability missing from the features does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:09:10 +01:00
Ludovic MarieandClaude Opus 5.5 d33f547bf7 Merge origin/main into feat/mods-lua
website/ moved to its own repo (#2286): this branch's changes to make-a-mod.md and nav.json go there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 17:08:55 +01:00
Ludovic Marie 2e7283d01c Merge remote-tracking branch 'origin/main' into feat/mods-lua 2026-10-26 16:10:40 +01:00
Ludovic Marie 9a7f955a77 Merge remote-tracking branch 'origin/main' into feat/mods-lua
# Conflicts:
#	Cargo.toml
#	docs/api/stability.md
2026-10-26 11:54:53 +01:00
Ludovic Marie 6b7617c277 Merge remote-tracking branch 'origin/main' into feat/mods-lua
# Conflicts:
#	Cargo.toml
2026-10-26 09:44:13 +01:00
Ludovic MarieandClaude Opus 5.5 7c5efff307 Merge main into feat/mods-lua
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:31:10 +01:00
Ludovic MarieandClaude Opus 5.5 1f6e498238 Merge main into feat/mods-lua
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 07:48:21 +01:00
Ludovic MarieandClaude Opus 5.5 32f6079134 Merge main into feat/mods-lua
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 06:03:23 +01:00
Ludovic MarieandClaude Opus 5.5 ce1db1b0c3 Merge main into feat/mods-lua
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 03:20:16 +01:00
Ludovic Marie ad3ddeb43f Merge remote-tracking branch 'origin/main' into feat/mods-lua
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/horizon-mods/tests/sandbox.rs
2026-10-26 01:43:14 +01:00
Ludovic Marie 34f4d3b451 Merge remote-tracking branch 'origin/main' into feat/mods-lua
# Conflicts:
#	Cargo.toml
2026-10-26 00:25:27 +01:00
Ludovic MarieandClaude Opus 5.5 27de895fdb Mods in Lua: horizon-mod-lua, a Lua 5.4 interpreter built into the mod
A mod's code can be a Lua script. The crate horizon-mod-lua embeds
piccolo (pure Rust Lua, MIT) in the wasm guest with the script, so a Lua
mod is an .hmod like any other: same sandbox, capabilities, fuel and
checks. `horizon_mod_lua::lua_mod!("main.lua")` is the whole Rust side.

- The script runs once at load and registers commands with
  horizon.command; hooks are globals (on_tick, on_frame, on_command,
  on_chat). Capabilities are tables of `horizon` (game, chat, hud,
  storage), each behind a cargo feature so unused imports aren't linked.
- piccolo's stdlib lacks them: string.format (C flags, width,
  precision), string.rep, table.insert/remove/concat.
- The sandbox has no entropy: getrandom 0.2 and 0.3 get a custom
  SplitMix64 backend (cfg getrandom_backend="custom", set by
  cargo horizon-mod build).
- Example coords-hud-lua: coords-hud line for line. The sandbox tests
  run the same checks on both, and measure a tick of each.
- cargo horizon-mod new --template lua.
- Docs: docs/mods.md, website "Write it in Lua", changelog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 20:32:50 +01:00
3952 changed files with 107347 additions and 1243133 deletions

No files matched your search

-76
View File
@@ -1,76 +0,0 @@
---
name: vanilla-parity
description: Fix a Horizon bug or gap so it behaves 1:1 like vanilla 26.3, starting from the port ledger (tools/port_ledger.py) instead of grepping. Use for any issue, Discord report or PR that makes Horizon match vanilla: "X doesn't work like in Java", a missing sound, particle, animation, mob behaviour, block rule, screen or packet.
---
# Vanilla parity with the port ledger
The port ledger pairs every vanilla method with the Rust fn that ports it (or says it is missing, or needs
no port) and lists where a port isn't wired like vanilla. Ask it first; grep only for what it can't answer.
Full reference: `docs/port-ledger.md`.
## 0. Have a fresh ledger
```sh
python3 tools/port_ledger.py build # ~20 s; again after pulling main
```
It needs the vanilla jar (the launcher's `versions/26.3/26.3.jar`, or `--jar`) and/or the lab's decompiled
sources (`python3 tools/lab.py decompile`, or `--sources DIR`). If `build` says it has no vanilla code, run
the lab first; don't guess vanilla from memory.
## 1. From the report to the code
```sh
python3 tools/port_ledger.py issue 3236 # a GitHub issue number
python3 tools/port_ledger.py issue "mooshroom stays red after lightning" # or the report's text
```
It ranks the vanilla classes the report names and lists, per class, its Rust home files, where each method is
ported, and the known gaps (`missing` rows from the verdict map, seam/client/drift gaps). Often the cause is
already written there ("Type is not saved"). Then drill in:
```sh
python3 tools/port_ledger.py where MushroomCow.readAdditionalSaveData # ports, callers, callees, gaps
python3 tools/port_ledger.py class MushroomCow # every method: where, or missing
python3 tools/port_ledger.py what crates/horizon-client/src/mobs/farm.rs:120 # which vanilla method a fn ports
```
A ledger answer is a lead, not a verdict: about 1 in 12 map rows is wrong. Read the vanilla method (path and
line are in the output) and the Rust fn before changing anything.
## 2. Port it the vanilla way
- Translate the vanilla method, don't reinvent it: same order of operations, same constants, the same random
draws in the same order (`horizon_core::random`), Java `Math` through `horizon_core::jmath`.
- Open the fn's doc with the vanilla method it ports: `` /// `MushroomCow.readAdditionalSaveData`: ... ``.
That one line is what makes the next agent find it.
- Fix the path players run: single player is a client of the integrated server, so server logic lives in
`game/lan*.rs` and `src/lan/`, and what only the client's copy shows (particles, local sounds, hand
animation) must be reached from `game/remote*.rs`.
- Look at the neighbours the ledger lists for the same class: a `client` or `seam` gap next to your fix is
usually the same bug on the other path.
## 3. Before the PR
```sh
python3 tools/port_ledger.py check --base origin/main
```
It lists the ports your branch touches with their open gaps, citations to add, stale citations, and (against
the base) the gaps the branch closes and opens. Close what belongs to your fix, cite what you ported, leave
no new stale citation. If you ported a method the verdict map called `missing`, change its row in
`tools/port_map/*.tsv` to `port` with the fn (`file.rs::Owner::fn`) and run
`python3 tools/port_ledger.py map-check tools/port_map/<file>.tsv`.
Then the usual: the harness for the area (`docs/architecture.md`), `cargo check`/tests per `AGENTS.md`, and
a small PR that says which vanilla method it now matches.
## Finding work
```sh
python3 tools/port_ledger.py gaps --kind seam # ported on two paths, wired on one: the best leads
python3 tools/port_ledger.py gaps --kind client # client-only effects the server path never reaches
python3 tools/port_ledger.py gaps --kind drift # a port missing a sound, particle, item or constant
rg -n "player:" tools/port_map/*.tsv # missing behaviour a player would see
```
+1 -10
View File
@@ -1,11 +1,2 @@
#!/bin/sh
# Git for Windows puts a `python3` on PATH that is only the Microsoft Store
# stub: it prints "Python" and exits non-zero, so the hook fails with no other
# output. Use the first name that actually runs Python 3.
for py in python3 python py; do
if "$py" -c "import sys; sys.exit(sys.version_info < (3,))" >/dev/null 2>&1; then
exec "$py" tools/check_public_tree.py
fi
done
echo "check_public_tree: no working Python 3 found" >&2
exit 1
exec python3 tools/check_public_tree.py
+1 -10
View File
@@ -1,11 +1,2 @@
#!/bin/sh
# Git for Windows puts a `python3` on PATH that is only the Microsoft Store
# stub: it prints "Python" and exits non-zero, so the hook fails with no other
# output. Use the first name that actually runs Python 3.
for py in python3 python py; do
if "$py" -c "import sys; sys.exit(sys.version_info < (3,))" >/dev/null 2>&1; then
exec "$py" tools/check_public_tree.py --pre-push
fi
done
echo "check_public_tree: no working Python 3 found" >&2
exit 1
exec python3 tools/check_public_tree.py --pre-push
+1 -2
View File
@@ -1,4 +1,3 @@
self-hosted-runner:
# The Mac mini's runner (ludovics-mac-mini), which holds the maintainer's own Minecraft files.
# windows-11-vs2026-arm is GitHub's Windows ARM64 image (2026-08), newer than actionlint's label list.
labels: [horizon-game, ludovics-mac-mini, windows-11-vs2026-arm]
labels: [horizon-game, ludovics-mac-mini]
-20
View File
@@ -1,32 +1,12 @@
<!-- Opened by an AI agent? First line: "> 🤖 Opened by <your GitHub login>'s AI agent (<tool, model>), not by <your GitHub login> personally." Use your own login, not someone else's. -->
## What and why
<!-- What this changes, and why. Link the issue it fixes ("Fixes #123"). -->
## Kind
<!-- Tick one: it sets the pull request's label (CONTRIBUTING.md "Pull requests"). -->
- [ ] parity: Horizon now does what vanilla 26.3 does
- [ ] bug: a crash or a breakage
- [ ] perf: faster, or less memory
- [ ] feature: something new for players (listed under New in the release notes)
- [ ] extra: Horizon-only, off by default
- [ ] tooling: tools, CI, tests, harnesses
- [ ] docs
## Vanilla reference
<!-- The vanilla classes and methods this follows (tools/rea_vanilla.py), and the harness that checks it.
"Horizon-only" for a feature vanilla doesn't have; "none" for tooling or docs. -->
## Other projects' code
<!-- "none", or for each project you read or copied from: its link at the commit, its license (only MIT,
BSD-2-Clause, BSD-3-Clause, Apache-2.0, ISC, zlib, 0BSD, MIT-0, Unlicense or CC0-1.0), and what you took.
CONTRIBUTING.md "Only original code". -->
## Self-check
<!-- Paste the block `python3 tools/pr_ready.py` prints (`--base upstream/main` from a fork). -->
-16
View File
@@ -1,16 +0,0 @@
# Trusted reviewers: one list for three things (docs/release-flow.md "Trusted reviewers").
# - `/nightly 2640 2642` picks the pull requests the Nightly holds (tools/ci/nightly_set.py);
# - `/close`, `/reopen`, `/label`, `/unlabel` triage issues and pull requests (tools/ci/triage_command.py);
# - `/unclaim @login` releases someone else's claim, and listed reviewers have no claim limit (tools/ci/claim_command.py).
# One reviewer per line: `<numeric GitHub ID> <login>`, with who added it and when. The ID decides; the login is only
# for people reading this file and for messages. A login is a name anyone can claim once its owner renames or deletes
# the account; an ID is never reused. Get it with `gh api users/<login> --jq .id`, or run
# `python3 tools/ci/resolve_reviewer_ids.py` to fill in the IDs and `--check` to catch renames.
# A line in any other form (a bare login included) grants nothing. Ludovic is always allowed (OWNER_ID in
# tools/ci/nightly_set.py) and isn't listed. Removing a line revokes that account at once. A line with only `paused`
# stops all three for every reviewer (Ludovic's still work).
54669067 kacpherek # added by Ludovic, 2026-10-27
87152661 rdbtCVS # added by Ludovic, 2026-10-27
84875247 Online5277 # added by Ludovic, 2026-10-27 (his top three by merged pull requests)
86072227 levzzz5154 # added by Ludovic, 2026-10-28 (Horizon Team; author of horizon-mixins)
87478350 Qwerner88880 # added by Ludovic, 2026-10-31 (Discord vladislav_hagen; native-mod hooks)
+5 -9
View File
@@ -9,23 +9,19 @@ permissions:
actions: write
jobs:
cancel:
# One of our small runners (horizon-light), out of GitHub's pool: no checkout, no code of the pull request
# (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
steps:
- name: Cancel the pull request's unfinished runs
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.event.pull_request.head.ref }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Every head the pull request had, not only its last: a run-parity label added on an older head left that
# run queued after the merge (#3104, 2026-10-29). Its branch in its own repository names its runs (another
# fork can have a branch of the same name).
for status in queued in_progress waiting pending requested; do
gh api -X GET "repos/$REPO/actions/runs" -f branch="$BRANCH" -f event=pull_request -f status="$status" \
-f per_page=100 --jq ".workflow_runs[] | select(.head_repository.full_name == \"$HEAD_REPO\") | .id"
gh run list -R "$REPO" --branch "$BRANCH" --status "$status" --limit 100 \
--json databaseId,event,headSha \
-q ".[] | select(.event == \"pull_request\" and .headSha == \"$SHA\") | .databaseId"
done | sort -u | while read -r id; do
gh run cancel -R "$REPO" "$id" || true
done
-107
View File
@@ -1,107 +0,0 @@
name: cargo-horizon-mod
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120): binstall reads
# [package.metadata.binstall] from crates/cargo-horizon-mod/Cargo.toml and downloads the archive this workflow
# attaches to the tag `cargo-horizon-mod-v<version>`, whose version must be the manifest's. The targets come from
# tools/horizon_mod_binstall.py, the same file the manifest's template is checked against, so a platform added there
# is built here and one nobody publishes is caught before it ships. A pull request builds the archives and publishes
# nothing.
on:
push:
tags: ['cargo-horizon-mod-v*']
pull_request:
paths:
- 'crates/cargo-horizon-mod/**'
- 'tools/horizon_mod_binstall.py'
- '.github/workflows/cargo-horizon-mod.yml'
permissions:
contents: read
jobs:
plan:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
platforms: ${{ steps.platforms.outputs.platforms }}
steps:
- uses: actions/checkout@v7
- id: platforms
run: echo "platforms=$(python3 -c 'import json, tools.horizon_mod_binstall as m; print(json.dumps(m.PLATFORMS))')" >> "$GITHUB_OUTPUT"
build:
needs: plan
name: build ${{ matrix.target }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.plan.outputs.platforms) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: The aarch64 Linux linker
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
- name: Build
run: cargo build --release --locked -p cargo-horizon-mod --target ${{ matrix.target }}
- name: It starts
if: matrix.run
shell: bash
run: |
set -euo pipefail
binary="target/${{ matrix.target }}/release/cargo-horizon-mod"
[ -f "$binary.exe" ] && binary="$binary.exe"
out=$("$binary" 2>&1 || true)
case "$out" in
*"usage: cargo horizon-mod"*) ;;
*) echo "$out"; echo "::error::$binary does not start"; exit 1 ;;
esac
- name: Package
shell: bash
run: |
set -euo pipefail
binary="target/${{ matrix.target }}/release/cargo-horizon-mod"
[ -f "$binary.exe" ] && binary="$binary.exe"
mkdir -p dist
tar -czf "dist/$(basename "$binary" | sed 's/\.exe$//')-${{ matrix.target }}.tar.gz" -C "$(dirname "$binary")" "$(basename "$binary")"
- uses: actions/upload-artifact@v7
with:
name: cargo-horizon-mod-${{ matrix.target }}
path: dist/*.tar.gz
if-no-files-found: error
retention-days: 3
publish:
needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
pattern: cargo-horizon-mod-*
path: dist
merge-multiple: true
- name: The tag and the manifest name the same release and archives
run: python3 tools/horizon_mod_binstall.py dist "$GITHUB_REF_NAME"
- name: Checksums
run: |
set -euo pipefail
cd dist
for file in *.tar.gz; do sha256sum "$file" | cut -d' ' -f1 > "$file.sha256"; done
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
notes="\`cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod\`"
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" dist/* --clobber
else
gh release create "$GITHUB_REF_NAME" dist/* --latest=false --title "cargo-horizon-mod ${GITHUB_REF_NAME#cargo-horizon-mod-v}" --notes "$notes"
fi
+30 -175
View File
@@ -24,28 +24,14 @@ env:
HORIZON_TELEMETRY_URL: "off"
jobs:
# One job, not one per check: each job takes a runner from the organisation's shared pool, and with every pull request
# and every merge to main starting this workflow, three jobs a run left the pool's queue tens of minutes deep.
#
# Where (docs/ci-runners.md): our checks runners (label horizon-checks: the desktop's, an isolated user, so a fork's
# pull request runs there too) take the pull requests whose number ends in 0 or 1, about what their three runners
# can do; GitHub's runners take the others and main's pushes (which also save the Rust cache GitHub's pull request
# runs read), so both pools work at once. The repository variable SELF_HOSTED_CI moves them: `all` every job to
# ours, `light` (the desktop paused) or `off` every job to GitHub's. On ours the build folder persists
# (CARGO_TARGET_DIR, outside the checkout), so the apt packages, Python and the Rust cache steps are GitHub's only.
verify:
runs-on: >-
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && (vars.SELF_HOSTED_CI == 'all' ||
github.event_name == 'pull_request' && (endsWith(format('{0}', github.event.pull_request.number), '0') ||
endsWith(format('{0}', github.event.pull_request.number), '1'))) &&
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
timeout-minutes: 45
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v7
if: runner.environment == 'github-hosted'
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Does this pull request need Rust checks?
@@ -55,43 +41,20 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 tools/ci/rust_changes.py "$BASE_SHA" "$HEAD_SHA" >> "$GITHUB_OUTPUT"
# A pull request adds only its own commits to main, so it reads those; main's pushes read every ref's history.
- name: Keep game artifacts out of Git
env:
EVENT: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
python tools/check_public_tree.py
if [ "$EVENT" = pull_request ]; then
python tools/check_public_tree.py --range "$BASE_SHA..$HEAD_SHA"
else
python tools/check_public_tree.py --history
fi
# Our runners as GitHub's: some Python tests compile Rust with a bare `rustc` (GitHub's image has a default
# toolchain, our runners' own rustup homes may not), and the GPU tests find no adapter there, so they return
# early. Ours have Mesa's software Vulkan for parity; a backend Linux lacks keeps the tests from it, so a pull
# request's verify doesn't depend on which pool took it (#8627: a GPU test that flakes on lavapipe).
- name: Our runners as GitHub's (a default Rust toolchain, no GPU)
if: runner.environment != 'github-hosted'
run: |
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
rustup default stable
echo "WGPU_BACKEND=dx12" >> "$GITHUB_ENV"
python tools/check_public_tree.py --history
- name: Test offline research tooling
run: python -m unittest discover -s tests -v
- name: Install Rust toolchain and the ALSA and udev headers the client links against
if: steps.rust.outputs.required != 'false'
env:
HOSTED: ${{ runner.environment == 'github-hosted' }}
run: |
if [ "$HOSTED" = true ]; then
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
fi
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
rustup toolchain install stable --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown
# Dependencies and the workspace's own build, saved from main's runs (pull requests read main's cache).
- uses: Swatinem/rust-cache@v2
if: steps.rust.outputs.required != 'false' && runner.environment == 'github-hosted'
if: steps.rust.outputs.required != 'false'
with:
shared-key: verify
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -99,34 +62,28 @@ jobs:
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable fmt --all -- --check
cargo +stable clippy --workspace --all-targets -- -D warnings
# With the game's code mods (on in every release build, off by default).
cargo +stable clippy --workspace --all-targets --features horizon-client/mods -- -D warnings
# The server build (released as horizon-engine): the game without sound or controllers.
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
# Libraries, binaries and integration tests, then doc tests: not the ~275 examples, which clippy above already
# type-checks and the parity job builds and runs (they were most of this step's build and link time).
# cargo test, not nextest: nextest's process per test reloads each binary's shared tables for every test, and on
# these 4-core runners it was no faster (#4978).
- name: Test Rust core and voxel kernel correctness
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable test --workspace --lib --bins --tests
cargo +stable test --workspace --lib --bins --tests --features horizon-client/mods,horizon-mods/pack
# The full content-root tests (#[ignore]d: minutes each on a laptop). The same build as above; they need the
# prepared vanilla data, so without it they pass at once. Keep the target selection identical to avoid
# recompiling the workspace with a different dependency graph.
cargo +stable test --workspace --lib --bins --tests mod_content::tests:: -- --ignored
cargo +stable test --workspace --doc
# prepared vanilla data, so without it they pass at once.
cargo +stable test --workspace --lib --features horizon-client/mods,horizon-mods/pack mod_content::tests:: -- --ignored
cargo +stable test --workspace --doc --features horizon-client/mods,horizon-mods/pack
# The launcher is its own workspace (launcher/Cargo.toml), so verify's cargo runs never build it. A pull request that
# changes it or a crate it builds is checked here, and so is every push to main: two pull requests can each pass
# alone and break it together (#2059 and the registry ratings did, 2026-10-26).
launcher:
runs-on: >-
${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && (vars.SELF_HOSTED_CI == 'all' ||
github.event_name == 'pull_request' && (endsWith(format('{0}', github.event.pull_request.number), '0') ||
endsWith(format('{0}', github.event.pull_request.number), '1'))) &&
fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
timeout-minutes: 45
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Does this pull request change the launcher or a crate it builds?
@@ -138,16 +95,12 @@ jobs:
run: python3 tools/ci/rust_changes.py --launcher "$BASE_SHA" "$HEAD_SHA" >> "$GITHUB_OUTPUT"
- name: Install Rust and the headers GPUI links against
if: steps.launcher.outputs.required != 'false'
env:
HOSTED: ${{ runner.environment == 'github-hosted' }}
run: |
if [ "$HOSTED" = true ]; then
sudo apt-get update && sudo apt-get install -y --no-install-recommends libxkbcommon-x11-dev libwayland-dev \
libx11-xcb-dev libfontconfig-dev libvulkan-dev libasound2-dev libudev-dev
fi
sudo apt-get update && sudo apt-get install -y --no-install-recommends libxkbcommon-x11-dev libwayland-dev \
libx11-xcb-dev libfontconfig-dev libvulkan-dev libasound2-dev libudev-dev
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
- uses: Swatinem/rust-cache@v2
if: steps.launcher.outputs.required != 'false' && runner.environment == 'github-hosted'
if: steps.launcher.outputs.required != 'false'
with:
workspaces: launcher
shared-key: launcher
@@ -158,114 +111,17 @@ jobs:
run: |
cargo +stable fmt --check
cargo +stable clippy --all-targets --locked -- -D warnings
cargo +stable test --locked --lib --bins
# Compile and test Windows ARM64 on an actual ARM64 host, and inspect the resulting PE headers. By hand only
# (`gh workflow run checks --ref <branch>`): GitHub's runner queue is the bottleneck, and main's runs gate the
# releases (tools/release_train.py waits for them), so neither pull requests nor main's pushes start this
# 90-minute job. The Nightly and launcher releases build and check these binaries on every run.
windows-arm64:
name: Windows ARM64 game and launcher
if: github.event_name == 'workflow_dispatch'
runs-on: windows-11-vs2026-arm
timeout-minutes: 90
env:
RUSTFLAGS: -C target-feature=+crt-static
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Use every runner core for Cargo
shell: pwsh
run: |
$cores = (Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
"CARGO_BUILD_JOBS=$cores" >> $env:GITHUB_ENV
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-pc-windows-msvc
- uses: Swatinem/rust-cache@v2
with:
shared-key: windows-arm64-game
- uses: Swatinem/rust-cache@v2
with:
workspaces: launcher
shared-key: windows-arm64-launcher
- name: Check LLVM assembler used by ring
shell: pwsh
run: |
$llvmBin = Join-Path $env:ProgramFiles 'LLVM\bin'
if (-not (Get-Command clang -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $llvmBin 'clang.exe'))) {
$env:PATH = "$llvmBin;$env:PATH"
"PATH=$env:PATH" >> $env:GITHUB_ENV
}
$clang = Get-Command clang -ErrorAction Stop
Write-Output "Using $($clang.Source)"
& $clang.Source --version
- name: Build the game for native Windows ARM64
run: cargo build --release --locked --target aarch64-pc-windows-msvc -p horizon-client -p horizon-client-lib --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Check the game's PE machine
run: python tools/check_pe_arch.py arm64 target/aarch64-pc-windows-msvc/release/horizon-client.exe target/aarch64-pc-windows-msvc/release/horizon-ctl.exe target/aarch64-pc-windows-msvc/release/horizon-prepare.exe target/aarch64-pc-windows-msvc/release/horizon-server.exe
- name: Check game DLL imports
run: python tools/check_imports.py target/aarch64-pc-windows-msvc/release/horizon-client.exe target/aarch64-pc-windows-msvc/release/horizon-ctl.exe target/aarch64-pc-windows-msvc/release/horizon-prepare.exe target/aarch64-pc-windows-msvc/release/horizon-server.exe
- name: Start every game executable
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$client = 'target/aarch64-pc-windows-msvc/release/horizon-client.exe'
$output = & $client --help 2>&1
if ($LASTEXITCODE -ne 0 -or ($output -join "`n") -notmatch '--resources') { throw 'horizon-client --help failed' }
foreach ($binary in @('horizon-ctl.exe', 'horizon-prepare.exe', 'horizon-server.exe')) {
$path = "target/aarch64-pc-windows-msvc/release/$binary"
$output = & $path --help 2>&1
if ($LASTEXITCODE -ne 0 -or -not $output) { throw "$binary --help failed" }
}
foreach ($binary in @('horizon-prepare.exe', 'horizon-server.exe')) {
$path = "target/aarch64-pc-windows-msvc/release/$binary"
$output = & $path --version 2>&1
if ($LASTEXITCODE -ne 0 -or -not $output) { throw "$binary --version failed" }
Write-Output ($output -join "`n")
}
- name: Test and build the native Windows ARM64 launcher
working-directory: launcher
run: |
cargo test --release --locked --target aarch64-pc-windows-msvc --lib
cargo build --release --locked --target aarch64-pc-windows-msvc
- name: Check the launcher's PE machine
working-directory: launcher
run: python ../tools/check_pe_arch.py arm64 target/aarch64-pc-windows-msvc/release/horizon-launcher.exe
- name: Check launcher DLL imports and command-line startup
working-directory: launcher
# bash, as launcher.yml's check on the same runner: the launcher is a GUI-subsystem program, and a pwsh
# `$output = & launcher --version` read no output from it (run 42217014588).
shell: bash
run: |
launcher=target/aarch64-pc-windows-msvc/release/horizon-launcher.exe
python ../tools/check_imports.py "$launcher"
out=$("$launcher" --version)
echo "$out"
[[ "$out" == "horizon-launcher "* ]] || { echo "::error::horizon-launcher --version printed '$out'"; exit 1; }
- name: Build a native-mod game with the launcher's managed toolchain
working-directory: launcher
shell: pwsh
env:
HORIZON_LAUNCHER_HOME: ${{ runner.temp }}\horizon-native-arm64-toolchain
HORIZON_NATIVE_TARGET_DIR: ${{ runner.temp }}\horizon-native-arm64-toolchain\target
run: |
$ErrorActionPreference = 'Stop'
cargo test --release --locked --target aarch64-pc-windows-msvc --lib rust_toolchain::tests::managed_arm64_toolchain_builds_the_game -- --ignored --exact --nocapture
$client = Join-Path $env:HORIZON_NATIVE_TARGET_DIR 'fast/horizon-client.exe'
python ../tools/check_pe_arch.py arm64 $client
python ../tools/check_imports.py $client
cargo +stable test --locked --lib
# Most pull requests leave the harnesses to parity-main, which runs them on main a batch of merges at a time
# (.github/workflows/parity-main.yml; releases wait for it). A pull request still runs them itself when it changes
# a harness, an oracle, a probe or the runner, or carries the run-parity label (run-parity.yml, when it is added).
parity-scope:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
# A git diff of this repository's own branch: one of our small runners (horizon-light, docs/ci-runners.md).
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
outputs:
run: ${{ steps.scope.outputs.run }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
@@ -277,7 +133,7 @@ jobs:
run: |
changed=$(git diff --name-only "$(git merge-base "$BASE_SHA" HEAD)" HEAD)
harness=$(grep -E '^(tools/test_[a-z0-9_]+_parity\.py|tools/(parity[a-z_]*|lab)\.py|tools/parity_hosts\.json|mods/horizon/src/test/|crates/[^/]+/examples/|crates/horizon-client/src/game/[a-z_]*(fixture|harness)\.rs|\.github/workflows/parity[a-z-]*\.yml)' <<< "$changed" || true)
if [ -n "$harness" ] || [[ " $LABELS " == *" run-parity "* ]] || [[ " $LABELS " == *" frame-identity "* ]]; then
if [ -n "$harness" ] || [[ " $LABELS " == *" run-parity "* ]]; then
echo "run=true" >> "$GITHUB_OUTPUT"
echo "The pull request runs its harnesses: ${harness:-label run-parity}"
else
@@ -292,7 +148,6 @@ jobs:
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
full: ${{ contains(github.event.pull_request.labels.*.name, 'full-parity') }}
frame_identity: ${{ contains(github.event.pull_request.labels.*.name, 'frame-identity') }}
# Horizon never downloads the game. These jobs need a self-hosted Linux runner
# (label horizon-game) holding the maintainer's own copy: the official launcher's
# game directory in HORIZON_MINECRAFT_DIR and the pinned server jar in
@@ -301,13 +156,13 @@ jobs:
if: github.event_name == 'workflow_dispatch'
runs-on: [self-hosted, linux, horizon-game]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal
- uses: actions/setup-java@v6
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
@@ -649,11 +504,11 @@ jobs:
if: github.event_name == 'workflow_dispatch'
runs-on: [self-hosted, linux, horizon-game]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-java@v6
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
-80
View File
@@ -1,80 +0,0 @@
name: claim command
# `/claim` and `/unclaim` comments: anyone takes an issue before working on it, and a daily run releases the claims
# older than 5 days that no open pull request links (CONTRIBUTING.md "Claim before you start",
# tools/ci/claim_command.py).
#
# Why no pull request's code runs here, though the token can write:
# - `issue_comment` and `schedule` always run this file from main, whoever comments and wherever.
# - It checks out main's script and list only, never a pull request; it reads threads through the API.
# - The comment's text and author reach the script as env vars, never in a shell line. The token stays in the step
# and can only write issues (assignees, comments, reactions).
on:
issue_comment:
types: [created]
schedule:
- cron: "23 5 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
command:
# Every other comment skips here, before a runner starts. Pull requests are answered by the script.
if: >-
github.event_name == 'issue_comment' && github.event.comment.user.type != 'Bot' && (
startsWith(github.event.comment.body, '/claim') ||
startsWith(github.event.comment.body, '/unclaim') ||
startsWith(github.event.comment.body, '🤖 Agent: /claim') ||
startsWith(github.event.comment.body, '🤖 Agent: /unclaim'))
# One command per issue at a time, so two `/claim`s can't both find it free. On the job, not the workflow:
# the skipped runs of ordinary comments must not cancel a waiting one.
concurrency:
group: claim-${{ github.event.issue.number }}
# One of our small runners (horizon-light), out of GitHub's pool: main's script only (docs/ci-runners.md).
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
timeout-minutes: 5
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v7
with:
ref: main
persist-credentials: false
sparse-checkout: |
.github/trusted-reviewers.txt
tools/ci/nightly_set.py
tools/ci/claim_command.py
sparse-checkout-cone-mode: false
- name: Run the command
env:
GH_TOKEN: ${{ github.token }}
BODY: ${{ github.event.comment.body }}
AUTHOR: ${{ github.event.comment.user.login }}
# Who may act is decided by this numeric ID: a login can be claimed by someone else after a rename.
AUTHOR_ID: ${{ github.event.comment.user.id }}
ISSUE: ${{ github.event.issue.number }}
COMMENT_ID: ${{ github.event.comment.id }}
REPO: ${{ github.repository }}
run: python3 tools/ci/claim_command.py command
release:
if: github.event_name != 'issue_comment' && github.repository == 'horizon-rust/horizon'
# One of our small runners (horizon-light), out of GitHub's pool: main's script only (docs/ci-runners.md).
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v7
with:
ref: main
persist-credentials: false
sparse-checkout: |
tools/ci/nightly_set.py
tools/ci/claim_command.py
sparse-checkout-cone-mode: false
- name: Release stale claims
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/claim_command.py release
-349
View File
@@ -1,349 +0,0 @@
name: game build
# Builds the game for every platform from `ref`: the Java exporters, then Linux and the Linux server, Windows x64
# and ARM64 on GitHub, and macOS on the Mac mini's runner with a 1500-frame offscreen run of a new world (smoke-test.json): Apple
# Silicon, and Intel cross-built there for x86_64-apple-darwin and checked under Rosetta 2 (#1161). Called by
# release.yml (the releases) and nightly-build.yml (the opt-in Nightly), always from main's copy of this file: the
# caller's commit, never the one being built. It holds no secrets: signing the checksums is the callers' own job,
# which never runs what this one built. See docs/release-flow.md.
on:
workflow_call:
inputs:
ref:
description: The commit to build
type: string
required: true
version:
description: The version built in (HORIZON_VERSION), e.g. 0.2.81 or nightly-2026-10-28
type: string
required: true
nightly:
description: A nightly's "<date> <short sha>" (HORIZON_NIGHTLY), shown by the game; empty for a release
type: string
default: ''
trusted:
# Only release.yml says true: it builds main. A nightly can hold pull requests nobody reviewed yet, whose
# build scripts and tests run here and could rewrite target/, the cargo home or a compiler cache. So an
# untrusted build never runs on the release runner, never saves the Rust cache (its key is its own anyway),
# and a trusted one never reads the machine's shared sccache, which pull requests' jobs write
# (tests/test_nightly.py pins all of it).
description: The source is main (a release); false for anything else (a nightly)
type: boolean
default: false
developer_id:
description: Sign the macOS programs with Developer ID and notarize the zip (the runner's keychain)
type: boolean
default: true
permissions:
contents: read
jobs:
exporters:
# This runner holds the maintainer's own pinned Minecraft jars. Only our classes leave it. An untrusted build
# goes to the Mac mini's nightly runner (macOS + horizon-nightly: ludovics-mac-mini-nightly, beside the one that
# runs pull requests' parity jobs, so a nightly never waits behind their queue), never the release runner
# (MAC_RUNNER: ludovics-mac-mini-release) nor parity-main's.
runs-on: ${{ inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-nightly"]') }}
# Three full preparations (two isolated, one batched) besides the compile.
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
clean: true
- uses: dtolnay/rust-toolchain@stable
- run: bash tools/ci/runner-env.sh
env:
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
- run: bash tools/ci/vanilla-data.sh
- name: Compile and validate original exporters against the pinned jars
shell: bash
run: |
set -euo pipefail
# target/ is gone after the clean checkout, and vanilla-data.sh builds the preparer only when it
# prepares afresh.
cargo build --release --locked -p horizon-prepare
# Compiled here, never taken from the runner's shared preparations: pull requests' parity jobs
# write those (vanilla-data.sh links .local/exporters to them).
rm -rf .local/exporters
bundle=$(target/release/horizon-prepare compile-exporters --root "$PWD" | tail -1)
mkdir -p exporters
cp -R "$bundle/server" "$bundle/client" "$bundle/manifest.json" exporters/
# Only our classes and their manifest leave this runner.
stray=$(find exporters -type f ! -path exporters/manifest.json \
! \( -name '*.class' \( -path 'exporters/server/dev/horizonlab/parity/*' -o -path 'exporters/client/dev/horizonlab/parity/*' \) \))
[ -z "$stray" ] || { echo "::error::not one of our classes: $stray"; exit 1; }
python3 tools/check_prepare_exporters.py --binary target/release/horizon-prepare
- uses: actions/upload-artifact@v7
with:
name: original-exporters
path: exporters/
if-no-files-found: error
retention-days: 3
build:
needs: exporters
name: build ${{ matrix.name }}
# Windows ARM64 and musl are new: a failure never holds the other platforms' release (the signing job
# needs this job, and a failed entry of it would fail it). The release steps leave the missing
# optional archives out with a warning.
continue-on-error: ${{ matrix.name == 'windows-arm64' || matrix.name == 'linux-musl' }}
strategy:
fail-fast: false
matrix:
include:
- name: linux
os: ubuntu-22.04
dir: horizon-linux-x86_64
asset: horizon-linux-x86_64.tar.gz
# The server build too: packaging/server/install.sh installs it on Linux.
server_dir: horizon-server-linux-x86_64
server_asset: horizon-server-linux-x86_64.tar.gz
- name: linux-musl
os: ubuntu-22.04
musl: true
dir: horizon-linux-musl-x86_64
asset: horizon-linux-musl-x86_64.tar.gz
- name: windows
os: windows-latest
dir: horizon-windows-x64
asset: horizon-windows-x64.zip
exe: .exe
pe_arch: x64
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
rustflags: -C target-feature=+crt-static
- name: windows-arm64
os: windows-11-vs2026-arm
target: aarch64-pc-windows-msvc
dir: horizon-windows-arm64
asset: horizon-windows-arm64.zip
exe: .exe
pe_arch: arm64
# The ARM64 runner builds and runs native ARM64 executables; keep the C runtime in the archive.
rustflags: -C target-feature=+crt-static
- name: macos
mac: true
dir: horizon-macos-arm64
asset: horizon-macos-arm64.zip
# Intel Macs: built on the same runner for another target; its checks and smoke test run the x86_64
# programs under Rosetta 2.
- name: macos-x64
mac: true
target: x86_64-apple-darwin
dir: horizon-macos-x64
asset: horizon-macos-x64.zip
runs-on: ${{ matrix.mac && (inputs.trusted && vars.MAC_RUNNER || fromJSON('["self-hosted", "macOS", "horizon-nightly"]')) || matrix.os }}
# Notarizing the zip can take a few minutes.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# The release's version, built in (horizon_core::release): --version, user agents, crash reports and
# telemetry say it. No version-bump commits.
HORIZON_VERSION: ${{ inputs.version }}
# A nightly's date and commit ("2026-10-28 0c02067"), which the game shows; empty for a release.
HORIZON_NIGHTLY: ${{ inputs.nightly }}
# Where cargo puts the programs: target/release, or target/<target>/release for another target.
BIN: ${{ matrix.target && format('target/{0}/release', matrix.target) || 'target/release' }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
# parity jobs, and nothing they leave (target/, an untracked .cargo/config.toml, a build script's output)
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
# keeps its preparations outside the checkout.
clean: true
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted' && !matrix.musl
with:
# A release restores only what a release saved: an untrusted build has its own key and saves nothing.
shared-key: ${{ inputs.trusted && 'release' || 'nightly' }}-${{ matrix.name }}
save-if: ${{ inputs.trusted }}
- name: ALSA and udev headers
if: runner.os == 'Linux' && !matrix.musl
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
if: ${{ !matrix.musl }}
run: cargo build --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} -p horizon-client -p horizon-client-lib --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Every program the release folder takes was built
if: ${{ !matrix.musl }}
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
test -f "$BIN/$binary${{ matrix.exe }}" || { echo "::error::$BIN/$binary${{ matrix.exe }} is missing"; exit 1; }
done
- name: Verify Windows executable architecture
if: runner.os == 'Windows'
run: |
python tools/check_pe_arch.py "${{ matrix.pe_arch }}" \
"$BIN/horizon-client${{ matrix.exe }}" "$BIN/horizon-ctl${{ matrix.exe }}" \
"$BIN/horizon-prepare${{ matrix.exe }}" "$BIN/horizon-server${{ matrix.exe }}"
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: packaging/macos/keychain.sh unlock
- name: Signed with Developer ID and the hardened runtime
# Before the checks below, so they run the programs as players will.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --force --options runtime --timestamp --keychain "$MACOS_SIGN_KEYCHAIN" \
--identifier "dev.horizon.${binary#horizon-}" --sign "$MACOS_SIGN_IDENTITY" "$BIN/$binary"
codesign --verify --strict --verbose=2 "$BIN/$binary"
done
- name: Only system libraries
if: ${{ !matrix.musl }}
# Windows: no non-system DLL (a "DLL not found" dialog on some players' machines). Linux: the standard
# loader and libraries every desktop distribution has. macOS: only the system's own libraries.
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
file="$BIN/$binary${{ matrix.exe }}"
if [[ "$RUNNER_OS" == macOS ]]; then
others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
[[ -z "$others" ]] || { echo "::error::$binary links $others"; exit 1; }
else
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
fi
done
- name: Baseline x86-64 CPUs only
# No POPCNT or LZCNT at all (the script says why only those): a C library's count intrinsic killed the
# game before main on a Core 2 (#7264). Not the Mac builds: every Intel Mac that runs macOS 11 has both.
if: runner.os != 'macOS' && matrix.pe_arch != 'arm64' && !matrix.musl
run: |
set -euo pipefail
rustup component add llvm-tools
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_cpu_baseline.py \
$(for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do echo "$BIN/$binary${{ matrix.exe }}"; done)
- name: Every program starts
if: ${{ !matrix.musl }}
run: |
set -euo pipefail
bin=$BIN
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
# The version is the release's.
for binary in horizon-prepare horizon-server; do
out=$("$bin/$binary${{ matrix.exe }}" --version | tr -d '\r')
echo "$out"
[[ "$out" == "$binary $HORIZON_VERSION" ]] || { echo "::error::expected $binary $HORIZON_VERSION"; exit 1; }
done
- name: Tools from the runner
if: runner.environment == 'self-hosted'
env:
HORIZON_NO_SCCACHE: ${{ inputs.trusted && '1' || '' }}
run: bash tools/ci/runner-env.sh
- name: Resources prepared from the runner's own Minecraft
if: runner.environment == 'self-hosted'
run: bash tools/ci/vanilla-data.sh
- name: The game runs (offscreen, a new world, 1500 frames)
if: runner.environment == 'self-hosted'
run: |
set -euo pipefail
# From a scratch folder: the game keeps its saves and options in the folder it starts in.
home="$RUNNER_TEMP/horizon-smoke"
rm -rf "$home" && mkdir -p "$home"
version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' research-lock.json | head -1)
client="$PWD/$BIN/horizon-client"
resources="$PWD/.local/horizon-client/$version"
(cd "$home" && HORIZON_HOME="$home" HORIZON_NO_AUDIO=1 "$client" --resources "$resources" --offscreen \
--world smoke --seed 1 --render-distance 4 --world-frames 1500 --timeout-seconds 180 \
--smoke-report "$home/smoke.json" > "$home/client.log" 2>&1) || { tail -40 "$home/client.log"; exit 1; }
python3 - "$home/smoke.json" <<'EOF'
import json, sys
report = json.load(open(sys.argv[1]))
print(json.dumps(report))
if report.get("error") is not None or report.get("frames", 0) < 1500 or not report.get("chunks") \
or not report.get("meshed_sections") or not report.get("player"):
sys.exit("the smoke run did not draw a world")
EOF
# Published with the release; the publish job checks it again before it publishes anything.
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
# One smoke-test.json per build, the Apple Silicon one's; the Intel build must pass its own run too.
- uses: actions/upload-artifact@v7
if: runner.environment == 'self-hosted' && !matrix.target
with:
name: smoke-test.json
path: ${{ runner.temp }}/smoke-test.json
if-no-files-found: error
retention-days: 3
- uses: actions/download-artifact@v8
with:
name: original-exporters
path: exporters
- name: Build, check and package for musl in Alpine
if: matrix.musl
# Run Docker on the Ubuntu host: JavaScript actions themselves require glibc.
run: |
docker run --rm -v "$PWD:/work" -w /work \
-e HORIZON_VERSION -e HORIZON_NIGHTLY \
rust:alpine3.22 sh tools/ci/build-musl.sh
- name: Package
if: ${{ !matrix.musl }}
run: |
set -euo pipefail
cargo run --locked -p xtask -- package "$BIN/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
cd dist
rm -f "${{ matrix.asset }}"
case "${{ matrix.asset }}" in
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
else
# Without resource forks and extended attributes: plain ditto adds an
# AppleDouble `._` file beside every file that has any.
ditto -c -k --norsrc --noextattr --noqtn --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"
if unzip -Z1 "${{ matrix.asset }}" | grep -E '(^|/)(\._|__MACOSX/)'; then
echo "::error::macOS metadata files in ${{ matrix.asset }}"; exit 1
fi
fi ;;
esac
ls -l "${{ matrix.asset }}"
- name: Notarized
# A zip of programs can't be stapled: Gatekeeper asks Apple the first time each one runs.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
done
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v7
with:
name: ${{ matrix.asset }}
path: dist/${{ matrix.asset }}
if-no-files-found: error
retention-days: 3
- name: The server build (the game without sound or controllers, as horizon-engine)
if: matrix.server_asset
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them).
cargo build --release --locked -p horizon-client --no-default-features --bin horizon-client
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
folder="dist/${{ matrix.server_dir }}"
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
"$folder/horizon-engine" --help | grep -q -- '--resources'
[[ "$("$folder/horizon-server" --version)" == "horizon-server $HORIZON_VERSION" ]]
cd dist
rm -f "${{ matrix.server_asset }}"
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
ls -l "${{ matrix.server_asset }}"
- uses: actions/upload-artifact@v7
if: matrix.server_asset
with:
name: ${{ matrix.server_asset }}
path: dist/${{ matrix.server_asset }}
if-no-files-found: error
retention-days: 3
- name: The signing keychain, locked again
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != '' && inputs.developer_id
run: packaging/macos/keychain.sh lock
+24 -118
View File
@@ -1,5 +1,5 @@
name: launcher
# Builds the launcher on Linux, macOS, Windows x64 and ARM64, and releases it by itself (docs/release-flow.md): after a
# Builds the launcher on Linux, macOS and Windows, and releases it by itself (docs/release-flow.md): after a
# push to main that changes the launcher or a crate it builds in (the paths below),
# every 10 minutes (a release waiting for merges to settle goes out) and by hand, tools/release_train.py decides as
# for the game.
@@ -8,16 +8,9 @@ name: launcher
# feature. The version and the tag are given to the build (HORIZON_LAUNCHER_VERSION, HORIZON_LAUNCHER_TAG): no bump
# commits.
# Never marked latest. Pushes to launcher branches only build. RELEASES_PAUSED=1 stops releases.
# Linux ships two archives of one binary: the .tar.gz the launcher updates itself from, and an
# AppImage (packaging/linux/appimage.sh) for players without a distribution package, which replaces
# itself instead (launcher/src/backend.rs).
# macOS: Apple Silicon, and Intel cross-built for x86_64-apple-darwin on the same runner, its tests and smoke test
# under Rosetta 2 (#1161). With the repository variable MACOS_SIGN_IDENTITY set, Horizon.app and the .dmg are signed with
# macOS: with the repository variable MACOS_SIGN_IDENTITY set, Horizon.app and the .dmg are signed with
# Developer ID, notarized and stapled, from the runner's signing keychain (packaging/macos/README.md);
# unset (forks), they are signed ad hoc.
# Windows ships each architecture's launcher in its ZIP (what a launcher updates itself from) and as
# horizon-launcher-setup.exe or horizon-launcher-setup-arm64.exe, which opens the per-user installer instead of Home
# (launcher/src/installer.rs).
# gh workflow run launcher --ref <branch> # dry run: builds everything, publishes nothing
# gh workflow run launcher --ref main -f dry_run=false -f force=true
@@ -25,19 +18,16 @@ on:
push:
branches: [main, launcher, 'launcher-*']
paths:
# Everything the launcher is built and packaged from, as tools/release_train.py's LAUNCHER_PATHS.
# Every crate the launcher builds in, as tools/release_train.py's LAUNCHER_PATHS.
- 'launcher/**'
- 'crates/horizon-bench/**'
- 'crates/horizon-friends/**'
- 'crates/horizon-mcp/**'
- 'crates/horizon-mods/**'
- 'crates/horizon-port-pack/**'
- 'crates/horizon-prepare/**'
- 'crates/horizon-release-sign/**'
- 'crates/horizon-telemetry/**'
- 'crates/horizon-theme/**'
- 'packaging/linux/**'
- 'packaging/icons/**'
- '.github/workflows/launcher.yml'
schedule:
- cron: '7-59/10 * * * *'
@@ -74,7 +64,7 @@ jobs:
version: ${{ steps.decide.outputs.version }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: mode
@@ -103,10 +93,6 @@ jobs:
needs: decide
if: needs.decide.outputs.release == 'true'
name: build ${{ matrix.name }}
# Windows ARM64 is new: its failure never holds the other platforms' release (the signing job
# needs this job, and a failed entry of it would fail it). The release steps leave the missing
# ARM64 files out with a warning.
continue-on-error: ${{ matrix.name == 'windows-arm64' }}
strategy:
fail-fast: false
matrix:
@@ -119,38 +105,18 @@ jobs:
mac: true
asset: horizon-launcher-macos-arm64.zip
binary: horizon-launcher
mac_arch: arm64
- name: macos-x64
mac: true
target: x86_64-apple-darwin
asset: horizon-launcher-macos-x64.zip
binary: horizon-launcher
mac_arch: x64
- name: windows
os: windows-latest
asset: horizon-launcher-windows-x64.zip
binary: horizon-launcher.exe
pe_arch: x64
setup_asset: horizon-launcher-setup.exe
# The C runtime linked in: without it the launcher asks for VCRUNTIME140.dll,
# which only the Visual C++ redistributable installs.
rustflags: -C target-feature=+crt-static
- name: windows-arm64
os: windows-11-vs2026-arm
target: aarch64-pc-windows-msvc
asset: horizon-launcher-windows-arm64.zip
binary: horizon-launcher.exe
pe_arch: arm64
setup_asset: horizon-launcher-setup-arm64.exe
# The C runtime linked in, so the standalone ARM64 launcher needs no redistributable.
rustflags: -C target-feature=+crt-static
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
# Notarizing the app and then the .dmg can take a few minutes each.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# Where cargo puts the launcher: target/release, or target/<target>/release for another target.
BIN: ${{ matrix.target && format('target/{0}/release', matrix.target) || 'target/release' }}
# Horizon's Azure app for Microsoft sign-in (docs/microsoft-account.md): a repository
# variable, not a secret. Unset, the launcher says sign-in is not configured.
HORIZON_MSA_CLIENT_ID: ${{ vars.HORIZON_MSA_CLIENT_ID }}
@@ -166,7 +132,7 @@ jobs:
working-directory: launcher
shell: bash
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
@@ -174,8 +140,6 @@ jobs:
# parity jobs, and nothing they leave may end up in a launcher release (target/ is rebuilt from scratch).
clean: true
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
with:
@@ -185,7 +149,7 @@ jobs:
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libxkbcommon-x11-dev libwayland-dev libx11-xcb-dev libfontconfig-dev libvulkan-dev libasound2-dev patchelf
sudo apt-get install -y libxkbcommon-x11-dev libwayland-dev libx11-xcb-dev libfontconfig-dev libvulkan-dev libasound2-dev
- name: Metal shaders compiled now, or at run time without the Metal toolchain
# GPUI compiles its Metal shaders when it builds; the Mac mini has Xcode without the Metal toolchain
# (xcodebuild -downloadComponent MetalToolchain), so there they are compiled when the launcher starts.
@@ -193,23 +157,20 @@ jobs:
run: |
if ! xcrun metal --version > /dev/null 2>&1; then
echo "::notice::No Metal toolchain on this runner: GPUI compiles its shaders at run time"
echo "LAUNCHER_FEATURES=gpui_platform/runtime_shaders" >> "$GITHUB_ENV"
echo "LAUNCHER_FEATURES=gpui/runtime_shaders" >> "$GITHUB_ENV"
fi
# Every test target (the library, the binary, tests/), on each system the launcher ships for.
- run: cargo test --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- run: cargo build --release --locked ${{ matrix.target && format('--target {0}', matrix.target) || '' }} ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- name: Verify Windows executable architecture
if: runner.os == 'Windows'
run: python ../tools/check_pe_arch.py "${{ matrix.pe_arch }}" "$BIN/${{ matrix.binary }}"
- run: cargo test --release --locked ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- run: cargo build --release --locked ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- name: The version is the release's
run: |
out=$("$BIN/${{ matrix.binary }}" --version)
out=$(target/release/${{ matrix.binary }} --version)
echo "$out"
expected="${HORIZON_LAUNCHER_VERSION:-$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)}"
[[ "$out" == "horizon-launcher $expected" ]] || { echo "::error::expected horizon-launcher $expected"; exit 1; }
- name: Only system libraries
run: |
file="$BIN/${{ matrix.binary }}"
file=target/release/${{ matrix.binary }}
case "$RUNNER_OS" in
Windows) python ../tools/check_imports.py "$file" ;;
macOS) others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
@@ -224,43 +185,20 @@ jobs:
export HORIZON_LAUNCHER_HOME="${RUNNER_TEMP//\\//}/horizon-launcher-smoke"
export HORIZON_HOME="$HORIZON_LAUNCHER_HOME/game"
rm -rf "$HORIZON_LAUNCHER_HOME"
launcher="$PWD/$BIN/${{ matrix.binary }}"
launcher="$PWD/target/release/${{ matrix.binary }}"
# GitHub's API rate-limits shared runner addresses now and then: try a few times.
retry() { for i in 1 2 3 4; do "$@" && return 0; sleep $((i * 15)); done; return 1; }
retry "$launcher" versions
"$launcher" links --json
# The Intel Mac launcher can come out before any game build has its asset (#1161): then there is nothing
# to install yet. Only that entry skips, and only on a version list that parsed and had no build here
# (exit 3); a launcher or a script that fails still fails the step. The Windows ARM64 launcher always has
# one: it installs the x64 build where a release has no ARM64 one (backend.rs, GAME_FALLBACK).
if [[ "${{ matrix.name }}" == macos-x64 ]]; then
versions=$(retry "$launcher" --json versions)
no_build='import json, sys; sys.exit(3 if not any(r["available_here"] for r in json.load(sys.stdin)) else 0)'
status=0
python3 -c "$no_build" <<<"$versions" || status=$?
if [[ $status == 3 ]]; then
echo "::warning::No game build has an asset for ${{ matrix.name }} yet: install not tested"
rm -rf "$HORIZON_LAUNCHER_HOME"
exit 0
fi
[[ $status == 0 ]] || exit $status
fi
retry "$launcher" install latest
"$launcher" versions --installed
# `install latest` is the Nightly since #3342 (versions/nightly-<date>-<commit>), a vX.Y.Z before.
clients=("$HORIZON_LAUNCHER_HOME"/versions/*/horizon-client*)
"$launcher" links --json
clients=("$HORIZON_LAUNCHER_HOME"/versions/v*/horizon-client*)
client="${clients[0]}"
echo "installed client: $client"
[[ -f "$client" ]] || { echo "::error::no game installed under versions/"; exit 1; }
# The game's Linux build is the game's to keep portable (release.yml checks it): a
# release linked to a Nix store is a warning here, not a broken launcher.
soft() { if [[ "$RUNNER_OS" == "Linux" ]]; then "$@" || echo "::warning::$* failed"; else "$@"; fi; }
if [[ "$RUNNER_OS" == "Windows" ]]; then
if ! python ../tools/check_pe_arch.py "${{ matrix.pe_arch }}" "$client"; then
# A Nightly without an ARM64 build: the ARM64 launcher installs the x64 one (backend.rs, GAME_FALLBACK).
[[ "${{ matrix.pe_arch }}" == arm64 ]] && python ../tools/check_pe_arch.py x64 "$client" || exit 1
echo "::warning::the latest game has no ARM64 build: the launcher installed the x64 one"
fi
python ../tools/check_imports.py "$client"
elif [[ "$RUNNER_OS" == "Linux" ]]; then
soft python3 ../tools/check_imports.py "$client"
@@ -268,18 +206,6 @@ jobs:
# The client starts: --help prints before any window or GPU is needed.
help() { "$client" --help > "$RUNNER_TEMP/help.txt" 2>&1 && head -3 "$RUNNER_TEMP/help.txt" && grep -q -- '--resources' "$RUNNER_TEMP/help.txt"; }
soft help
# The per-user Windows setup: the same binary under the setup's name installs into a scratch
# folder, and the copy it installs uninstalls itself. HKCU of the runner only.
if [[ "$RUNNER_OS" == "Windows" ]]; then
setup="$RUNNER_TEMP/setup-smoke"
cp "$launcher" "$RUNNER_TEMP/horizon-launcher-setup.exe"
"$RUNNER_TEMP/horizon-launcher-setup.exe" setup --quiet --dir "$setup"
installed="$setup/Horizon Launcher/horizon-launcher.exe"
[[ -f "$installed" ]] || { echo "::error::the setup copied no launcher"; exit 1; }
"$installed" uninstall --quiet
[[ ! -f "$installed" ]] || { echo "::error::uninstall left the launcher"; exit 1; }
rm -rf "$setup"
fi
rm -rf "$HORIZON_LAUNCHER_HOME"
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
@@ -296,18 +222,11 @@ jobs:
# Developer ID): packaging/macos.
python3 -m venv "$RUNNER_TEMP/dmg" && "$RUNNER_TEMP/dmg/bin/pip" install --quiet dmgbuild==1.6.5
version=${HORIZON_LAUNCHER_VERSION:-$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)}
DMGBUILD="$RUNNER_TEMP/dmg/bin/dmgbuild" ../packaging/macos/package.sh "$BIN/${{ matrix.binary }}" "$version" out ${{ matrix.mac_arch }}
DMGBUILD="$RUNNER_TEMP/dmg/bin/dmgbuild" ../packaging/macos/package.sh target/release/${{ matrix.binary }} "$version" out
rm -rf out/Horizon.app
else
version=${HORIZON_LAUNCHER_VERSION:-$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)}
if [[ "$RUNNER_OS" == "Linux" ]]; then
# The .tar.gz below is what the launcher updates itself from; the AppImage is what a
# player downloads and double-clicks, and it replaces itself instead
# (packaging/linux/appimage.sh, launcher/src/backend.rs).
../packaging/linux/appimage.sh "$BIN/${{ matrix.binary }}" "$version" out
fi
mkdir -p dist
cp "$BIN/${{ matrix.binary }}" dist/
cp target/release/${{ matrix.binary }} dist/
if [[ "${{ matrix.asset }}" == *.tar.gz ]]; then
(cd dist && tar czf ../out/${{ matrix.asset }} ${{ matrix.binary }})
else
@@ -315,13 +234,7 @@ jobs:
fi
fi
ls -l out
- name: Setup exe (Windows)
# The launcher's binary under another name: started with no arguments it shows the per-user
# installer instead of Home (launcher/src/installer.rs). Its name is what makes it the setup,
# so the ZIP's copy stays the launcher. Both go into the release's signed SHA256SUMS.
if: runner.os == 'Windows'
run: cp "$BIN/${{ matrix.binary }}" "out/${{ matrix.setup_asset }}"
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: launcher/out/*
@@ -339,12 +252,12 @@ jobs:
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-launcher-*
@@ -356,13 +269,13 @@ jobs:
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
TAG: ${{ needs.decide.outputs.tag }}
run: bash tools/ci/sign_release.sh "$TAG" dist/horizon-launcher-*
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
if: needs.decide.outputs.dry_run == 'true'
with:
name: ephemeral.pub
@@ -376,7 +289,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Notes
@@ -386,7 +299,7 @@ jobs:
python3 tools/release_train.py notes --kind launcher --tag "${{ needs.decide.outputs.tag }}" \
--head "${{ needs.decide.outputs.head }}" --out notes > /dev/null
{ echo "## $(cat notes/title.txt)"; echo; cat notes/body.md; } >> "$GITHUB_STEP_SUMMARY"
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
@@ -398,13 +311,6 @@ jobs:
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
# Never marked latest: Horizon's own releases stay the repository's latest.
- name: The Windows ARM64 launcher, if it built
# Its build may fail without holding the release (continue-on-error): the release then has no ARM64
# files, and ARM64 launchers stay on their version until one has (backend::launcher_update_from).
run: |
for file in horizon-launcher-windows-arm64.zip horizon-launcher-setup-arm64.exe; do
[[ -f "dist/$file" ]] || echo "::warning::$file is missing: this release goes out without it"
done
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
env:
@@ -419,7 +325,7 @@ jobs:
run: |
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }}: $(cat notes/title.txt)"
ls -l dist
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: launcher-release-notes
path: notes/
+2 -8
View File
@@ -15,23 +15,17 @@ on:
- '.github/workflows/mod-registry.yml'
permissions:
contents: read
concurrency:
group: mod-registry-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
check:
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's.
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: crates/horizon-mod-registry
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
# Ours keep their build folder between jobs.
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
with:
workspaces: crates/horizon-mod-registry
save-if: ${{ github.ref == 'refs/heads/main' }}
-32
View File
@@ -1,32 +0,0 @@
name: mojang republish
# Hourly: does Mojang's version manifest list a pinned-version JSON that research-lock.json does not know?
# If so, one issue "Mojang republished <version>: <sha8>" with the hashes and a lock snippet
# (tools/ci/mojang_republish.py). Reads only Mojang's public manifest.
# gh workflow run "mojang republish" -f dry_run=true
on:
schedule:
- cron: '23 * * * *' # hourly: a republished JSON breaks horizon-prepare for every new player until accepted
workflow_dispatch:
inputs:
dry_run:
description: Only print what it would do with issues
type: boolean
default: false
permissions:
contents: read
issues: write
concurrency:
group: mojang-republish
cancel-in-progress: false
jobs:
check:
# One of our small runners (label horizon-light, docs/ci-runners.md): main's script, Mojang's public manifest.
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Check the manifest
env:
GH_TOKEN: ${{ github.token }}
run: python3 tools/ci/mojang_republish.py ${{ inputs.dry_run && '--dry-run' || '' }}
-59
View File
@@ -1,59 +0,0 @@
name: native hook stamp
# Opens a PR that numbers pending fragments after they merge. Only pushes to
# main run main's own tool; fragments are read as data, not PR code.
on:
push:
branches: [main]
paths:
- 'tools/native-hook-fragments/*.json'
permissions:
contents: read
concurrency:
group: native-hook-stamp
cancel-in-progress: false
jobs:
stamp:
if: github.repository == 'horizon-rust/horizon'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
ref: main
fetch-depth: 0
- name: Open a stamp pull request
env:
GH_TOKEN: ${{ github.token }}
run: |
set -eu
branch=automation/native-hook-stamp
if [ -n "$(gh pr list --state open --head "$branch" --json number --jq '.[0].number // empty')" ]; then
echo "A stamp pull request is already open."
exit 0
fi
python3 tools/stamp_native_hooks.py
if git diff --quiet; then
echo "No fragments to stamp."
exit 0
fi
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git checkout -b "$branch"
git add -u
git commit -m 'Stamp native hook API fragments'
if git ls-remote --exit-code --heads origin "$branch" >/dev/null; then
git fetch origin "+refs/heads/$branch:refs/remotes/origin/$branch"
fi
git push --force-with-lease origin "$branch"
# The repo doesn't let GitHub Actions open pull requests (that setting would also let it approve them):
# the branch is pushed, and a maintainer opens the PR from it.
if ! gh pr create --base main --head "$branch" --title 'Stamp native hook API fragments' --body 'Assign API numbers to the pending native hook fragments. Close and reopen this PR to run CI.'; then
echo "::warning::Stamped fragments are on $branch; open a pull request from it (GitHub Actions may not open one here)."
fi
-315
View File
@@ -1,315 +0,0 @@
name: nightly build
run-name: nightly build${{ inputs.request && format(' (request {0})', inputs.request) || '' }}
# The Nightly, the only build Horizon ships since 2026-10-28 (docs/release-flow.md "Nightly"; Ludovic). When the batch
# train decides (release.yml, with the commit as `base`), once a night, and when a trusted
# reviewer asks (`/nightly 2640 2642`, nightly-command.yml), it builds main plus the pull requests the reviewers
# picked, merged in order (tools/ci/nightly_set.py), or main alone with none picked. Main is its newest commit where
# checks and parity-main passed (or the `base` input), as for a release, and no Nightly is published while a
# release-blocker is open. It publishes the result as the `nightly` pre-release, which every launcher and
# `horizon-server update` install. The previous nightly stays a week as nightly-<date>.
#
# Why the signing key never meets the pull requests' code:
# - `pick` merges the picked heads onto main with `git merge-tree --write-tree`: git objects only, no working tree,
# so none of their files reaches the disk and no hook, filter or script of theirs runs. It refuses any pull
# request that changes a path CODEOWNERS gives to Ludovic (this folder, tools/ci/, the signing and update code),
# pushes the merge as nightly/build-<date>-<n> and saves the set on the nightly-state branch.
# - This file, game-build.yml and every script below run from main's copy: a schedule runs the default branch, and
# `uses: ./…` takes the caller's commit. A picked pull request's own .github/ is never read.
# - `build` (game-build.yml, `trusted: false`) never reaches a release's tools: its macOS jobs run on the runner
# that already runs pull requests' parity jobs, never the release runner; its Rust cache has its own key and is
# never saved; and releases don't read the machine's shared sccache.
# - `build` (game-build.yml) checks out and compiles the merge with no secrets: its token reads contents only and
# isn't kept in the checkout; the macOS programs are signed ad hoc (Developer ID needs the runner's keychain,
# which stays locked for nightlies).
# - `sign` is the only job with RELEASE_SIGNING_KEY. It checks out nothing but main's tools/ci/sign_release.sh, which
# runs the runner's sha256sum and OpenSSL over the archives: it hashes them, never runs them. The signed message
# names the channel ("horizon release nightly"), so a nightly's signature can't pass for a release's.
# - `publish` verifies the signature with main's horizon-release-sign, checks the smoke test, and only then moves
# the tag. Like sign, it never executes what build produced.
# - The source ships with the build: `pick` writes horizon-source.tar.gz with `git archive` (git objects again, nothing
# run), `sign` signs it with the builds, and `publish` attaches it and tags the commit nightly-source/<date>-<short>
# for good (the dated release and the build branch go after 7 days; native mods pin the commit: docs/native-mods.md).
# gh workflow run "nightly build" # now (skips when the pick is the same commit as last night's)
# gh workflow run "nightly build" -f force=true # build and publish even if unchanged
# gh workflow run "nightly build" -f dry_run=true # build and sign with a throwaway key, publish nothing
on:
schedule:
- cron: '17 20 * * *' # 20:17 UTC: after the day's waves, before the 03:07 Paris parity audit takes the Mac mini
workflow_dispatch:
inputs:
force:
description: Build and publish even if tonight's pick is the commit already published
type: boolean
default: false
dry_run:
description: Build and sign with a throwaway key, publish nothing
type: boolean
default: false
issue:
description: The issue or pull request whose `/nightly` comment asked for this build (answered there)
type: string
default: ''
request:
description: The command's id, in the run's name so it can link the run
type: string
default: ''
prs:
description: A dry run's own set, `2640@<head> 2642@<head>` (the saved set stays)
type: string
default: ''
base:
description: The commit of main to build on (default main's newest with green checks and parity-main)
type: string
default: ''
urgent:
description: Past the day's cap of 12 builds (the train, for a release-now pull request or a forced run)
type: boolean
default: false
permissions:
contents: read
concurrency:
# One build at a time. A newer request replaces one still waiting: it builds the set as it is by then, so no pick
# is lost.
group: nightly-build
cancel-in-progress: false
jobs:
pick:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write # nightly/build-<date>-<n> and the nightly-state branch (main is protected)
issues: write # says in the thread when the picked pull requests conflict
pull-requests: write
actions: read # main's newest green checks and parity-main runs (the base)
outputs:
build: ${{ steps.pick.outputs.build }}
sha: ${{ steps.pick.outputs.sha }}
short: ${{ steps.pick.outputs.short }}
source: ${{ steps.pick.outputs.source }}
date: ${{ steps.pick.outputs.date }}
version: ${{ steps.pick.outputs.version }}
nightly: ${{ steps.pick.outputs.nightly }}
source_tag: ${{ steps.pick.outputs.source_tag }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- id: mode
# A dry run never publishes; a hand-started run off main is always one.
run: echo "dry_run=${{ github.event_name == 'workflow_dispatch' && (inputs.dry_run || github.ref != 'refs/heads/main') }}" >> "$GITHUB_OUTPUT"
- id: pick
env:
GH_TOKEN: ${{ github.token }}
FORCE: ${{ (inputs.force || steps.mode.outputs.dry_run == 'true') && 'yes' || '' }}
DRY_RUN: ${{ steps.mode.outputs.dry_run }}
NIGHTLY_ISSUE: ${{ inputs.issue }}
NIGHTLY_PRS: ${{ inputs.prs }}
NIGHTLY_BASE: ${{ inputs.base }}
NIGHTLY_URGENT: ${{ inputs.urgent }}
run: |
python3 tools/ci/nightly.py pick ${FORCE:+--force} | tee -a "$GITHUB_STEP_SUMMARY"
{ echo; cat nightly-notes.md; } >> "$GITHUB_STEP_SUMMARY"
- name: Old nightly/build-* branches deleted (scheduled runs only)
# Ludovic, 2026-10-27: older than 7 days, but never the newest build or one a published nightly points to;
# nothing but nightly/build-* (nightly.py stale_builds, tests/test_nightly.py).
if: github.event_name == 'schedule'
env:
GH_TOKEN: ${{ github.token }}
run: |
{ echo "### nightly/build-* branches"; python3 tools/ci/nightly.py prune-builds --days 7; } | tee -a "$GITHUB_STEP_SUMMARY"
- name: The source of the build (horizon-source.tar.gz)
if: steps.pick.outputs.build == 'true'
env:
SHA: ${{ steps.pick.outputs.sha }}
run: git archive --format=tar.gz --prefix="horizon-${SHA:0:7}/" -o horizon-source.tar.gz "$SHA"
- uses: actions/upload-artifact@v7
if: steps.pick.outputs.build == 'true'
with:
name: horizon-source
path: horizon-source.tar.gz
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v7
with:
name: nightly-notes
path: nightly-notes.md
retention-days: 3
build:
needs: pick
if: needs.pick.outputs.build == 'true'
uses: ./.github/workflows/game-build.yml
with:
ref: ${{ needs.pick.outputs.sha }}
version: ${{ needs.pick.outputs.version }}
nightly: ${{ needs.pick.outputs.nightly }}
developer_id: false
trusted: false # it can hold unreviewed pull requests: their own cache key, never saved, not the release runner
sign:
needs: [pick, build]
runs-on: ubuntu-latest
timeout-minutes: 10
# The key's only job: main's sign_release.sh hashes the archives and signs with the runner's OpenSSL.
environment: ${{ needs.pick.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v7
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v8
with:
path: dist
pattern: horizon-*
merge-multiple: true
- name: Sign the checksums for the nightly channel (SHA256SUMS.sig) and for this build (SHA256SUMS.v2.sig)
# SHA256SUMS.sig is for launchers from before v2 (signed for "nightly"); SHA256SUMS.v2.sig is signed for
# nightly-<date>-<commit>, the version the release title names, which current launchers and servers
# require (docs/security.md, "Nightly signatures"). The publish step titles the release from the same values.
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
ALSO_FOR: nightly-${{ needs.pick.outputs.date }}-${{ needs.pick.outputs.short }}
run: |
# Windows ARM64's build may fail without holding the Nightly (game-build.yml): then it is left out.
arm64=(); if [[ -f dist/horizon-windows-arm64.zip ]]; then arm64=(dist/horizon-windows-arm64.zip)
else echo "::warning::horizon-windows-arm64.zip is missing: SHA256SUMS goes out without it"; fi
musl=(); if [[ -f dist/horizon-linux-musl-x86_64.tar.gz ]]; then musl=(dist/horizon-linux-musl-x86_64.tar.gz)
else echo "::warning::horizon-linux-musl-x86_64.tar.gz is missing: SHA256SUMS goes out without it"; fi
bash tools/ci/sign_release.sh nightly dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
"${arm64[@]}" "${musl[@]}" \
dist/horizon-macos-arm64.zip dist/horizon-macos-x64.zip dist/horizon-server-linux-x86_64.tar.gz \
dist/horizon-source.tar.gz
- uses: actions/upload-artifact@v7
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v7
if: needs.pick.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
publish:
needs: [pick, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
issues: read # the open release-blocker issues (gate)
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- name: The signatures, checked as launchers check them
env:
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
DATE: ${{ needs.pick.outputs.date }}
SHORT: ${{ needs.pick.outputs.short }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag nightly --publishing "${key[@]}" dist
# The version's own signature, which current launchers require: the title below names the same version.
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "nightly-${DATE}-${SHORT}" \
--signature SHA256SUMS.v2.sig --publishing "${key[@]}" dist
- name: Ready to publish (the smoke test drew a world, no release-blocker is open)
# Right before publishing, as for a release: an issue labelled while the builds ran still holds it. A dry run
# only reports what a real run would do (that is how a blocker's fix is tried).
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ needs.pick.outputs.dry_run }}
run: |
if ! python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json --blockers; then
[[ "$DRY_RUN" == true ]] || { echo "::error::Nothing published"; exit 1; }
echo "::warning::A real run would publish nothing"
fi
- name: Publish as the `nightly` pre-release
if: needs.pick.outputs.dry_run != 'true'
env:
GH_TOKEN: ${{ github.token }}
SHA: ${{ needs.pick.outputs.sha }}
SHORT: ${{ needs.pick.outputs.short }}
DATE: ${{ needs.pick.outputs.date }}
SOURCE_TAG: ${{ needs.pick.outputs.source_tag }}
run: |
set -euo pipefail
python3 tools/ci/nightly.py rotate
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
[[ -f dist/SHA256SUMS.v2.sig ]] && signature+=(dist/SHA256SUMS.v2.sig)
# The title is what launchers parse (strictly) into the version SHA256SUMS.v2.sig is signed for.
[[ "${SHA:0:7}" == "$SHORT" ]] || { echo "::error::the build's commit isn't the one signed"; exit 1; }
# The ARM64 archive only when its build succeeded (the sign job left it out of SHA256SUMS otherwise).
arm64=()
if [[ -f dist/horizon-windows-arm64.zip ]]; then
arm64=("dist/horizon-windows-arm64.zip#Game files the launcher installs (Nightly): Windows ARM64 (horizon-windows-arm64.zip)")
else
echo "::warning::horizon-windows-arm64.zip is missing: the Nightly goes out without it (ARM64 launchers install the x64 build)"
fi
musl=()
if [[ -f dist/horizon-linux-musl-x86_64.tar.gz ]]; then
musl=("dist/horizon-linux-musl-x86_64.tar.gz#Game files for musl Linux (horizon-linux-musl-x86_64.tar.gz)")
else
echo "::warning::horizon-linux-musl-x86_64.tar.gz is missing: the Nightly goes out without it"
fi
# A pre-release, never GitHub's latest: the latest stays the last vX.Y.Z, which launchers and servers from
# before the Nightly-only switch keep (they don't read Nightlies).
gh release create nightly --prerelease --latest=false --target "$SHA" \
--title "Nightly $DATE · ${SHA:0:7}" --notes-file dist/nightly-notes.md \
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs (Nightly): Linux (horizon-linux-x86_64.tar.gz)" \
"${musl[@]}" \
"dist/horizon-windows-x64.zip#Game files the launcher installs (Nightly): Windows (horizon-windows-x64.zip)" \
"${arm64[@]}" \
"dist/horizon-macos-arm64.zip#Game files the launcher installs (Nightly): macOS on Apple Silicon (horizon-macos-arm64.zip)" \
"dist/horizon-macos-x64.zip#Game files the launcher installs (Nightly): macOS on Intel (horizon-macos-x64.zip)" \
"dist/horizon-server-linux-x86_64.tar.gz#Nightly dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
"dist/horizon-source.tar.gz#Source of this Nightly, commit ${SHA:0:7} (horizon-source.tar.gz; kept for good as the tag $SOURCE_TAG)" \
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
python3 tools/ci/nightly.py keep-source "$SHA" "$DATE"
python3 tools/ci/nightly.py prune --days 7
- name: Dry run, the nightly that would be published
if: needs.pick.outputs.dry_run == 'true'
run: |
echo "Would publish nightly at ${{ needs.pick.outputs.sha }} (${{ needs.pick.outputs.source }})"
ls -l dist
answer:
# Tells the thread that asked for this build how it ended. A conflict `pick` already explained there.
needs: [pick, build, sign, publish]
if: always() && inputs.issue != '' && needs.pick.result == 'success'
# One comment, no checkout and no secret: one of our small runners (label horizon-light, docs/ci-runners.md).
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
timeout-minutes: 5
permissions:
issues: write
pull-requests: write
steps:
- env:
GH_TOKEN: ${{ github.token }}
ISSUE: ${{ inputs.issue }}
BUILD: ${{ needs.pick.outputs.build }}
PUBLISHED: ${{ needs.publish.result }}
DRY_RUN: ${{ needs.pick.outputs.dry_run }}
NAME: Nightly ${{ needs.pick.outputs.date }} · ${{ needs.pick.outputs.short }}
RUN: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
if [[ "$BUILD" != true ]]; then
text="nothing new: the published Nightly is already this build."
elif [[ "$PUBLISHED" == success && "$DRY_RUN" == true ]]; then
text="the dry run built and signed $NAME; nothing was published. $RUN"
elif [[ "$PUBLISHED" == success ]]; then
text="published $NAME: https://github.com/$GITHUB_REPOSITORY/releases/tag/nightly. Launchers install it at their next check."
else
text="the build failed, nothing was published: $RUN"
fi
gh api "repos/$GITHUB_REPOSITORY/issues/$ISSUE/comments" -f body="🤖 Nightly: $text"
-48
View File
@@ -1,48 +0,0 @@
name: nightly command
# `/nightly …` comments: the trusted reviewers in .github/trusted-reviewers.txt (and Ludovic) pick the pull requests
# the Nightly holds (docs/release-flow.md "Nightly channel", tools/ci/nightly_set.py). This job only checks who asked,
# saves the set on the nightly-state branch, starts `nightly build` and answers in the thread.
#
# Why no pull request's code runs here, though the token can write:
# - `issue_comment` always runs this file from main, whoever comments and wherever; a pull request's own copy of it
# is never read.
# - It checks out main and runs main's nightly_set.py. The comment's text and author reach it as env vars, never in
# a shell line.
# - It never checks out, fetches or merges a pull request: it reads their file lists through the API. The merge
# happens in nightly-build.yml's `pick`, with git objects only; their code runs in its `build`, without secrets.
on:
issue_comment:
types: [created]
permissions:
contents: read
jobs:
command:
# Every other comment skips here, before a runner starts.
if: startsWith(github.event.comment.body, '/nightly') && github.event.comment.user.type != 'Bot'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # the nightly-state branch (main is protected)
actions: write # starts `nightly build`
issues: write # answers in the thread
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
ref: main
persist-credentials: false
sparse-checkout: |
.github/CODEOWNERS
.github/trusted-reviewers.txt
tools/ci/nightly_set.py
sparse-checkout-cone-mode: false
- name: Run the command
env:
GH_TOKEN: ${{ github.token }}
BODY: ${{ github.event.comment.body }}
AUTHOR: ${{ github.event.comment.user.login }}
# Who may act is decided by this numeric ID: a login can be claimed by someone else after a rename.
AUTHOR_ID: ${{ github.event.comment.user.id }}
ISSUE: ${{ github.event.issue.number }}
COMMENT_URL: ${{ github.event.comment.html_url }}
run: python3 tools/ci/nightly_set.py command
+2 -2
View File
@@ -48,7 +48,7 @@ jobs:
else
echo "run=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v7
- uses: actions/checkout@v4
if: steps.when.outputs.run == 'true'
with:
fetch-depth: 0
@@ -82,7 +82,7 @@ jobs:
run: python3 tools/ci/parity_issues.py --since "${{ steps.audit.outputs.since }}" ${{ inputs.dry_run && '--dry-run' || '' }} | tee -a "$GITHUB_STEP_SUMMARY"
- name: Logs of the failed harnesses
if: steps.when.outputs.run == 'true' && steps.audit.outputs.exit != '0'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: nightly-parity-logs
path: .local/parity-runner/logs/
-43
View File
@@ -1,43 +0,0 @@
name: overlap note
# One comment on a pull request naming the other open pull requests that close the same issue or change the same
# functions, edited on every push (tools/ci/overlap_note.py, issue #3859).
#
# Why no pull request's code runs here, though `pull_request_target` gives a token that can write:
# - `pull_request_target` runs this file and the script from main, never the pull request's copy.
# - The checkout is main's (`ref: main`), blobless, with the script alone on disk. The pull requests' heads are fetched
# as git objects and only diffed, never checked out; git runs with hooks, fsmonitor, external diff and textconv off.
# - The rest comes from the API. The token writes the comment only.
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
permissions:
contents: read
jobs:
note:
if: github.event.pull_request.base.ref == 'main'
# A newer push replaces the note being computed.
concurrency:
group: overlap-note-${{ github.event.pull_request.number }}
cancel-in-progress: true
# One of our small runners (horizon-light), out of GitHub's pool: it runs main's script only (above), so a fork's
# pull request runs none of its code there (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
ref: main
fetch-depth: 0
filter: blob:none
persist-credentials: false
sparse-checkout: |
tools/ci/overlap_note.py
sparse-checkout-cone-mode: false
- name: Note
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/overlap_note.py event
-72
View File
@@ -1,72 +0,0 @@
name: pack load bench
# How long resource packs take to load: crates/horizon-client-render/examples/pack_load_bench.rs on generated packs
# (no game files), on the branch and then on `base` with the branch's copy of the bench, on the same runner, so a pull
# request can show its numbers next to main's. Started by hand, or by a push to a `pack-bench/*` branch (a fork can't
# start a workflow its main lacks), against horizon-rust/horizon's main.
on:
push:
branches: ['pack-bench/**']
workflow_dispatch:
inputs:
base:
description: Ref to compare against
default: main
scale:
description: How many times the default pack sizes
default: '1'
permissions:
contents: read
env:
HORIZON_TELEMETRY_URL: "off"
CARGO_TERM_COLOR: never
jobs:
bench:
# Our checks runners (label horizon-checks, docs/ci-runners.md): the branch and the base on the same machine, as
# the comparison needs. SELF_HOSTED_CI=light or off: GitHub's.
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install Rust and the ALSA and udev headers
env:
HOSTED: ${{ runner.environment == 'github-hosted' }}
run: |
if [ "$HOSTED" = true ]; then
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
fi
rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
with:
shared-key: pack-bench
- name: Bench the branch
env:
SCALE: ${{ inputs.scale || '1' }}
run: |
cargo run --profile fast -p horizon-client-render --example pack_load_bench -- --scale "$SCALE" > head.md
- name: Bench the base, with the branch's bench
env:
BASE: ${{ inputs.base || 'main' }}
SCALE: ${{ inputs.scale || '1' }}
run: |
git fetch https://github.com/horizon-rust/horizon.git "$BASE"
# Our runners keep the folder beside the checkout between jobs.
git worktree remove --force ../base 2> /dev/null || rm -rf ../base
git worktree prune
git worktree add ../base FETCH_HEAD
cp crates/horizon-client-render/examples/pack_load_bench.rs ../base/crates/horizon-client-render/examples/
# A bench row that times a function the base lacks keeps it from building there: then
# the branch's own rows compare the two ways.
target=${CARGO_TARGET_DIR:-$GITHUB_WORKSPACE/target}
(cd ../base && CARGO_TARGET_DIR="$target" cargo run --profile fast -p horizon-client-render \
--example pack_load_bench -- --scale "$SCALE") > base.md \
|| echo "The bench does not build on the base." > base.md
- name: Summary
run: |
{
echo "### Branch ($(git rev-parse --short HEAD))"; cat head.md
echo; echo "### Base (${{ inputs.base || 'main' }})"; cat base.md
} | tee -a "$GITHUB_STEP_SUMMARY"
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
runs-on: [self-hosted, macOS, horizon-parity-main]
timeout-minutes: 240 # a core-crate batch reruns ~300 harnesses (~60-90 min), a bisect a few rounds of the failures
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
fetch-depth: 0
@@ -69,7 +69,7 @@ jobs:
python3 tools/parity_main.py bisect "$BASE_SHA" --comment
- name: Logs of the failed harnesses
if: steps.harnesses.outcome == 'failure'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: parity-main-logs
path: .local/parity-main-logs/
+2 -14
View File
@@ -20,12 +20,6 @@ on:
busy the job waits for it.
type: boolean
default: false
frame_identity:
description: >-
The pull request carries the frame-identity label: it must leave the picture alone (HDR, shader packs), so
the frame_identity harness, which no automatic selection picks, runs too.
type: boolean
default: false
permissions:
contents: read
env:
@@ -39,7 +33,7 @@ jobs:
# harnesses at a time) up to a few hours, and any pull request's job may land there.
timeout-minutes: ${{ inputs.full && 360 || 180 }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
# The commit the event names: a fork's later pushes need another run-parity label. The next step merges
# main into it.
@@ -93,15 +87,9 @@ jobs:
PARITY_HOSTS: ''
BASE_SHA: ${{ steps.merge.outputs.base }}
run: python3 tools/parity.py --changed "$BASE_SHA" --timeout 2400 # world_spawn takes ~1480 s on a busy runner
- name: The pictures this change must leave alone
if: ${{ !cancelled() && inputs.frame_identity }}
env:
PARITY_HOSTS: ''
BASE_SHA: ${{ steps.merge.outputs.base }}
run: python3 tools/parity.py frame_identity --timeout 2400
- name: Logs of the failed harnesses
if: failure()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: parity-logs
path: .local/parity-runner/logs/
-38
View File
@@ -1,38 +0,0 @@
name: pr labels
# A pull request's kind label (the box ticked in the template) and area labels (the paths it changes):
# tools/ci/pr_labels.py, CONTRIBUTING.md "Pull requests".
#
# Why no pull request's code runs here, though `pull_request_target` gives a token that can write to fork PRs:
# - `pull_request_target` runs this file and the script from main, never the pull request's copy.
# - It checks out main's script only (`ref: main`), never the pull request's head or merge commit.
# - The script reads the event file (the description is data in JSON, never in a shell line) and the API's list of
# changed file names; it runs, builds and imports nothing of theirs. The token writes labels only.
on:
pull_request_target:
types: [opened, reopened, synchronize, edited, ready_for_review]
permissions:
contents: read
jobs:
label:
# Title-only edits change nothing: skip them before a runner starts.
if: github.event.action != 'edited' || github.event.changes.body != null
# One of our small runners (horizon-light), out of GitHub's pool: it runs main's script only (above), so a fork's
# pull request runs none of its code there (docs/ci-runners.md). SELF_HOSTED_CI=off: GitHub's runners.
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
ref: main
persist-credentials: false
sparse-checkout: |
tools/ci/pr_labels.py
sparse-checkout-cone-mode: false
- name: Label
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: python3 tools/ci/pr_labels.py event
+381 -35
View File
@@ -1,15 +1,22 @@
name: release
# Automatic game releases (docs/release-flow.md). Horizon ships only the Nightly since 2026-10-28 (Ludovic): this
# workflow is the batch train that decides when a new Nightly goes out, and `nightly build` (nightly-build.yml) builds,
# signs and publishes it. After every green `checks` run on main, every 10 minutes (so a batch waiting for merges to
# settle goes out without another merge) and by hand, tools/release_train.py decides, for a commit whose checks and
# parity-main (the vanilla harnesses on main) both passed: a Nightly when 5 pull requests merged since the published
# Nightly changed what players run and the newest merged 5 minutes ago, or when the oldest has waited 6 hours, or at
# once for a pull request labelled release-now. RELEASES_PAUSED=1 (repository variable) stops it; so does an open issue
# labelled release-blocker, forced runs too (nightly-build.yml checks again before publishing). One Nightly build at a
# time, at most 12 a day (release-now and force=true pass the cap). No `vX.Y.Z` game release is made any more.
# gh workflow run release --ref <branch> # dry run: decides, starts nothing
# gh workflow run release -f dry_run=false -f force=true # on main: a Nightly now, whatever the rules say
# Automatic game releases (docs/release-flow.md). After every green `checks` run on main, every 10 minutes (so a
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides,
# for a commit whose checks and parity-main (the vanilla harnesses on main) both passed:
# a release when 5 pull requests merged since the last one changed what players run and the newest merged 5 minutes
# ago, or when the oldest has waited 6 hours, or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them; so does an open issue labelled release-blocker, forced runs too.
# Builds Linux (an older Ubuntu, so the binary runs on any recent distribution and on NixOS through nix-ld) and
# Windows on GitHub, macOS on the Mac mini's runner (MAC_RUNNER=macos-latest sends it to GitHub), then
# publishes vX.Y.Z on the release line in .github/release-line.txt (docs/versioning.md; without it, the dated
# vYYYY.MM.DD[.N] from before 0.2) with notes from the merged pull requests, as the release and GitHub's latest: one
# channel, which launchers, `horizon-server update` and install.sh follow at once. So it is published only after the
# checks that used to gate a promotion pass in this run: the Mac mini's smoke test drew a world, and no release-blocker
# is open (tools/release_train.py gate); otherwise nothing is published. macOS: with the repository variable
# MACOS_SIGN_IDENTITY set, the programs are signed with
# Developer ID and the hardened runtime and the zip is notarized, from the runner's signing keychain
# (packaging/macos/README.md); unset (forks), ad hoc.
# gh workflow run release --ref <branch> # dry run: builds and notes, publishes nothing
# gh workflow run release -f dry_run=false -f force=true # on main: release now, whatever the rules say
on:
workflow_run:
# A game release ships a commit both passed on (parity-main runs on batches: the newest commit it passed on).
@@ -21,24 +28,19 @@ on:
workflow_dispatch:
inputs:
dry_run:
description: Decide only; start no Nightly build
description: Build everything and write the notes, but publish nothing
type: boolean
default: true
force:
description: A Nightly even without player-facing changes, before merges settle, while paused or past the day's cap
description: Release even without player-facing changes, before merges settle or while paused
type: boolean
default: false
permissions:
contents: read
# The open release-blocker issues (decide).
issues: read
# The checks, parity-main and nightly build runs (decide).
actions: read
concurrency:
# One release at a time. A hand-started dry run publishes nothing, so it can't race a real release: it gets a
# group of its own. In the shared group, a release queued behind it replaced it as the pending run and GitHub
# cancelled it (#2779's dry run, 2026-10-27), which happens all day while releases go out in batches.
group: game-release${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && '-dry-run' || '' }}
group: game-release
cancel-in-progress: false
jobs:
decide:
@@ -49,11 +51,12 @@ jobs:
timeout-minutes: 10
outputs:
release: ${{ steps.decide.outputs.release }}
tag: ${{ steps.decide.outputs.tag }}
version: ${{ steps.decide.outputs.version }}
head: ${{ steps.decide.outputs.head }}
urgent: ${{ steps.decide.outputs.urgent }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: mode
@@ -70,21 +73,364 @@ jobs:
run: |
python3 tools/release_train.py decide --kind game ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
# The version the programs are built with (docs/versioning.md): the tag without its v.
tag=$(jq -r '.tag' decision.json)
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
# A dry run builds whatever the decision, to test the builds.
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then echo "release=true" >> "$GITHUB_OUTPUT"; fi
nightly:
# The Nightly is the release: nightly-build.yml builds the decided commit plus the reviewers' picks, signs it for
# the Nightly channel and publishes it once its smoke test passed and no release-blocker is open.
exporters:
needs: decide
if: needs.decide.outputs.release == 'true' && needs.decide.outputs.dry_run != 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: write # starts `nightly build`
if: needs.decide.outputs.release == 'true'
# This runner holds the maintainer's own pinned Minecraft jars. Only our classes leave it.
runs-on: ${{ vars.MAC_RUNNER || 'ludovics-mac-mini' }}
# Three full preparations (two isolated, one batched) besides the compile.
timeout-minutes: 60
steps:
- env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ needs.decide.outputs.head }}
URGENT: ${{ needs.decide.outputs.urgent }}
- uses: actions/checkout@v4
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
clean: true
- uses: dtolnay/rust-toolchain@stable
- run: bash tools/ci/runner-env.sh
- run: bash tools/ci/vanilla-data.sh
- name: Compile and validate original exporters against the pinned jars
shell: bash
run: |
gh workflow run nightly-build.yml --repo "$GITHUB_REPOSITORY" --ref main -f base="$HEAD_SHA" -f urgent="$URGENT"
echo "### Started \`nightly build\` on ${HEAD_SHA:0:9}" >> "$GITHUB_STEP_SUMMARY"
set -euo pipefail
# target/ is gone after the clean checkout, and vanilla-data.sh builds the preparer only when it
# prepares afresh.
cargo build --release --locked -p horizon-prepare
# Compiled here, never taken from the runner's shared preparations: pull requests' parity jobs
# write those (vanilla-data.sh links .local/exporters to them).
rm -rf .local/exporters
bundle=$(target/release/horizon-prepare compile-exporters --root "$PWD" | tail -1)
mkdir -p exporters
cp -R "$bundle/server" "$bundle/client" "$bundle/manifest.json" exporters/
# Only our classes and their manifest leave this runner.
stray=$(find exporters -type f ! -path exporters/manifest.json \
! \( -name '*.class' \( -path 'exporters/server/dev/horizonlab/parity/*' -o -path 'exporters/client/dev/horizonlab/parity/*' \) \))
[ -z "$stray" ] || { echo "::error::not one of our classes: $stray"; exit 1; }
python3 tools/check_prepare_exporters.py --binary target/release/horizon-prepare
- uses: actions/upload-artifact@v4
with:
name: original-exporters
path: exporters/
if-no-files-found: error
retention-days: 3
build:
needs: [decide, exporters]
if: needs.decide.outputs.release == 'true'
name: build ${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
- name: linux
os: ubuntu-22.04
dir: horizon-linux-x86_64
asset: horizon-linux-x86_64.tar.gz
# The server build too: packaging/server/install.sh installs it on Linux.
server_dir: horizon-server-linux-x86_64
server_asset: horizon-server-linux-x86_64.tar.gz
- name: windows
os: windows-latest
dir: horizon-windows-x64
asset: horizon-windows-x64.zip
exe: .exe
# The C runtime linked in, so the game runs without the Visual C++ redistributable.
rustflags: -C target-feature=+crt-static
- name: macos
mac: true
dir: horizon-macos-arm64
asset: horizon-macos-arm64.zip
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
# Notarizing the zip can take a few minutes.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# The release's version, built in (horizon_core::release): --version, user agents, crash reports and
# telemetry say it. No version-bump commits.
HORIZON_VERSION: ${{ needs.decide.outputs.version }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
# parity jobs, and nothing they leave (target/, an untracked .cargo/config.toml, a build script's output)
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
# keeps its preparations outside the checkout.
clean: true
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
with:
shared-key: release-${{ matrix.name }}
- name: ALSA and udev headers
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
# `horizon-client/mods`: every released game runs code mods (the feature is off in dev builds).
run: cargo build --release --locked --features horizon-client/mods -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Every program the release folder takes was built
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
test -f "target/release/$binary${{ matrix.exe }}" || { echo "::error::target/release/$binary${{ matrix.exe }} is missing"; exit 1; }
done
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: packaging/macos/keychain.sh unlock
- name: Signed with Developer ID and the hardened runtime
# Before the checks below, so they run the programs as players will.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --force --options runtime --timestamp --keychain "$MACOS_SIGN_KEYCHAIN" \
--identifier "dev.horizon.${binary#horizon-}" --sign "$MACOS_SIGN_IDENTITY" "target/release/$binary"
codesign --verify --strict --verbose=2 "target/release/$binary"
done
- name: Only system libraries
# Windows: no non-system DLL (a "DLL not found" dialog on some players' machines). Linux: the standard
# loader and libraries every desktop distribution has. macOS: only the system's own libraries.
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
file="target/release/$binary${{ matrix.exe }}"
if [[ "$RUNNER_OS" == macOS ]]; then
others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
[[ -z "$others" ]] || { echo "::error::$binary links $others"; exit 1; }
else
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
fi
done
- name: Every program starts
run: |
set -euo pipefail
bin=target/release
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
# The version is the release's.
for binary in horizon-prepare horizon-server; do
out=$("$bin/$binary${{ matrix.exe }}" --version | tr -d '\r')
echo "$out"
[[ "$out" == "$binary $HORIZON_VERSION" ]] || { echo "::error::expected $binary $HORIZON_VERSION"; exit 1; }
done
- name: Tools from the runner
if: runner.environment == 'self-hosted'
run: bash tools/ci/runner-env.sh
- name: Resources prepared from the runner's own Minecraft
if: runner.environment == 'self-hosted'
run: bash tools/ci/vanilla-data.sh
- name: The game runs (offscreen, a new world, 1500 frames)
if: runner.environment == 'self-hosted'
run: |
set -euo pipefail
# From a scratch folder: the game keeps its saves and options in the folder it starts in.
home="$RUNNER_TEMP/horizon-smoke"
rm -rf "$home" && mkdir -p "$home"
version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' research-lock.json | head -1)
client="$PWD/target/release/horizon-client"
resources="$PWD/.local/horizon-client/$version"
(cd "$home" && HORIZON_HOME="$home" HORIZON_NO_AUDIO=1 "$client" --resources "$resources" --offscreen \
--world smoke --seed 1 --render-distance 4 --world-frames 1500 --timeout-seconds 180 \
--smoke-report "$home/smoke.json" > "$home/client.log" 2>&1) || { tail -40 "$home/client.log"; exit 1; }
python3 - "$home/smoke.json" <<'EOF'
import json, sys
report = json.load(open(sys.argv[1]))
print(json.dumps(report))
if report.get("error") is not None or report.get("frames", 0) < 1500 or not report.get("chunks") \
or not report.get("meshed_sections") or not report.get("player"):
sys.exit("the smoke run did not draw a world")
EOF
# Published with the release; the publish job checks it again before it publishes anything.
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
- uses: actions/upload-artifact@v4
if: runner.environment == 'self-hosted'
with:
name: smoke-test.json
path: ${{ runner.temp }}/smoke-test.json
if-no-files-found: error
retention-days: 3
- uses: actions/download-artifact@v4
with:
name: original-exporters
path: exporters
- name: Package
run: |
set -euo pipefail
cargo run --locked -p xtask -- package "target/release/horizon-client${{ matrix.exe }}" "dist/${{ matrix.dir }}"
cd dist
rm -f "${{ matrix.asset }}"
case "${{ matrix.asset }}" in
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
else
# Without resource forks and extended attributes: plain ditto adds an
# AppleDouble `._` file beside every file that has any.
ditto -c -k --norsrc --noextattr --noqtn --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"
if unzip -Z1 "${{ matrix.asset }}" | grep -E '(^|/)(\._|__MACOSX/)'; then
echo "::error::macOS metadata files in ${{ matrix.asset }}"; exit 1
fi
fi ;;
esac
ls -l "${{ matrix.asset }}"
- name: Notarized
# A zip of programs can't be stapled: Gatekeeper asks Apple the first time each one runs.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
done
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: dist/${{ matrix.asset }}
if-no-files-found: error
retention-days: 3
- name: The server build (the game without sound or controllers, as horizon-engine)
if: matrix.server_asset
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them),
# and with code mods like the client.
cargo build --release --locked -p horizon-client --no-default-features --features mods --bin horizon-client
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
folder="dist/${{ matrix.server_dir }}"
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
"$folder/horizon-engine" --help | grep -q -- '--resources'
[[ "$("$folder/horizon-server" --version)" == "horizon-server $HORIZON_VERSION" ]]
cd dist
rm -f "${{ matrix.server_asset }}"
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
ls -l "${{ matrix.server_asset }}"
- uses: actions/upload-artifact@v4
if: matrix.server_asset
with:
name: ${{ matrix.server_asset }}
path: dist/${{ matrix.server_asset }}
if-no-files-found: error
retention-days: 3
- name: The signing keychain, locked again
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: packaging/macos/keychain.sh lock
sign:
needs: [decide, build]
runs-on: ubuntu-latest
timeout-minutes: 10
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-*
merge-multiple: true
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
bash tools/ci/sign_release.sh "$TAG" dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
if: needs.decide.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
publish:
needs: [decide, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
issues: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Notes
env:
GH_TOKEN: ${{ github.token }}
run: |
python3 tools/release_train.py notes --kind game --tag "${{ needs.decide.outputs.tag }}" \
--head "${{ needs.decide.outputs.head }}" --out notes > /dev/null
{ echo "## $(cat notes/title.txt)"; echo; cat notes/body.md; } >> "$GITHUB_STEP_SUMMARY"
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: The signature, checked as launchers check it
env:
TAG: ${{ needs.decide.outputs.tag }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
- name: Ready to publish (the smoke test passed, no release-blocker is open)
# Right before publishing: an issue labelled while the builds ran still holds this release. A dry run only
# reports what a real run would do.
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ needs.decide.outputs.dry_run }}
run: |
if ! python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json --blockers; then
[[ "$DRY_RUN" == true ]] || { echo "::error::Nothing published"; exit 1; }
echo "::warning::A real run would publish nothing"
fi
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
# The release and the latest at once: there is one channel (docs/release-flow.md). Each archive gets a
# label (file#label), which GitHub's page shows instead of the file name, so nobody takes the game's
# files for the download (#1647). Launchers, servers and install.sh match the name, which stays.
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --latest \
"dist/horizon-linux-x86_64.tar.gz#Game files the launcher installs: Linux (horizon-linux-x86_64.tar.gz)" \
"dist/horizon-windows-x64.zip#Game files the launcher installs: Windows (horizon-windows-x64.zip)" \
"dist/horizon-macos-arm64.zip#Game files the launcher installs: macOS (horizon-macos-arm64.zip)" \
"dist/horizon-server-linux-x86_64.tar.gz#Dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
- name: Dry run, the release that would be published
if: needs.decide.outputs.dry_run == 'true'
run: |
echo "Would publish ${{ needs.decide.outputs.tag }} at ${{ needs.decide.outputs.head }} as the release and latest: $(cat notes/title.txt)"
ls -l dist
- uses: actions/upload-artifact@v4
with:
name: release-notes
path: notes/
retention-days: 3
+2 -7
View File
@@ -5,15 +5,11 @@ name: run-parity
# skipped `verify`, and GitHub counts a skipped required check as passed.
on:
pull_request:
# A push starts a run only to cancel the old head's (see concurrency): the job runs on the label alone.
types: [labeled, synchronize]
types: [labeled]
permissions:
contents: read
concurrency:
# The run-parity label and pushes share one group per pull request, so the newest run cancels the one before: a
# pushed pull request's run tested a head the merge would no longer land. Any other label gets a group of its own:
# in the shared one, adding needs-proof cancelled the parity run queued seconds before (run 40766180150).
group: run-parity-${{ github.event.pull_request.number }}${{ github.event.action == 'labeled' && github.event.label.name != 'run-parity' && format('-{0}', github.run_id) || '' }}
group: run-parity-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
@@ -27,4 +23,3 @@ jobs:
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
full: ${{ contains(github.event.pull_request.labels.*.name, 'full-parity') }}
frame_identity: ${{ contains(github.event.pull_request.labels.*.name, 'frame-identity') }}
+2 -8
View File
@@ -15,23 +15,17 @@ on:
- '.github/workflows/telemetry-server.yml'
permissions:
contents: read
concurrency:
group: telemetry-server-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
check:
# Our checks runners (label horizon-checks, docs/ci-runners.md); SELF_HOSTED_CI=light or off: GitHub's.
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && vars.SELF_HOSTED_CI != 'light' && fromJSON('["self-hosted", "linux", "horizon-checks"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: crates/horizon-telemetry-server
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
# Ours keep their build folder between jobs.
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
with:
workspaces: crates/horizon-telemetry-server
save-if: ${{ github.ref == 'refs/heads/main' }}
-51
View File
@@ -1,51 +0,0 @@
name: triage command
# `/close`, `/reopen`, `/label`, `/unlabel` comments: the trusted reviewers in .github/trusted-reviewers.txt (and
# Ludovic) triage issues and pull requests without any repository role (docs/release-flow.md "Trusted reviewers",
# tools/ci/triage_command.py).
#
# Why no pull request's code runs here, though the token can write:
# - `issue_comment` always runs this file from main, whoever comments and wherever.
# - It checks out main's script and list only, never a pull request; it reads threads through the API.
# - The comment's text and author reach the script as env vars, never in a shell line. The token stays in the step.
on:
issue_comment:
types: [created]
permissions:
contents: read
jobs:
command:
# Every other comment skips here, before a runner starts.
if: >-
github.event.comment.user.type != 'Bot' && (
startsWith(github.event.comment.body, '/close') ||
startsWith(github.event.comment.body, '/reopen') ||
startsWith(github.event.comment.body, '/label') ||
startsWith(github.event.comment.body, '/unlabel'))
# One of our small runners (label horizon-light, docs/ci-runners.md): main's script only, as above.
runs-on: ${{ vars.SELF_HOSTED_CI != 'off' && fromJSON('["self-hosted", "linux", "horizon-light"]') || 'ubuntu-latest' }}
timeout-minutes: 5
permissions:
contents: read
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
ref: main
persist-credentials: false
sparse-checkout: |
.github/trusted-reviewers.txt
tools/ci/nightly_set.py
tools/ci/triage_command.py
sparse-checkout-cone-mode: false
- name: Run the command
env:
GH_TOKEN: ${{ github.token }}
BODY: ${{ github.event.comment.body }}
AUTHOR: ${{ github.event.comment.user.login }}
# Who may act is decided by this numeric ID: a login can be claimed by someone else after a rename.
AUTHOR_ID: ${{ github.event.comment.user.id }}
ISSUE: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
COMMENT_URL: ${{ github.event.comment.html_url }}
run: python3 tools/ci/triage_command.py command
-1
View File
@@ -21,7 +21,6 @@
.DS_Store
# What the client writes to its working directory when run from the repo root.
/options.txt
/debug-profile.json
/horizon.txt
/horizon-graphics.txt
/horizon-controller.txt
+50 -86
View File
@@ -9,21 +9,17 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
- **Never commit Mojang's files**: game jars, assets, decompiled source, prepared resources (`.local/`).
`tools/check_public_tree.py` rejects them; `git config core.hooksPath .githooks` runs it on commit.
- **Keep your runs away from real player data.** Any game or launcher you start gets fresh `mktemp -d`
folders in `HORIZON_HOME` (logs, crash reports, telemetry consent, recordings; not saves), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`. Saves, `options.txt` and
caches go in the game folder, by default the working directory: start the game from a scratch folder with
`--game-dir` pointing at it, or they land in the repo root, hidden by `.gitignore`. For the
folders in `HORIZON_HOME` (the game's data), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`, and, for the
launcher, `HORIZON_LAUNCHER_TRASH`, `HORIZON_LAUNCHER_CLIPBOARD` and `HORIZON_LAUNCHER_DESKTOP` pointing into the scratch home. Never write to
`~/.local/share/{applications,horizon,horizon-launcher}` or `~/.local/bin`.
- **Mark what an agent posts.** A PR body starts with a line naming the person whose agent opened it and
the tool: `> 🤖 Opened by <login>'s AI agent (<tool, model>), not by <login> personally.`, where `<login>`
is the GitHub account the PR is opened from (`gh api user --jq .login`). Issues say "Filed by", reviews
"Review by". Every comment starts with `🤖 Agent:`. Write only as the person you work for: the lines on
this page that name Ludovic are for his own agents (account `ludovic111`), never for anyone else's.
- **Mark what an agent posts.** A PR body starts with a line naming whose agent opened it and with what.
Ludovic's agents write `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, and a
contributor's agent names its own person and tool (issues: "Filed by", reviews: "Review by"). Every
comment starts with `🤖 Agent:`. Never write as Ludovic or as anyone else.
- **Attribution.** Commits end with a `Co-Authored-By:` line for the agent that wrote them, and PR bodies
with that agent's own "Generated with" line. Example, Ludovic's agents (Claude Code on `ludovic111`):
first line `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, commits
`Co-Authored-By: Claude <model> <[email protected]>`, last line
with that agent's own "Generated with" line. Ludovic's agents use
`Co-Authored-By: Claude <model> <[email protected]>` and
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
- Everything players run is Rust. Python/Java only for dev tools and harness drivers in `tools/`.
- Horizon-only features follow [docs/design-system.md](docs/design-system.md) and are off by default where
@@ -32,12 +28,6 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
change), none for a fix. Agents never add `release-now`: the maintainer decides it. Merges ship in batches of 5, or after
6 hours ([docs/release-flow.md](docs/release-flow.md)).
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
a claim with no open PR linking it is released after 5 days. The issues free to claim, without reading threads:
`gh issue list --search 'is:open no:assignee -label:"in progress" -label:tracker -label:proposal -label:"not claimable" -linked:pr'`
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
## Crates
@@ -45,13 +35,9 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| Crate | Owns |
|---|---|
| `horizon-client` | Gameplay library (`src/lib.rs`: `game.rs` + `game/`, `entry.rs`) and thin executable (`src/main.rs`). Also the dedicated server (`--dedicated`). Re-exports `horizon-client-lib` for existing mod paths. |
| `horizon-client-lib` | Reusable client systems, `horizon-ctl`, probes and their tests. Its manifest points to the existing sources under `crates/horizon-client/`; the library root is `src/client_lib.rs`. It never depends on gameplay. |
| `horizon-items` | Blocks and items below gameplay: `blocks.rs` (block facts), `items.rs`, `item_data.rs` (components), `enchant.rs`, `cooldowns.rs`, `bundle.rs`, `item_nbt.rs`. Re-exported as `horizon_client::items`... |
| `horizon-client` | The game: a lib (`src/lib.rs`, ~180 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings), modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-core` | Simulation primitives: Java `Random`s, `jmath` (HotSpot-exact `Math`), positions, noise, density, paletted storage, `version.rs` (the pinned version) |
| `horizon-protocol` | Network protocol 777: codecs, framing, encryption, `packets/{handshake,status,login,configuration,play}.rs`, `entity_data.rs` |
@@ -59,13 +45,9 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-prepare` | Builds `.local/horizon-client/26.3` from the player's own Minecraft install (runs the Java exporters) |
| `horizon-server` | `horizon-server`: `server.toml`, CLI, supervisor of the headless game ([docs/hosting.md](docs/hosting.md)) |
| `horizon-server-files` | `server.properties`, eula, ops/whitelist/bans, usercache |
| `horizon-anticheat` | The dedicated server's anticheat, off by default: `[anticheat]` settings, violation levels, the checks' rules; the hub runs them (`lan/anticheat.rs`) ([docs/anticheat.md](docs/anticheat.md)) |
| `horizon-cosmetics` | Cosmetic items Horizon clients draw on players (off by default): the item format, bones and animations ([docs/cosmetics.md](docs/cosmetics.md)) |
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
| `horizon-mods`, `cargo-horizon-mod` | Content mods (`.hmod`: manifest, content fence, no code), the registry index and `.hpack`, and `cargo horizon-mod` (`new`, `build`, `run`, `check`, `registry`) ([docs/mods.md](docs/mods.md)). Code mods are native, built by the launcher ([docs/native-mods.md](docs/native-mods.md)) |
| `horizon-mods`, `horizon-mod-sdk`, `cargo-horizon-mod` | Sandboxed WebAssembly mods: runtime, SDK + WIT, `cargo horizon-mod` ([docs/mods.md](docs/mods.md)) |
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
| `horizon-optifine` | OptiFine-format pack files below every feature that reads them: `.properties`, the name/range/block matchers, paths and `optifine/` folders, and custom entity models (`cem/`: `.jem`/`.jpm` parsing and baking) |
| `horizon-polytone` | Polytone-format pack files (`assets/*/polytone/`), read clean-room from the public wiki: targets, biome and dimension modifiers and their environment attribute layers (#11486) |
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
@@ -75,53 +57,39 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
Other top-level folders: `tools/` (harnesses and dev scripts), `tests/` (Python unit tests: `python3 -m
unittest discover -s tests`), `mods/horizon/src/test/java/dev/horizonlab/parity/` (Java oracles),
`docs/`, `.github/workflows/`. The website (playhorizon.dev) is the private horizon-rust/horizon-site: its build
`docs/`, `.github/workflows/`. The website (playhorizon.dev) is the private ludovic111/horizon-site: its build
reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` and `crates/horizon-theme/tokens.css` from main.
## Write tests with signal
- Test observable behavior, not words in docs, comments or source. Keep checks for machine-consumed contracts.
- Use independent expected values or a vanilla oracle; do not derive the answer from the code under test.
- Test outcomes, not only calls to stubs. Use mocks to isolate behavior, not as the behavior itself.
- Skip tests of trivial constants, derives and getters, or behavior already covered by another test.
- Avoid pinning implementation details; a harmless refactor should not break a behavior test.
- Make slow tests faster without removing cases or weakening their coverage.
- Never delete parity harnesses or tests that pin vanilla behaviour: they are the spec.
## Build and test fast
```sh
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo test -p horizon-client --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client --features horizon-client/mods --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
python3 tools/parity.py mob_ai sculk # named harnesses
python3 tools/lan_e2e.py --check chest # two real games over LAN (server-thread host)
python3 tools/lan_e2e.py --check chest # two real games over LAN (server-thread host); --in-frame compares the fallback
python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
```
- Harnesses and the game need the vanilla data in `.local/` (`minecraft/`, `horizon-client/26.3`, `datagen/`,
`tools/`): see [docs/parity-workflow.md](docs/parity-workflow.md). Without it, pr_ready lists the
harnesses for CI.
- A harness = `tools/test_<name>_parity.py` + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (mostly horizon-world and horizon-client),
- A harness = `tools/test_<name>_parity.py` (324 of them) + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (horizon-world 175, horizon-client 104),
or the game binary with a fixture env var (`game/dispense_fixture.rs`, `game/sculk_fixture.rs`,
`game/command_harness.rs`, `game/net_harness.rs`). Copy a nearby harness for a new one.
- Developing a native mod? Run `horizon-launcher settings native-profile dev` once. `dev` builds the game crate
unoptimized: a mod edit rebuilds about 6× faster than with `performance`, while game logic runs up to about twice as
slow. Use it while developing, not for playing; `performance` stays the default for players and releases
([docs/native-mods.md](docs/native-mods.md)).
- Probes build into `.local/parity-target/`; harnesses find binaries via `lab.cargo_target()`, never `target/`.
- Harness selection uses Cargo dep-info and reachable-code narrowing (`tools/parity_reach.py`).
`horizon-client-lib` builds the control tool and client probes without gameplay; their dep-info
excludes `game.rs` and `game/`, so gameplay edits do not rebuild or select those probes.
Source paths remain in `crates/horizon-client/`. Measure actual harness selection on refreshed
records with `tools/parity.py --changed-files paths.txt --list`; historical counts are not a guarantee.
- What a change reruns (of 329 harnesses, measured 2026-10-23): a file of the game binary (`game.rs`, `game/`)
~25, a file of the client lib ~103, of `horizon-world` ~103–109, of `horizon-core` ~119. Plan for it.
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio, controllers, `discord`, `friends` and `heartbeat` (the server build).
- **macOS:** `cargo build/test --workspace` may fail to link the two wasm guest cdylibs (undefined
`horizon:mods/events` symbols) until #1446 lands: add `--exclude coords-hud --exclude predator-missile --exclude horizon-mods-test-guest --exclude horizon-mods-test-ui-guest`.
Their real check is `cargo test -p horizon-mods` (builds them for `wasm32-unknown-unknown`).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio and controllers (the server build).
- Run and drive the game (starts the `horizon-client` beside `horizon-ctl`; other arguments go to the game):
`target/fast/horizon-ctl --socket $PWD/ctl.sock launch --world W --creative --offscreen --wait --resources $PWD/.local/horizon-client/26.3`,
then `horizon-ctl --socket … state|keys|turn|command|screenshot|quit`
@@ -129,9 +97,6 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## Vanilla reference
- Start parity work from the port ledger, not grep: `python3 tools/port_ledger.py issue N` (or `where
Class.method`, `what file:line`), and `check --base origin/main` before the PR. How:
[.claude/skills/vanilla-parity/SKILL.md](.claude/skills/vanilla-parity/SKILL.md), [docs/port-ledger.md](docs/port-ledger.md).
- Decompiled sources: `.local/minecraft/26.3/sources/{client,server}/` (`python3 tools/lab.py decompile`).
- Bytecode queries (callers through lambdas, every overload):
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`
@@ -143,70 +108,69 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## horizon-client: where things live
`L:` = lib (`crates/horizon-client/src/`, or `crates/horizon-client-render/src/` for the modules that crate holds, under
the same `horizon_client::<module>` paths), `G:` = gameplay (`crates/horizon-client/src/game/`), also in the lib in normal builds.
Gameplay keeps its `horizon_client::<module>` imports through a self-crate alias. The executable
contains only the allocator, mod API marker and a call to `entry::main`. For unit tests, the lib
excludes `entry` and `game`, and the executable test target owns `game` so its process-wide test
state stays separate from the library tests. This compiles gameplay twice for executable tests.
`L:` = lib (`crates/horizon-client/src/`), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`Game`; bin code reaches the lib as `horizon_client::<module>`.
| Subsystem | Files |
|---|---|
| Game loop, tick, frame | `game.rs`: `fn run`, `fn tick`, `fn tick_world`, `fn render`; G: `window_handler.rs` (`impl ApplicationHandler for Game`: `window_event`, `about_to_wait`); L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.rs` |
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs`, `screen_backgrounds.rs` (panorama, menu blur, border wall) |
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `world_entry.rs`, `postprocess.rs`; game.rs `fn update_world` |
| Game loop, tick, frame | `game.rs`: `fn run`, `impl ApplicationHandler for Game` (`window_event`, `about_to_wait`), `fn tick`, `fn tick_world`, `fn render`; L: `clock.rs`, `window.rs`, `spans.rs` |
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs` |
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
| World generation | `crates/horizon-world/src/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `carver/`) |
| Block behaviour | `crates/horizon-world/src/level/` (`mod.rs`, `tick.rs`, `hooks.rs`, `redstone.rs`, `place.rs`, `interact.rs`); game.rs `fn tick_blocks`, `fn process_level_events` |
| Mobs | L: `mobs.rs` (types), `mobs/` (`ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `species.rs`, `spawning.rs`, `render.rs`, `anim.rs`); G: `creatures.rs` (ticking, spawning, mob events), one file per mob family (`dragon.rs`, `wither.rs`, `villagers.rs`, `piglins.rs`, `golems.rs`...), `natural_spawning.rs` |
| Other entities | L: `entities.rs`, `projectiles.rs`, `arrows.rs`; G: `boats.rs`, `minecarts.rs`, `mounts.rs`, `armor_stands.rs`, `paintings.rs`, `frames.rs`, `fireworks.rs`, `tnt.rs`, `explosion.rs` |
| Player actions | game.rs (`fn use_hand`, `fn tick_mining`, `fn break_block`); G: `item_actions.rs` (`fn use_item`), `melee.rs` (`fn attack`), `hand.rs`, `combat.rs`, `consume.rs`, `using_item.rs` (an item in use, tick by tick), `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.rs` |
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs`, `inventory_input.rs` (keys and clicks over a container), `inventory_slots.rs` (slot layout and drawing) |
| Player actions | game.rs (`fn use_hand`, `fn use_item`, `fn attack`, `fn tick_mining`, `fn break_block`); G: `hand.rs`, `combat.rs`, `consume.rs`, `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`, `items.rs`, `item_data.rs` |
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs` |
| Commands | parse: crate `horizon-command` (`args.rs`, `selector.rs`, `tree_*.rs`, `vanilla.rs`); run: G: `commands.rs` (dispatch) + `commands/*.rs` (`admin`, `execute`, `items`, `players`, `world_cmds`...); L: `scoreboard_cmd.rs`, `data_cmd.rs`; `horizon-client-core`: `nbt_path.rs`, `scoreboard.rs`; G: `functions.rs`, `command_blocks.rs` |
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `graphics_ui.rs`, `screen_draw.rs`, `ui.rs` (widgets), `hud.rs`, `hud_layout.rs`, `hud_overlays.rs`, `chat.rs`, `tab_list.rs`; G: `startup_menu.rs`, `loading_screen.rs` (the loading screen, the pre-game `App`), `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs`, `creative_screen.rs` |
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `graphics_ui.rs`, `screen_draw.rs`, `ui.rs` (widgets), `hud.rs`, `hud_layout.rs`, `hud_overlays.rs`, `chat.rs`, `tab_list.rs`; G: `startup_menu.rs`, `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs` |
| Multiplayer client | L: `net.rs` (network thread); G: `remote.rs` (`fn handle_packet`, `fn tick_remote`), `remote_*.rs` (mob data, objects, projectiles, vehicles, stations) |
| LAN host / server side | L: `lan/` (`conn.rs` handshake + login threads, `hub.rs` + `hub/` play state, `chunks.rs`, `entity.rs`, `tracking.rs`, `guest.rs`, `playerdata.rs`); G: `lan.rs` (`LanHost`), `lan_actions.rs` (`fn as_guest`, `fn tick_guests`), `lan_*.rs` per feature, `lan_entities.rs` (tracked entities) |
| Integrated server | G: `integrated.rs` (the server thread), `levels.rs` (one level per dimension, `fn tick_server`), `server_travel.rs` |
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server`, `horizon-anticheat` (L: `lan/anticheat.rs`) |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities), `world_session.rs` (opening and closing a world); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`; G: `play_keys.rs` (`fn key_pressed`), `play_input.rs` (`fn play_mouse`, toggled keys, the hotbar wheel); crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs`, `pointer_capture.rs` (mouse look, pointer grab) |
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server` |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; L: `keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `osk.rs`, `clipboard.rs`; G: `pad.rs`, `pad_menu.rs` |
| Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` |
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`, `mobs/cem.rs` + `mobs/cem_anim.rs` (OptiFine/EMF entity models and their animations); `horizon-client-core/src/lang.rs`; crates `horizon-assets`, `horizon-optifine`; G: `packs.rs` |
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`; `horizon-client-core/src/lang.rs`; crate `horizon-assets`; G: `packs.rs` |
| Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) |
| Mods | crate `horizon-mods`; G: `mods.rs`; L: `mod_content.rs`, `mod_list.rs` |
| Horizon extras, settings | L: `extras.rs`, `graphics.rs`, `settings.rs`, `minimap.rs`; G: `extras.rs`, `tweaks.rs`, `options.rs` |
## How `Game` is organised
- `struct Game` (game.rs) holds everything. Its methods are spread over many files: game.rs keeps
- `struct Game` (game.rs, ~290 fields) holds everything. Its methods are spread over ~180 files: game.rs keeps
the core (tick, render, input, inventory, world open/close) and each `game/<feature>.rs` adds an
`impl Game<'_>` block for one feature, with its own state types. Fields stay private to the gameplay module and its descendants. Gameplay tests run in the gameplay library, separate from reusable-library tests.
`impl Game<'_>` block for one feature, with its own state types. Fields are private to the bin.
- Find a feature's code: `grep -rn "fn tick_<thing>\|fn <verb>_<thing>" crates/horizon-client/src/game*`;
the file is usually `game/<thing>s.rs`. Who calls it: grep the name in game.rs's `fn tick` / `fn tick_world`.
- Fields that say which mode runs: `remote` (this window is a client of a server: multiplayer, LAN guest, or
single player on its server thread), `lan` (`LanHost`: this world is open to the network), `integrated`
(the window's own server thread), `serving` (this `Game` *is* that server thread), `options.dedicated`.
`fn tick` returns `tick_remote()` when `remote` is set; a server ticks its levels (`fn tick_world`).
- A server's players run through the shared gameplay code: `as_guest` swaps the guest's
`fn tick` returns `tick_remote()` when `remote` is set; nothing else of the local simulation runs.
- A server's players run through the same gameplay code as the local player: `as_guest` swaps the guest's
state (position, inventory, mode, mining, item use) into `Game` for one call (`game/lan_actions.rs`), and
`levels.rs` swaps whole dimensions in the same way. Changes reach clients through the block log, level
events and the entity tracker (`lan/tracking.rs`, `game/lan_entities.rs`).
- Shared read-only data: `Resources` (`res`: blocks, items, recipes, mob types, biomes) and `PackResources`
(`pack`), both in game.rs, their methods in `game/resource_tables.rs`.
(`pack`), both in game.rs.
## Single player runs on a server thread
## The two single-player paths
`game/integrated.rs` runs a second `Game` (`serving`) on a "Server thread" with one level per dimension;
the window joins it as a client through memory (`remote`), exactly as a LAN guest or a dedicated server's
player does (#1514). A window's `Game` never ticks a world itself: the old in-frame path is gone
(`--in-frame` and `--no-server-thread` only print a notice; `HORIZON_SERVER_THREAD` is ignored).
Open `[server-thread]` issues list the gaps left.
1. **Server thread (default since #1514):** `game/integrated.rs` runs a second `Game` (`serving`) on a
"Server thread" with one level per dimension; the window joins it as a client through memory (`remote`),
exactly as a LAN guest or a dedicated server's player does. This is what players run. Open
`[server-thread]` issues list the gaps left.
2. **In-frame (`--in-frame`, `--no-server-thread` or `HORIZON_SERVER_THREAD=0`):** the window's `Game` ticks
the world itself, one level that follows the player (`fn enter_dimension`). Deterministic rule fixtures still use this path; player-facing integration uses the default server thread.
Single player, LAN guests and dedicated servers all run the server code, so a fix there serves all three.
Test the server thread when you touch player-facing gameplay:
LAN guests and dedicated servers always use the server code, so a fix there serves both. Test both paths
when you touch player-facing gameplay:
```sh
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T # server thread
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T --in-frame # in-frame
python3 tools/lan_e2e.py --check <name> # host on its server thread, guest is a server client
python3 tools/lan_e2e.py --in-frame --check <name> # comparison (portals, levels, distance, lava need server-thread mode)
python3 tools/parity.py dedicated_server server_entities server_player_damage
```
+32 -80
View File
@@ -2,11 +2,10 @@
Thanks for helping. Horizon is Minecraft: Java Edition 26.3 rewritten in Rust, and the one rule that shapes
everything is that **it plays exactly like vanilla**. This page is the short version of how we work; questions are
welcome on [Discord](https://discord.gg/playhorizon).
welcome on [Discord](https://discord.gg/HcTeMZxe5E).
This repository holds the game and the launcher, plus the tools, tests and docs that build, check and release them.
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything
else (videos, Discord and other community material) lives elsewhere.
The website (playhorizon.dev) is built in a separate repository from this one's README, `docs/api/` and roadmap. Anything else (videos, Discord and other community material) lives elsewhere.
**Where to start: [playhorizon.dev/hub](https://playhorizon.dev/hub).** It shows what is being done now and next
([ROADMAP.md](ROADMAP.md) in the repository), the work board, good first tasks, and the forms to report a bug or
@@ -15,25 +14,23 @@ queue (the hub, or `/suggest` on Discord) rather than to issues, which are for b
## Build it
- **Prerequisites:** Rust (stable), Python 3.10+, JDK 25, and on Linux the ALSA and udev headers
(`libasound2-dev libudev-dev`).
- **Resources:** Horizon never downloads the game; it prepares its resources from your own copy of Minecraft 26.3, as
described in [Build from source](README.md#build-from-source).
- **Commands:**
You need Rust (stable), Python 3.10+, JDK 25, and on Linux the ALSA and udev headers (`libasound2-dev libudev-dev`).
Horizon never downloads the game: it prepares its resources from your own copy of Minecraft 26.3, as described in
[Build from source](README.md#build-from-source).
```sh
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
cargo run --release -p horizon-client
```
```sh
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
cargo run --release -p horizon-client
```
- **Launcher:** its own Cargo workspace in `launcher/` (`cd launcher && cargo run`; on NixOS, `nix-shell` first).
The launcher is its own Cargo workspace in `launcher/` (`cd launcher && cargo run`; on NixOS, `nix-shell` first).
## Vanilla first
Wherever vanilla has a feature, vanilla is the specification, down to the order of random calls. "Parity" here means
Horizon gives the same results as the pinned 26.3 client and server for the same inputs, checked by a **harness**: a
`tools/test_*_parity.py` script that runs a small Java oracle against the real game and a Rust probe against Horizon,
and compares their outputs.
Wherever vanilla has a feature, vanilla is the specification, down to the order of random calls. "Parity" here
means Horizon gives the same results as the pinned 26.3 client and server for the same inputs, checked by a
**harness**: a `tools/test_*_parity.py` script that runs a small Java oracle against the real game and a Rust
probe against Horizon, and compares their outputs. There are about 290 of them.
- **Read the vanilla code before writing Horizon's.** `tools/rea_vanilla.py` queries the pinned game bytecode:
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`.
@@ -44,39 +41,20 @@ and compares their outputs.
off by default wherever they would change the vanilla look.
- **Everything players run is Rust.** Python and Java are fine for dev tools and harness drivers in `tools/`, never
for player-facing code.
- **No game files in the repo.** Never commit game files, decompiled source or prepared resources (`.local/`):
`tools/check_public_tree.py` rejects them (`git config core.hooksPath .githooks` runs it before every commit).
The pre-push hook checks the full history reachable from the branches and tags being pushed. Run
`python3 tools/check_public_tree.py --history` to audit the history of all local refs.
## Only original code, or code whose license allows it
Never commit game files, decompiled source or prepared resources (`.local/`): `tools/check_public_tree.py` rejects
them (`git config core.hooksPath .githooks` runs it before every commit).
The pre-push hook checks the full history reachable from the branches and tags being pushed. Run
`python3 tools/check_public_tree.py --history` to audit the history of all local refs.
Horizon is MIT. Its code is written from Minecraft 26.3 itself and from public documentation. Since 30 October 2026
another project's source may also be read and used, **only when that project's license allows it**:
## Only original code
- **Allowed:** MIT, MIT-0, BSD-2-Clause, BSD-3-Clause, 0BSD, ISC, zlib, Apache-2.0, Unlicense, and the CC0-1.0
public-domain dedication. That code may be read, copied or adapted.
- **Off limits, even to read:** GPL, LGPL (Iris), AGPL, MPL, EPL, Polyform (Sodium), the Creative Commons licenses
(CC BY, CC BY-SA, CC BY-NC and the others; CC0 is a public-domain dedication, not one of them), custom or
"source available" licenses, all rights reserved (OptiFine), and any project with no license or an unclear one.
Don't read, copy, translate or paraphrase their source, and don't send code an AI tool wrote from it. Being able
to download a mod or a pack does not give you the right to read its files.
- **Java's own library is GPL too.** OpenJDK's source (`java.lang.Math`, `StrictMath`, `FdLibm`, `HashMap`, the
sorts, `Double.toString`, HotSpot's intrinsics...) is GPL-2.0, so it is off limits like the rest. Reproduce Java's
behaviour from its API documentation (the Javadoc) and from what a JVM returns, or from a permissive original such
as Sun's fdlibm C sources (see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)); see #6741.
- **Check the license yourself.** Read the LICENSE file at the commit you use and the header of each file you take:
some repositories mix licenses, and a file under another license is off limits. A license that changed: the one at
your commit counts. In doubt, don't: ask in #contributors first.
- **Say it in the pull request:** the project, a link to the commit, its license, and what you read or copied. A
pull request that used another project's source without saying so is closed.
- **Copied or adapted code** keeps its copyright line and license notice at the top of the file, and gets an entry
in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Apache-2.0 code also keeps its NOTICE text and says what
was changed.
User documentation and file formats are fine to read, and packs and mods are fine to run and observe, whatever their
license. Shader packs are a clean-room rewrite of the pack format, produced from a written description of it (see
#1561); they carry no foreign source, and the rules above still apply to anything you read while working on them.
Horizon's code is written from Minecraft 26.3 itself and from public documentation, never from another mod, loader
or renderer. Don't read, copy, translate or paraphrase the source of Iris, OptiFine, Sodium or any other project,
and don't send code an AI tool wrote from it. Their user documentation and file formats are fine to read, and their
packs and mods are fine to run and observe. Shader packs were removed on 24 October 2026 (#1561) because their code
had been written from another project's source; they come back only as a clean-room rewrite from a written
description of the pack format.
## Before asking for review
@@ -92,25 +70,10 @@ your change can affect. Without the vanilla data on your machine it lists those
`--quick` skips the harnesses. It exits non-zero when something fails.
For the game workspace, Clippy checks all targets of the touched crates and their transitive workspace dependents,
including examples. The dependency scope includes optional, platform, build and development dependencies. Tests run
library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
including examples. The dependency scope includes optional, platform, build and development dependencies.
Tests run library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
## Claim before you start
Work is tracked in GitHub issues, and an issue that is assigned is taken (so is one with the `in progress` label:
claimed on Discord with Kira's `/claim`): pick another, or ask its holder in the thread. Before you start, comment
**`/claim`** on the issue; you are assigned at once, no role needed. No issue for your work yet? Open one (a line is
enough), then claim it. `/unclaim` gives it back. Issues labelled `tracker`, `proposal` or `not claimable` aren't
claimed whole: claim one of the issues a tracker lists, or open one for the part you take.
A claim with no open pull request linking the issue (`Fixes #123` in the description, or a draft) is released after
5 days, with a comment: open a draft early. You can hold 8 open issues at a time, not counting the ones your
pull requests already fix. Agents claim the same way.
When your pull request changes the same functions as another open one, or closes the same issue, a bot comment names
it (edited on every push, never repeated): talk to its author in the thread before you both finish the same work.
## Pull requests
- **Small.** One change per pull request, under about 500 lines of code where you can; split bigger work into
@@ -119,20 +82,9 @@ it (edited on every push, never repeated): talk to its author in the thread befo
can see.
- **Tick "Allow edits by maintainers".** When something small is left (a lint, a rebase, a nit) we push the fixup
ourselves instead of sending it back to you; only design questions come back.
- **Once we've pushed to your branch, merge main into it; don't rebase or force-push.** A rebase or force-push
replaces our commits with yours and erases the fixes we added (it happened on #5188 and #5203). Tell your agent
too: `git merge origin/main`, never `git rebase` or `git push --force` on a branch a maintainer has touched. If
our fixes get erased anyway, we finish the work in our own pull request that keeps your commits and credits you.
- **Tick its kind** in the template: `parity` (Horizon now does what vanilla does), `bug` (a crash or breakage),
`perf`, `feature` (new for players), `extra` (Horizon-only, off by default), `tooling` or `docs`. A workflow sets
that label, and area labels (`launcher`, `mobs`, `worldgen`, `rendering`...) from the paths you change.
`label:parity` is the list of real progress toward vanilla.
- **Release labels** (we set them if you can't): `feature` also puts it under "New" in the release notes,
`skip-release` when players see no change (tooling, docs, tests), nothing for a fix. `release-now` is for crash
fixes that shouldn't wait. pr_ready suggests one.
Who reviews and merges: the Horizon Team, on CI for ordinary pull requests; risky ones (saves, network, world
generation, the launcher's updates) wait for a local proof first ([merging](docs/release-flow.md#merging)).
- **Labels** (we set them if you can't): `feature` when players get something new (it appears under "New" in the
release notes), `skip-release` when players see no change (tooling, docs, tests), nothing for a fix.
`release-now` is for crash fixes that shouldn't wait. pr_ready suggests one.
Every merge to main is released automatically ([release flow](docs/release-flow.md)), so main always stays playable.
How agents, checks, releases and signed updates keep this safe: [security](docs/security.md).
@@ -146,5 +98,5 @@ new pushes, we add it again.
## Talk to us
[Discord](https://discord.gg/playhorizon) for questions and ideas, [issues](https://github.com/horizon-rust/horizon/issues)
for bugs. To take something on, `/claim` its issue ([above](#claim-before-you-start)).
[Discord](https://discord.gg/HcTeMZxe5E) for questions and ideas, [issues](https://github.com/ludovic111/horizon/issues)
for bugs. If you want to take something on, say so in its issue so nobody else starts the same work.
Generated
+948 -1265
View File
File diff suppressed because it is too large. Load diff
+19 -45
View File
@@ -1,25 +1,15 @@
[workspace]
exclude = ["launcher", "crates/horizon-telemetry-server", "crates/horizon-mod-registry", "third_party/wgpu-hal", "third_party/naga", "third_party/libmimalloc-sys"]
exclude = ["launcher", "crates/horizon-telemetry-server", "crates/horizon-mod-registry", "third_party/wgpu-hal"]
members = [
"crates/horizon-core",
"crates/horizon-dualsense",
"crates/horizon-narrator",
"crates/horizon-threads",
"crates/horizon-hdr",
"crates/horizon-world",
"crates/horizon-client",
"crates/horizon-client-lib",
"crates/horizon-client-core",
"crates/horizon-client-input",
"crates/horizon-client-render",
"crates/horizon-cosmetics",
"crates/horizon-command",
"crates/horizon-items",
"crates/horizon-server-files",
"crates/horizon-anticheat",
"crates/horizon-chat-filter",
# The Model Context Protocol, shared by horizon-ctl, horizon-server and the launcher.
"crates/horizon-mcp",
"crates/horizon-server",
"crates/horizon-bots",
"crates/horizon-assets",
@@ -30,25 +20,35 @@ members = [
"crates/horizon-bench",
"crates/horizon-friends",
"crates/horizon-relay",
"crates/horizon-mod-sdk",
"crates/horizon-mods",
"crates/horizon-mod-api-gen",
"crates/horizon-mod-api-gen/dispatch-tests",
"crates/horizon-mods/tests/guest",
"crates/horizon-mods/tests/ui_guest",
"crates/cargo-horizon-mod",
"crates/horizon-port-pack",
"crates/horizon-shaders",
"crates/horizon-optifine",
"crates/horizon-polytone",
"crates/horizon-shader-api",
"crates/horizon-release-sign",
"crates/horizon-mod-deps",
"crates/horizon-mod-sdk/examples/coords-hud",
"crates/horizon-mod-sdk/examples/coords-hud-lua",
"crates/horizon-mod-sdk/examples/predator-missile",
"crates/horizon-mod-sdk/examples/welcome",
"crates/horizon-mod-sdk/examples/beacons",
"mods/official/fps-overlay",
"mods/official/keystrokes",
"mods/official/far-view",
"mods/official/minimap",
"mods/official/zoom",
"xtask",
"mods/official/recipes",
"crates/horizon-mod-lua",
]
resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
rust-version = "1.95"
rust-version = "1.85"
# Ports mirror vanilla's Java signatures, index loops and nested generic types
# one to one so they can be checked against the decompiled sources.
@@ -61,42 +61,16 @@ large_enum_variant = "allow"
# wgpu-hal 30.0.1 with gfx-rs/wgpu#10124: wgpu 30 creates a Vulkan descriptor
# pool with no pool sizes while it creates the device, which Vulkan allows only
# since 1.3.215, and Intel's Windows driver for 6th-10th gen iGPUs (HD 520/530,
# UHD 620/630) crashes on it with an access violation (#746). It also fixes the
# Metal backend's lookup of the CoreGraphics HDR colour space names, which handed
# CoreGraphics a symbol's address as the string and crashed HDR output on macOS.
# And gfx-rs/wgpu#10714: on Windows, Vulkan waits for the acquire fence only in
# FIFO (VSync), where it was meant to pace frames; with VSync off it made every
# frame wait for the GPU to finish the one before (#4604).
# Drop this and the folder once a wgpu release has these fixes.
# UHD 620/630) crashes on it with an access violation (#746). Drop this and the
# folder once a wgpu release has the fix.
[patch.crates-io]
wgpu-hal = { path = "third_party/wgpu-hal" }
# naga 30.0.1, with the fixes to its GLSL frontend that shader packs need (THIRD_PARTY_NOTICES.md).
naga = { path = "third_party/naga" }
# libmimalloc-sys 0.1.49 whose mimalloc v3 counts bits without POPCNT when MSVC builds it: the
# game died before main on CPUs without it (Core 2, #7264; THIRD_PARTY_NOTICES.md). Drop this and
# the folder once a libmimalloc-sys release has microsoft/mimalloc#1291's fix.
libmimalloc-sys = { path = "third_party/libmimalloc-sys" }
# Line tables keep file:line in backtraces; full debug info made every
# example and test binary hundreds of megabytes and slow to link.
[profile.dev]
debug = "line-tables-only"
# The native mod API is generated by a build script that parses every file of game/ with syn on
# each gameplay edit; unoptimized that took ~6 s of every rebuild, optimized ~1.5 s.
[profile.dev.package.horizon-mod-api-gen]
opt-level = 3
[profile.dev.package.syn]
opt-level = 3
[profile.dev.package.proc-macro2]
opt-level = 3
[profile.release.package.horizon-mod-api-gen]
opt-level = 3
[profile.release.package.syn]
opt-level = 3
[profile.release.package.proc-macro2]
opt-level = 3
[profile.release]
lto = "thin"
codegen-units = 1
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 Ludovic Marie and the Horizon contributors
Copyright (c) 2026 Ludovic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+93 -92
View File
@@ -1,170 +1,171 @@
<p align="center">
<img src="docs/media/banner.webp" alt="Horizon: the game you know, rebuilt in Rust" width="100%">
<img src="docs/media/banner.webp" alt="Horizon: Minecraft: Java Edition 26.3, rewritten in Rust and checked against vanilla by 281 differential harnesses" width="100%">
</p>
<p align="center">
<a href="https://github.com/ludovic111/horizon/actions/workflows/ci.yml"><img src="https://github.com/ludovic111/horizon/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
<img src="https://img.shields.io/badge/Minecraft-Java%2026.3-5D9C3F" alt="Minecraft Java 26.3">
<img src="https://img.shields.io/badge/Rust-stable-B7410E?logo=rust&logoColor=white" alt="Rust">
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-2A2470" alt="Linux, macOS and Windows">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3B2F7E" alt="MIT license"></a>
</p>
<p align="center">
<a href="https://playhorizon.dev"><b>Website</b></a> ·
<a href="#download"><b>Download</b></a> ·
<a href="https://playhorizon.dev/docs/"><b>Docs</b></a> ·
<a href="https://discord.gg/playhorizon"><b>Discord</b></a> ·
<a href="https://playhorizon.dev/docs/"><b>Documentation</b></a> ·
<a href="https://github.com/ludovic111/horizon/releases"><b>Release notes</b></a> ·
<a href="ROADMAP.md"><b>Roadmap</b></a> ·
<a href="#project-status"><b>Project status</b></a> ·
<a href="#contributing"><b>Contributing</b></a>
</p>
<p align="center">
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-0%20open-F0795A" alt="Player-visible gaps open"></a>
</p>
# Horizon
**Minecraft: Java Edition 26.3, rewritten in Rust.** Your worlds, the same rules and the same terrain as the Java game, checked against it by hundreds of differential tests, in one native program with no Java. Free, open source (MIT) and unofficial: you play it with your own copy of Java Edition.
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by 281 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
<p align="center">
<img src="docs/media/readme/valley.webp" alt="A river valley with a plains village, savanna hills and a cherry grove, seen from above with a long view distance" width="100%">
</p>
## What works today
Everything below is in the current release.
- **The game.** Survival, creative, hardcore and spectator; vanilla world generation in all three dimensions, mobs, villagers, raids, redstone, every command, the HUD and the recipe book. Vanilla saves open as they are, and worlds from 1.8 to 26.2 are upgraded.
- **Multiplayer.** Join vanilla and Horizon servers (online mode included), open a world to LAN, or invite friends with a code from the launcher's Home page.
- **Graphics.** Distant terrain up to 1,024 chunks, graphics presets, HDR and controllers, all off by default where they would change the vanilla look ([settings](https://playhorizon.dev/docs/settings)).
- **Packs.** Resource packs, data packs and shader packs ([below](#mods-and-shader-packs)).
- **Mods.** Content mods and native Rust mods, installed from the launcher ([below](#mods-and-shader-packs)).
- **Servers.** Host one from the launcher, or run `horizon-server` on a VPS ([below](#host-a-server)).
- **The launcher.** It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
<table>
<tr>
<td width="50%"><img src="docs/media/readme/sunset.webp" alt="Sunset behind a cherry grove on a hill, with a river below"></td>
<td width="50%"><img src="docs/media/readme/nether.webp" alt="A crimson forest in the Nether, with a piglin among the fungi"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/readme/survival.webp" alt="Survival in a village square: two villagers, flowers, the hotbar, health and hunger"></td>
<td width="50%"><img src="docs/media/readme/multiplayer.webp" alt="Two players in a LAN world: the other player's name tag and the player list"></td>
</tr>
</table>
<sub>Nightly 2026-11-05 on a Mac mini (Apple M4 Pro, Metal): vanilla's textures, no mods or shader packs, render distance 32 with distant terrain on.</sub>
## Performance
**Horizon draws more frames per second than Java Edition, and loads faster, on the same machine, world and path.** `tools/perf_bench.py` runs each game in turn, from a fresh world (seed 1234), waiting for the terrain and then flying it over a village, a forest, an ocean and a mountain. Measured on 2026-10-29/30 on an idle Windows 11 machine (Ryzen 7 9800X3D, RTX 5080), Horizon v0.2.97 as the launcher installs it, 1280x720 in a window, VSync off, three games per figure. Frames per second, mean of the four scenes, with the slowest 1% of frames in brackets:
| Render distance | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- |
| 8 chunks | 3,289 (1,327) | 2,437 (640) | 2,375 (709) |
| 16 chunks | 2,074 (598) | 1,661 (375) | 1,586 (432) |
| 32 chunks | 1,207 (333) | 1,031 (176) | 879 (264) |
"Vanilla + mods" is Sodium, Lithium, FerriteCore and ImmediatelyFast. Horizon reaches the title screen in 2.9–3.2 s against vanilla's 7.5–7.7 s (the modded client's 10.8–11.1 s), loads a scene's chunks 2.9–3.8 times faster, and playing holds a steady working set of 1.1–1.7 GB against 1.9–2.9 GB. Every scene with its confidence interval and the run's load log: [the 2026-10-29 reports](benchmarks/perf/windows-9800x3d-5080-2026-10-29.md). Method, and how to measure your own machine: [docs/benchmark.md](docs/benchmark.md). [playhorizon.dev/benchmarks](https://playhorizon.dev/benchmarks) puts these runs beside a 2016 laptop, a Mac mini and a gaming desktop.
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
## Download
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev), or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true):
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your own copy of Minecraft, and updates itself.
| Platform | File | Needs |
| --- | --- | --- |
| Windows x64 | `horizon-launcher-setup.exe` | Windows 10 or 11, a GPU with Vulkan or DirectX 12 |
| Windows on Arm | `horizon-launcher-setup-arm64.exe` | Windows 11 on Arm |
| macOS, Apple Silicon | `horizon-launcher-macos-arm64.dmg` | macOS 12 or later |
| macOS, Intel | `horizon-launcher-macos-x64.dmg` | macOS 12 or later |
| Linux x86-64 | `horizon-launcher-linux-x86_64.AppImage` or `.tar.gz` | a glibc distribution, X11 or Wayland, a GPU with Vulkan or OpenGL 3.3 |
**You also need a Microsoft account that owns Minecraft: Java Edition.** You sign in with it in the launcher, which plays only with an account that owns the game ([why](docs/microsoft-account.md)). Horizon contains no Minecraft code or assets: an existing install (official launcher, Prism, MultiMC...) is reused, and without one the launcher downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
**You need** Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 runtime, for the one-time preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
<td width="50%"><img src="docs/media/readme/launcher-home.webp" alt="The launcher's Home page: the Horizon instance, Play, the version picker, the friend invite field, screenshots and the patch log"></td>
<td width="50%"><img src="docs/media/readme/launcher-mods.webp" alt="The launcher's Mods page: Horizon's mod registry with Zoom selected, and tabs for modpacks, resource packs, shader packs and data packs"></td>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: a version ready to play, its summary, Client and Server, the version picker and Play"></td>
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page"></td>
</tr>
</table>
## Mods and shader packs
## What works today
- **Content mods** (`.hmod`): blocks, items, recipes, loot, models and textures, with no code.
- **Native mods** (`.hnmod`): Rust that the launcher compiles into the game for you, with nothing to install.
- **Java mods** (Fabric, NeoForge, Forge) don't run: Horizon has no Java. The launcher carries over the data and assets inside their jars, and imports modpacks (`.mrpack`, CurseForge zips, Prism instances) the same way ([modpacks](https://playhorizon.dev/docs/modpacks)).
- **Shader packs**: Iris/OptiFine-format packs (`shaders/` folders) and Horizon's own WGSL packs, under Video Settings → Shader Packs ([pack format](docs/shaders.md)).
The [documentation](https://playhorizon.dev/docs/) covers each of these.
Mods are off by default: a fresh install plays exactly like vanilla 26.3. The launcher's Mods page browses Horizon's registry and Modrinth. [Mods](https://playhorizon.dev/docs/mods) · [make a mod](docs/mods.md).
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs and data packs:** resource packs work, and data packs load functions, predicates and item modifiers.
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
<table>
<tr>
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village among savanna hills, with farms, villagers and an iron golem"></td>
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
</tr>
</table>
## Host a server
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends. On a VPS, `horizon-server` installs with one command, needs no Java, and takes vanilla 26.3 and Horizon players alike. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
The launcher's **Servers** page creates, starts, backs up and updates a Horizon server with buttons, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and runs without Java. Guide: [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
## Modpacks
The launcher's **Modpacks** page imports a Modrinth or CurseForge pack, shows which of its mods Horizon can carry over, and plays the ones it can as their own profile. Today that means content mods made of data and assets; mods with Java code are not ported yet. See [Modpacks](https://playhorizon.dev/docs/modpacks).
## Agent control
AI agents and scripts can play Horizon. `horizon-ctl`, shipped with the game, reads its state as JSON and acts like a player; `horizon-ctl mcp` serves the same commands over MCP.
AI agents and scripts can play Horizon. `horizon-ctl`, shipped beside the game in every release folder, is a command line that reads the game as JSON and acts like a player; `horizon-ctl mcp` serves the same commands as an MCP server (one tool per command, screenshots returned as images).
```sh
horizon-ctl launch --world Survival --seed 5 --wait # finds the version the launcher installed
horizon-ctl state # position, health, food, target, screen...
horizon-ctl find-block birch_log # the nearest logs
horizon-ctl goto -0.5 65 -2.5 --near 2.5 # walks there along a path
horizon-ctl break -1 65 -3 # mines it; replies with the drops
horizon-ctl open-inventory && horizon-ctl click-slot 0 # menus: slots, buttons by label, text fields
```
`horizon-ctl help` lists every command. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
Commands block until the action is done and reply with what happened; errors are JSON with distinct exit codes, `horizon-ctl help` lists every command and `horizon-ctl COMMAND --help` explains one with examples. It talks to the game over a Unix socket on Linux and macOS and over a token-protected localhost port on Windows. See [Agent control in the native client guide](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
## Build from source
Needs Rust 1.90+, Python 3.10+, JDK 25, and on Linux `libasound2-dev libudev-dev`. `horizon-prepare` builds the game's resources from **your own copy** of Java Edition 26.3 (checked against `research-lock.json`) and never downloads it:
For developers. Requirements: Rust 1.90 or newer, Python 3.10 or newer, JDK 25, and on Linux the ALSA and udev development headers (`libasound2-dev libudev-dev` on Debian and Ubuntu). Development and the vanilla harnesses run on Linux.
1. **Client:** play 26.3 once in the official launcher. Horizon reads `~/.minecraft` (or `--minecraft-dir` / `HORIZON_MINECRAFT_DIR`).
2. **Server jar:** the 26.3 server jar (`downloads.server.url` in `versions/26.3/26.3.json`), passed once with `--server-jar` or `HORIZON_SERVER_JAR`.
Horizon contains no game files and never downloads them. It builds its resources from **your own copy** of Minecraft: Java Edition 26.3, verified against the checksums in `research-lock.json`:
1. **Client:** install Java Edition with the official launcher and play 26.3 once. Horizon reads `versions/26.3/`, `libraries/` and `assets/` from the launcher's game directory (`~/.minecraft` on Linux). If yours lives elsewhere, pass `--minecraft-dir` or set `HORIZON_MINECRAFT_DIR`.
2. **Server jar:** the 26.3 server jar, from the `downloads.server.url` in your launcher's `versions/26.3/26.3.json` (or [minecraft.net/download/server](https://www.minecraft.net/en-us/download/server) while 26.3 is the current release). Pass it with `--server-jar` or set `HORIZON_SERVER_JAR`. It is only needed the first time.
```sh
git clone https://github.com/horizon-rust/horizon.git
git clone https://github.com/ludovic111/horizon.git
cd horizon
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar # needs JDK 25
cargo run --release -p horizon-prepare -- sounds # optional: sounds from your launcher install
cargo run --release -p horizon-client
```
Prepared files land in the gitignored `.local/`: never commit or share them. Options, controls and architecture: [the native client guide](docs/native-client.md); all developer docs: [docs/README.md](docs/README.md).
Everything prepared lands in the gitignored `.local/`. It is derived from the game: never commit it or share it.
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, scripting through `horizon-ctl` and the architecture are described in [the native client guide](docs/native-client.md). The rest of the developer documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
## Roadmap
What is being done now, what comes next and where you can help, in plain words and in technical detail: [ROADMAP.md](ROADMAP.md). [playhorizon.dev/hub](https://playhorizon.dev/hub) shows it with the work board, the journal of what shipped and how to help.
## Contributing
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests), and see [ROADMAP.md](ROADMAP.md) or [playhorizon.dev/hub](https://playhorizon.dev/hub) for what's next. Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels), or [the contributing guide](https://playhorizon.dev/docs/contributing) on the site. Questions and ideas: [Discord](https://discord.gg/HcTeMZxe5E). Before opening a pull request, run the self-check and paste the block it prints:
```sh
git config core.hooksPath .githooks
python3 tools/pr_ready.py
```
It runs rustfmt, clippy, the touched crates' tests, the Python tests, the public-tree check and the vanilla harnesses your change affects. Never commit game files, decompiled source or prepared resources.
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a maintainer's computer holding their own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
## Project status
**Player-visible gaps: 0 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Animals [0](https://github.com/horizon-rust/horizon/issues/3512); Monsters and bosses [0](https://github.com/horizon-rust/horizon/issues/3513); Entities, projectiles, vehicles and decorations [0](https://github.com/horizon-rust/horizon/issues/3514); Items, enchantments and inventories [0](https://github.com/horizon-rust/horizon/issues/3515); Blocks, redstone and fluids [0](https://github.com/horizon-rust/horizon/issues/3516); World, world generation and saves [0](https://github.com/horizon-rust/horizon/issues/3517); Screens, HUD and client packets [0](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [0](https://github.com/horizon-rust/horizon/issues/3519); Server and players on the server [0](https://github.com/horizon-rust/horizon/issues/3520); Mob AI, villagers and raids [0](https://github.com/horizon-rust/horizon/issues/3521). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs. What players still run into: [known issues](docs/known-issues.md).
The table below is the project's own audit of each subsystem. Every subsystem now matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not proof that nothing else differs. The remaining known differences are listed below.
### Java parity: 100%
Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75 (mostly complete) or 1 (complete and verified by differential tests); the percentage is their average. Last audited 2026-10-13: the harnesses pass on Linux and macOS. The [port ledger](tools/port_ledger.py) adds a method-by-method view of what is ported.
Each subsystem scores 0 (absent), 0.25 (started, not playable), 0.5 (playable,
major vanilla features missing), 0.75 (playable, mostly complete) or 1 (complete
and verified against vanilla by differential tests). The percentage is the
average; `tests/test_parity.py` keeps it in sync with this table. Last audited
2026-10-13 against the harness results: all 281 harnesses pass on Linux, each
rerun whenever one of its inputs changed, and the Mac mini now runs every one of
them too (Rust uses Java's own float maths on each platform).
| Subsystem | Score | Main gaps |
| --- | ---: | --- |
| Window, input and game loop | 1 | None: game loop, options, camera, auto-jump, toggle sprint/sneak and frame limiter match |
| Assets and resources | 1 | None: item models match over 196,290 stacks; the resource pack screen over 3,000 sessions |
| Network protocol and multiplayer | 1 | None: joins vanilla servers; the dedicated server matches vanilla's files, login, configuration, play and command tree; LAN discovery over 462 cases (offline mode only) |
| Window, input and game loop | 1 | None: the game loop, options, camera, auto-jump, toggle sprint/sneak, local input and the frame limiter match vanilla |
| Assets and resources | 1 | None: item models match vanilla over 196,290 stacks and the client draws every item through them; the resource pack screen matches vanilla's drawing over 3,000 sessions of clicks, scrolling and searching, its incompatibility confirmation included |
| Network protocol and multiplayer | 1 | None: joins vanilla servers; the dedicated server (`--dedicated`) matches vanilla's files, status ping, login, configuration, play, chunks, chat, recipes, advancements and command tree, a client built from vanilla's packet code joins it, Horizon joins a vanilla server, and LAN discovery matches over 462 cases (offline mode only) |
| Chunk storage and palettes | 1 | None: client and saves share paletted containers, identical to vanilla over 266 scenarios |
| Section meshing | 1 | None: 112 scenes and over a million quads identical to vanilla |
| Rendering | 1 | None: entity models, block entity renderers (407 cases) and animations (296), HUD overlays (2,200 states), the recipe book (600), player list, subtitles and world border match |
| Rendering | 1 | None: entity models, block entity renderers (407 cases, End portals, gateways and the loom's banner preview included) and their animations (296 cases, End gateway beams included), the HUD overlays (2,200 states), the recipe book (600 states), the player list, subtitles and the world border match vanilla |
| Lighting | 1 | None: saved and relit light matches vanilla, slabs and stairs included |
| World generation | 1 | None: 2,352 whole chunks over 225 structure sites, in all three dimensions, identical to vanilla |
| Region files and saves | 1 | None: saves round-trip with vanilla; worlds from 1.8 to 26.2 upgrade as vanilla's DataFixers do |
| Entities and physics | 1 | None: attributes, spawn rules, natural spawning (15,500 cases), raids and patrols (1,900), `finalizeSpawn` (24,000) and mob AI (92 scenarios) match |
| Interaction and inventory | 1 | None: menu clicks over 8,000 sequences; recipe book placement (6,000), buttons (1,200), saved book (1,500), loom and crafter screens (1,600) |
| Menus, HUD and chat | 1 | None: every command (758 over 39,685 inputs), scoreboards (81,000 operations), the player list (872,000 draw calls), chat and its suggestions (3,000 sessions) match |
| Region files and saves | 1 | None: saves round-trip with vanilla, and worlds from 1.8 to 26.2 upgrade as vanilla's DataFixers do, superflat worlds included; old options.txt and hotbar.nbt files are upgraded too |
| Entities and physics | 1 | None: attributes, dimensions, death loot, spawn rules, natural spawning (15,500 cases), raids and patrols (1,900 cases), spawned mobs' gear, enchantments and state (`finalizeSpawn`, 24,000 cases) and mob AI (92 scenarios: villager breeding and gossip, the warden, ghastlings and copper golems included) match vanilla |
| Interaction and inventory | 1 | None: menu clicks match over 8,000 click sequences, the loom and the crafter included; recipe book placement (6,000 cases), its buttons (1,200), the saved book (1,500) and the loom and crafter screens (1,600) match vanilla |
| Menus, HUD and chat | 1 | None: every command runs as vanilla (758 commands over 39,685 parsed inputs), scoreboards and teams match (81,000 operations), the player list matches over 872,000 draw calls with heads drawn from players' skins (authlib's textures, SkinManager's lookup, legacy skin conversion), and the chat matches focused and scrolled; the resource pack screen (selection over 4,000 cases, drawing and its incompatibility confirmation over 3,000 sessions) and the chat input with its command suggestions, usage lines, Tab completion and history (3,000 sessions on vanilla's client command tree) match vanilla |
| Audio | 1 | None: the sound engine, sound events, music, situational music, ambience and subtitles match vanilla |
| Block behaviours and ticking | 1 | None: block updates, random ticks, redstone, dispensers (6,000 cases), crafters (4,000), container spills (4,000), copper golems and the sculk family (435 scenarios) match |
| Block behaviours and ticking | 1 | None: block updates, random ticks, redstone, dispensers (6,000 cases), crafters (4,000 crafts), block entity removal (containers spill, 4,000 cases), copper golem chest sorting and the sculk family (435 scenarios: shrieker warnings and warden summoning included) match vanilla |
### Known issues
Horizon can't sign in to a Microsoft account yet, so it can't join online-mode servers (it joins offline-mode ones); skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
## Licence
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Horizon is made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)) and [its contributors](https://github.com/horizon-rust/horizon/graphs/contributors); support it on [GitHub Sponsors](https://github.com/sponsors/ludovic111).
[MIT](LICENSE), for the project's original work only. Dependencies keep their own licences. Minecraft is a trademark of Mojang Synergies AB; Minecraft code and assets belong to Mojang and Microsoft and are covered by the [Minecraft EULA](https://www.minecraft.net/en-us/eula). Decompiled game source and prepared resources stay on your machine and are never published.
NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT. Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
Third-party credits and licenses: [Third-party notices](THIRD_PARTY_NOTICES.md).
## Author
Made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)).
+566 -502
View File
File diff suppressed because it is too large. Load diff
+2 -289
View File
@@ -1,74 +1,12 @@
# Third-party notices
## Code removed for its license
Horizon once shipped three pieces of code under MIT that came from projects whose licenses don't allow that. All
three are gone from main. They are listed here for anyone who holds an older commit, tag or release: in those
versions, that code is not covered by Horizon's MIT license. It stays under its authors' copyright and its original
license.
- **Shader packs written from Iris's source.** `crates/horizon-shaderpack` and the render stages that used it were
written from the source of [Iris](https://github.com/IrisShaders/Iris) (LGPL-3.0, copyright the Iris contributors).
They were added from 15 October 2026 (`b22583d057`), shipped in the game releases `v2026.10.16.02` to `v0.2.46`,
and were removed on 24 October 2026 ([#1561](https://github.com/horizon-rust/horizon/pull/1561)) after a developer
of Iris showed us. Shader packs came back in `crates/horizon-shaders`, first written from a description of the
pack format.
- **Math functions translated from OpenJDK.** In `crates/horizon-core/src/jmath.rs`, `fdlibm::{log, log10, cbrt,
hypot}` were translated from OpenJDK's `java/lang/FdLibm.java` and `round_f32` from its `java/lang/Math.java`
(GPL-2.0 with the Classpath Exception), and `aarch64::{sin, cos}` from HotSpot's `macroAssembler_aarch64_trig.cpp`
(GPL-2.0); copyright Oracle and/or its affiliates, and Cavium for that file. The `fdlibm` and `aarch64` functions
were added on 13 October 2026 (`558d140fca`, first released in `v2026.10.13`). `round_f32` was added on 14 October
2026 as `java_round` in `crates/horizon-client/src/mipmap.rs` (`95db0fce9d`) and moved into `jmath.rs` on 3
November ([#7770](https://github.com/horizon-rust/horizon/pull/7770)). All of them were replaced in
[#11944](https://github.com/horizon-rust/horizon/pull/11944) by code written without OpenJDK's sources, from Sun's
fdlibm C sources, the Java SE API documentation and a description of the behaviour, and checked against a JVM
([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
- **Java's `HashMap` translated from OpenJDK.** `crates/horizon-world/src/java_hash_map.rs`, which reproduces the
iteration order of `java.util.HashMap`, was translated from OpenJDK's `java/util/HashMap.java` (GPL-2.0 with the
Classpath Exception; copyright Oracle and/or its affiliates). It was added on 7 October 2026 (`118e93ea51` and
`194db0dbb4`, first released in `v2026.10.12`). It was replaced in
[#12043](https://github.com/horizon-rust/horizon/pull/12043) by code written without OpenJDK's sources, from a
description of the behaviour, the Java SE API documentation and the textbook red-black tree, and checked against a
JVM ([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
## Catnip / Ponder
`crates/horizon-client-render/src/fluid_render.rs` translates the still-fluid face tiling and
UV rules of Catnip's `FluidRenderHelper` from Ponder 1.0.82 for Minecraft 1.21.1
(`net.createmod.ponder:ponder-neoforge:1.0.82+mc1.21.1`). Copyright (c) 2022 The Create Team,
licensed under MIT (`LICENSE_Ponder` in that artifact); the full license heads the Rust file.
The native renderer uses Horizon's own block-sprite arrays, pose and light packing. The
development-only oracle calls the unchanged helper from the player's local artifact. No
original classes, game shaders, textures or prepared resources are distributed here.
## wgpu-hal
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
developers, with upstream fixes applied
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124),
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). It is licensed under MIT or
developers, with one upstream fix applied
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124)). It is licensed under MIT or
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
## naga
`third_party/naga` is [naga](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs developers, with
fixes to its GLSL frontend that shader packs need, each applied in the commit that needed it. It is licensed under MIT or Apache-2.0; both licenses
are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
The GLSL frontend also resets the common type for each comma-separated declarator so one
variable's array specifier does not change later variables.
It also reads a struct member outside a constant expression through a pointer to the member,
not out of a loaded copy of the whole struct, so a member an atomic function uses can be read.
## libmimalloc-sys and mimalloc
`third_party/libmimalloc-sys` is [libmimalloc-sys](https://github.com/purpleprotocol/mimalloc_rust)
0.1.49 by Octavian Oncescu, with the [mimalloc](https://github.com/microsoft/mimalloc) v3 sources it
bundles (Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen), both under the MIT license
(`LICENSE.txt` and `c_src/mimalloc/v3/LICENSE` in that folder). It holds only the v3 sources. One
change: `mi_popcount` in `include/mimalloc/bits.h` uses mimalloc's portable bit count on MSVC
x86/x64 instead of the POPCNT instruction, which CPUs before Nehalem lack (#7264).
## Pixelify Sans
`crates/horizon-theme/fonts/PixelifySans-*.ttf` is [Pixelify Sans](https://github.com/eifetx/Pixelify-Sans)
@@ -81,228 +19,3 @@ by The Pixelify Sans Project Authors, licensed under the SIL Open Font License 1
[Pixelarticons](https://github.com/halfmage/pixelarticons) by Gerrit Halfmann, under the MIT
license (`crates/horizon-theme/icons/pixel/LICENSE-pixelarticons.txt`). `star.svg` is Horizon's own, drawn on
the same grid.
## Hugeicons
`crates/horizon-theme/icons/glass/*.svg` are icons from the free set of
[Hugeicons](https://github.com/hugeicons/hugeicons) (`@hugeicons/core-free-icons` 4.4.0), under
the MIT license (`crates/horizon-theme/icons/glass/LICENSE-hugeicons.txt`), exported with a 1.6
stroke in `currentColor`.
`crates/horizon-theme/icons/glass/discord.svg` is Discord's logo as drawn by
[Simple Icons](https://github.com/simple-icons/simple-icons) 13.21.0 (CC0 1.0). The logo is Discord's
trademark, used only on the button that opens Horizon's Discord server.
## AMD FidelityFX SDK (FSR 3.1 frame generation)
`crates/horizon-client-render/src/gfx/frame_gen/optical_flow.wgsl` and `interpolation.wgsl` are
ports to WGSL of the optical flow and frame interpolation shaders of the
[AMD FidelityFX SDK](https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK) 1.1.4
(`sdk/include/FidelityFX/gpu/{opticalflow,frameinterpolation}`, at commit
[c6efa6bf7f20](https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/tree/c6efa6bf7f2027b3ec94f28578bb5965eabb9e55)),
and `optical_flow.rs` follows the SDK's `sdk/src/components/opticalflow/ffx_opticalflow.cpp`
(its passes, their order and their dispatch sizes). Copyright (C) 2024 Advanced Micro Devices,
Inc., under the MIT license, whose text heads each file. AMD and FidelityFX are AMD's
trademarks; Horizon is not affiliated with AMD.
## Mod loader logos
The launcher shows the logo of the mod loader an instance plays with, and offers the logos as
instance icons.
- `launcher/assets/logos/{fabric,quilt,neoforge,forge}.png` are the loaders' logos as the launcher
mockup on [playhorizon.dev/launcher](https://playhorizon.dev/launcher) shows them (horizon-site
`public/media/launcher/*.webp`, converted to PNG), each its project's own mark:
[Fabric](https://fabricmc.net/), [Quilt](https://quiltmc.org/) (its art is
[CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/)),
[NeoForged](https://neoforged.net/) (its art, by Ridanisaurus, is
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)) and
[Minecraft Forge](https://minecraftforge.net/). They name the loader an instance runs.
Each logo belongs to its project. Showing it says which loader an instance runs; it does not mean
the project endorses Horizon.
## Sign in with Microsoft
The launcher's sign-in buttons follow Microsoft's published branding guidelines for apps that sign in
with Microsoft accounts
([Sign in with Microsoft branding](https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps)).
The four-square Microsoft symbol in `launcher/src/microsoft.rs` reproduces the squares, positions and
colours of Microsoft's official `ms-symbollockup_mssymbol_19.svg` from that page, unmodified. Microsoft
and its logo are trademarks of the Microsoft group of companies; Horizon is not endorsed by Microsoft.
## CIT Resewn
`crates/horizon-optifine/src/cit.rs` and `crates/horizon-client-render/src/cit.rs` follow the
condition semantics of [CIT Resewn Continuation](https://github.com/EthanVisagie/CIT-Resewn-Continuation-1.21.11)
(at `b792f9a`), a continuation of [CIT Resewn](https://github.com/SHsuperCM/CITResewn) by SHsuperCM: the
1.21 component paths, enchantment levels, percent damage, text matching and rule ordering. It is
licensed under the MIT license:
> Copyright (c) 2021 SHsuperCM
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software
> and associated documentation files (the "Software"), to deal in the Software without
> restriction, including without limitation the rights to use, copy, modify, merge, publish,
> distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
> Software is furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all copies or
> substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
> BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
> DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## FusionExamples
The tile tables in `crates/horizon-optifine/src/fusion/layout.rs` were read off the connecting
texture templates (`templates/template <layout>.png`) of
[FusionExamples](https://github.com/SuperMartijn642/FusionExamples) (at `8c9c56e`), which is
licensed under the MIT license:
> Copyright (c) 2024 Martijn van den Brand
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software
> and associated documentation files (the "Software"), to deal in the Software without
> restriction, including without limitation the rights to use, copy, modify, merge, publish,
> distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
> Software is furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all copies or
> substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
> BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
> DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## BlueMap
`crates/horizon-client-render/src/bluemap.rs` reads the lowres tiles of a server's
[BlueMap](https://github.com/BlueMap-Minecraft/BlueMap) web map. It follows three of BlueMap's files:
`core/src/main/java/de/bluecolored/bluemap/core/map/lowres/LowresLayer.java` (`saveTile`) for the
tiles' pixel layout, `common/webapp/src/js/util/Utils.js` (`pathFromCoords`) for their paths, and
`core/src/main/java/de/bluecolored/bluemap/core/map/MapSettingsSerializer.java` for a map's
`settings.json`, at commit
[96ae320d1d39](https://github.com/BlueMap-Minecraft/BlueMap/tree/96ae320d1d39581c10058d8222f2290f2fcb5333).
BlueMap is licensed under the MIT license:
> Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
> Copyright (c) contributors
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.
## JOML
`crates/horizon-client-render/src/camera.rs` (`joml::Mat4::rotation`, `invert_orthonormal`) and
`crates/horizon-client-render/src/gfx/pack_uniforms.rs` (`invert_generic`, `invert_affine`, the
formulas of `pack_projection` and `shadow_ortho`) adapt the float arithmetic of
`Matrix4f.rotation(Quaternionfc)`, `invertOrthonormal`, `invertGenericNonThis`, `invertAffine`,
`setPerspective` and `setOrtho` from
[JOML](https://github.com/JOML-CI/JOML/tree/faf9eef2511242122beb6055fe3da92daed214c5) 1.10.9, the
library vanilla 26.3 uses, so that shader packs get the same matrices to the last bit. Changes:
ported to Rust; the pack_uniforms.rs functions drop JOML's property flags, and
`pack_projection` works the formula in doubles. The copyright line and license notice also head
both files.
`camera.rs` also adapts `Quaternionf.rotationX`, `rotationY` and `rotationZ`,
`Matrix4f.rotateAffine` and `Vector4f.mulAffine` (`joml::Quat::rotation_x`/`_y`/`_z`,
`joml::Mat4::rotate_affine`, `transform_affine`), for the sky's positions shader packs read.
Changes: ported to Rust; `rotate_affine` keeps only the affine case.
```
The MIT License
Copyright (c) 2015-2026 Richard Greenlees
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
## Just Enough Items
The Recipes GUI sprites and settings icon under `crates/horizon-client-render/src/jei_recipes` are from
[Just Enough Items 31.17.0.108](https://github.com/mezz/JustEnoughItems/tree/0371fa6fafa14635a356019e4604f5580b26a34f)
by mezz and contributors, under the MIT license in that directory (`LICENSE.txt`).
## Flywheel native model directional shading
The two-light diffuse expression and transformed-normal policy in
`terrain.wgsl`'s native model entries follow Flywheel 1.0.6
(`internal/diffuse.glsl`, `instance/transformed.vert`, `internal/common.vert`).
No Flywheel classes, compiled shaders or original model/texture assets are bundled.
Copyright (c) 2021-2024 Jozufozu
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## fdlibm
`crates/horizon-core/src/jmath/fdlibm.rs` and the `fdlibm_asin`, `fdlibm_atan` and
`fdlibm_atan2` functions in `crates/horizon-core/src/geometry/trig.rs` are Rust ports of
[fdlibm](https://www.netlib.org/fdlibm/) 5.3, Sun's freely distributable math library, whose
results the Java SE API specifies for `StrictMath`. `fdlibm.rs` ports `e_log.c`, `e_log10.c`,
`s_cbrt.c` and `e_hypot.c`, and, with fused multiply-adds, `s_sin.c`, `s_cos.c`, `k_sin.c`,
`k_cos.c` and `e_rem_pio2.c`; `trig.rs` ports `e_asin.c`, `s_atan.c` and `e_atan2.c`. This
notice also heads both files:
```
Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
Developed at SunSoft, a Sun Microsystems, Inc. business.
Permission to use, copy, modify, and distribute this
software is freely granted, provided that this notice
is preserved.
```
@@ -1,42 +0,0 @@
# Entity occlusion split measurements — 2026-11-02
These measurements came from the pre-split integration branch for PR #7054. That build contained the entity-occlusion rewrite and particle light lookup cache; Particle Culling was Off. They are useful comparison evidence, but they are not a benchmark of this isolated entity-only commit. The cache-only run in PR #7391 measured effectively the same CPU time on the ordinary scene, but the combined result still cannot isolate interactions.
Host: Intel Core i7-3770S, Intel HD Graphics 4000, Vulkan/Mesa, 675×727, render distance 4, VSync off, 260 FPS cap. Two opposite-order passes measured for 8 seconds after warmup. Values are means. CPU is harness average CPU ms per frame; p99 is frame time. The driver supplied no usable GPU timing.
## Ordinary scene
The scene had 24 mobs, roughly 9,400–10,100 rain particles and campfire smoke.
| Build and mode | FPS | CPU ms/frame | Frame p99 ms |
| --- | ---: | ---: | ---: |
| Main, entity culling Off | 88.59 | 1.829 | 24.51 |
| Pre-split integration branch, all culling Off | 88.33 | 2.063 | 24.51 |
| Main, entity culling Synchronous | 89.34 | 1.965 | 23.99 |
| Pre-split integration branch, entity Synchronous | 89.13 | 2.113 | 24.15 |
This normal-scene run does not show a large FPS change; its CPU differences are small relative to frame time and not attributable to the entity-only change by themselves.
## Opaque-wall scene
A 32×32 platform at Y=300, a 7-block wall with a one-block vertical opening, and 192 pigs spawned behind the wall (216 mobs in the captured scene). The player looked toward the opening from the near side. The same world and camera path were used for the main and integration builds.
| Build and mode | FPS | CPU ms/frame | Frame p99 ms |
| --- | ---: | ---: | ---: |
| Main, Off | 89.08 | 2.423 | 13.52 |
| Integration branch, Off | 89.23 | 2.355 | 13.27 |
| Main, Synchronous | 92.81 | 2.566 | 14.60 |
| Integration branch, Synchronous | 93.21 | 2.550 | 12.84 |
| Main, Asynchronous | 92.73 | 2.364 | 13.04 |
| Integration branch, Asynchronous | 92.62 | 2.314 | 13.18 |
The asynchronous mode reduced measured CPU time slightly in this wall scene but did not improve FPS beyond run-to-run spread. This is a synthetic occlusion-heavy scene, not a broad performance claim.
## Captures
The HUD performance overlay is visible in each capture. The integration-build screenshots were recorded with the same wall and camera position in Off, Synchronous, and Asynchronous modes.
- Main, Off: `main-off.png`
- Integration branch, Off: `branch-off.png`
- Integration branch, Synchronous: `branch-sync.png`
- Integration branch, Asynchronous: `branch-async.png`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

@@ -1,42 +0,0 @@
# Entity occlusion pipeline — release/OpenGL run (2026-11-04)
This is the final local smoke benchmark for the entity-occlusion pipeline before opening the PR. It was run from the branch after the release build completed, with the OpenGL backend selected explicitly. The same process created each scene, switched the culling mode, flew the camera for two seconds, collected frame/CPU telemetry, and captured the screenshot after the scene settled.
**Host:** Intel Core i7-3770S, Intel HD Graphics 4000, Mesa OpenGL 4.2
**Window:** 1280×720, offscreen, VSync disabled
**World:** fresh temporary home, 32×32 platform at Y=300; wall scenes add a solid stone wall and an optional one-block slit
**Entities:** 24 pigs in the normal scene, 192 pigs in wall scenes, 512 pigs in stress scenes
**Settings:** render distance 8, `entityCullingPrecheck:64`, FPS overlay on, no resource-pack occlusion assumptions
**Build:** `cargo build --release -p horizon-client`
## Results
The values below are branch-only measurements. They are useful for comparing the three modes under identical geometry and for checking that the worker is doing useful work. A fresh main binary was not built in this short pass, so these numbers do not claim a controlled main-to-branch delta.
| Scene | Off FPS / CPU p50 | Sync FPS / CPU p50 | Async FPS / CPU p50 | Async entities culled |
| --- | ---: | ---: | ---: | ---: |
| `entity_normal` (24 visible) | 240.5 / 0.586 ms | 233.5 / 0.762 ms | 239.3 / 0.686 ms | 0 |
| `entity_wall` (192, solid) | 228.8 / 1.245 ms | 318.6 / 1.342 ms | **324.5 / 0.478 ms** | 124,608 |
| `entity_wall_slit` (192, one-block slit) | 231.4 / 1.102 ms | 265.0 / 1.387 ms | **281.2 / 0.716 ms** | 102,826 |
| `entity_stress` (512, solid) | 190.3 / 2.467 ms | 299.2 / 2.593 ms | **325.5 / 0.691 ms** | 290,234 |
| `entity_stress_slit` (512, one-block slit) | 190.4 / 2.476 ms | 262.2 / 2.573 ms | **262.1 / 1.297 ms** | 227,301 |
Async telemetry confirmed that the render thread stayed non-blocking (`try_recv`/`try_send`, `worker_blocking_wait_ms: 0`). The latest wall/slit run completed 562 worker jobs, queued 3,982 queries, avoided about 4.0 million entity bytes, and had 379 ns recent p50 worker-job time. The solid 512-pig run culled 15,552 generated vertices; the slit run culled 120,290 generated vertices. The normal scene recorded no pre-model hides and kept the opt-in path close to Off.
## Captures
Every image includes the FPS performance overlay. The names are `<mode>-<scene>.png`:
- [Off — normal](screenshots/off-entity_normal.png), [wall](screenshots/off-entity_wall.png), [wall slit](screenshots/off-entity_wall_slit.png), [stress](screenshots/off-entity_stress.png), [stress slit](screenshots/off-entity_stress_slit.png)
- [Sync — normal](screenshots/sync-entity_normal.png), [wall](screenshots/sync-entity_wall.png), [wall slit](screenshots/sync-entity_wall_slit.png), [stress](screenshots/sync-entity_stress.png), [stress slit](screenshots/sync-entity_stress_slit.png)
- [Async — normal](screenshots/async-entity_normal.png), [wall](screenshots/async-entity_wall.png), [wall slit](screenshots/async-entity_wall_slit.png), [stress](screenshots/async-entity_stress.png), [stress slit](screenshots/async-entity_stress_slit.png)
## Reproduction
From the repository root, with prepared 26.3 resources available:
```sh
python3 tools/entity_occlusion_bench.py
```
The script uses fresh temporary game data, the release binary in `target/release`, OpenGL, the FPS overlay, and writes JSON plus captures under `/tmp/horizon-entity-occlusion-release-quick/`.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

@@ -1,27 +0,0 @@
# Particle culling review measurements — 2026-11-02
Ordinary-scene captures and measurements for PR #7054. The scene contains 24 mobs, about 9,400–10,100 rain particles, and about 50–100 campfire smoke particles. Two opposite-order passes were run with 2 s warmup and 8 s measurement per condition. Host: Intel Core i7-3770S, Intel HD Graphics 4000, Vulkan/Mesa, 675×727, render distance 4, VSync off, 260 FPS cap.
These are means of the two samples. CPU is the harness's average CPU time per frame; p99 is frame time. The normal scene is not expected to show large gains because most particles are visible.
| Condition | FPS | CPU ms/frame | Frame p99 ms |
| --- | ---: | ---: | ---: |
| Main, culling Off | 88.39 | 1.930 | 23.85 |
| Particle branch, culling Off | 88.55 | 1.828 | 24.88 |
| Main, Entity Culling Synchronous | 89.12 | 1.887 | 23.07 |
| Particle branch, Entity Culling Synchronous, Particle Culling Off | 89.06 | 1.897 | 25.18 |
| Particle Culling Synchronous | 88.87 | 1.774 | 24.94 |
| Particle Culling Asynchronous* | 89.00 | 1.754 | 24.90 |
*The asynchronous samples predate the final switch to a dedicated particle worker. They are retained only as historical context and are not a measurement of the final worker implementation. No FPS improvement is claimed from these ordinary-scene runs. The Off path delegates directly to `render`; `disabled_culled_render_matches_the_regular_particle_path` checks that it produces the same vertices.
The FPS and CPU differences are within run-to-run variability for this GPU-limited scene. The first four conditions are the reviewer's requested Main-versus-branch default and existing synchronous-mode comparisons. Particle Sync/Async rows are branch-internal mode comparisons.
## Captures
The screenshots show the same ordinary test world and FPS overlay:
- `main-off.png`
- `particle-off.png`
- `particle-sync.png`
- `particle-async.png`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

-870
View File
@@ -1,870 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-04T13:00:15Z",
"app": {
"name": "horizon-client",
"version": "nightly-2026-11-04-67dad0d",
"commit": "67dad0d"
},
"hardware": {
"os": "linux",
"arch": "x86_64",
"cpu_threads": 24,
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.57)",
"cpu": "13th Gen Intel(R) Core(TM) i7-13700KF",
"ram_mb": 31904,
"gpu": {
"name": "NVIDIA GeForce RTX 4080",
"vendor": "nvidia",
"driver": "NVIDIA 595.71.05",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 12,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 28.408,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 66658.183,
"stdev": 61839.502,
"min": 1827.42,
"max": 124993.694,
"ci95": 153630.237
},
"startup.world_ms": {
"n": 3,
"mean": 74754.411,
"stdev": 59189.587,
"min": 13600.651,
"max": 131761.773,
"ci95": 147046.952
},
"chunks.village.load_ms": {
"n": 3,
"mean": 5571.499,
"stdev": 1900.32,
"min": 3378.38,
"max": 6730.394,
"ci95": 4721.037
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 15287.0,
"stdev": 527.501,
"min": 14788.0,
"max": 15839.0,
"ci95": 1310.49
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2958.557,
"stdev": 376.543,
"min": 2526.453,
"max": 3216.434,
"ci95": 935.46
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 368.457,
"stdev": 90.668,
"min": 300.631,
"max": 471.438,
"ci95": 225.25
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 91.04,
"stdev": 40.506,
"min": 46.124,
"max": 124.796,
"ci95": 100.63
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 581.0,
"stdev": 48.0,
"min": 533.0,
"max": 629.0,
"ci95": 119.248
},
"memory.village.rss_mb": {
"n": 3,
"mean": 1277.531,
"stdev": 25.546,
"min": 1253.879,
"max": 1304.623,
"ci95": 63.466
},
"tick.village.client_ms": {
"n": 3,
"mean": 0.529,
"stdev": 0.127,
"min": 0.396,
"max": 0.649,
"ci95": 0.315
},
"cpu.village.p50_ms": {
"n": 3,
"mean": 0.185,
"stdev": 0.034,
"min": 0.162,
"max": 0.224,
"ci95": 0.085
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 3318.3,
"stdev": 911.608,
"min": 2266.627,
"max": 3883.075,
"ci95": 2264.743
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 16565.333,
"stdev": 172.755,
"min": 16367.0,
"max": 16683.0,
"ci95": 429.182
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2333.626,
"stdev": 569.124,
"min": 1745.365,
"max": 2881.457,
"ci95": 1413.897
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 336.615,
"stdev": 185.666,
"min": 208.133,
"max": 549.487,
"ci95": 461.257
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 160.734,
"stdev": 127.586,
"min": 39.777,
"max": 294.05,
"ci95": 316.967
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 666.333,
"stdev": 24.44,
"min": 645.0,
"max": 693.0,
"ci95": 60.718
},
"memory.forest.rss_mb": {
"n": 3,
"mean": 1346.816,
"stdev": 42.279,
"min": 1305.547,
"max": 1390.037,
"ci95": 105.035
},
"tick.forest.client_ms": {
"n": 3,
"mean": 0.669,
"stdev": 0.084,
"min": 0.598,
"max": 0.761,
"ci95": 0.208
},
"cpu.forest.p50_ms": {
"n": 3,
"mean": 0.222,
"stdev": 0.032,
"min": 0.197,
"max": 0.258,
"ci95": 0.079
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2463.434,
"stdev": 1056.0,
"min": 1527.516,
"max": 3608.283,
"ci95": 2623.462
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 14226.667,
"stdev": 307.077,
"min": 14038.0,
"max": 14581.0,
"ci95": 762.883
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2192.874,
"stdev": 1320.904,
"min": 673.829,
"max": 3071.415,
"ci95": 3281.573
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 345.406,
"stdev": 294.904,
"min": 8.977,
"max": 559.232,
"ci95": 732.642
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 172.573,
"stdev": 149.386,
"min": 1.038,
"max": 274.089,
"ci95": 371.125
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 629.0,
"stdev": 32.0,
"min": 597.0,
"max": 661.0,
"ci95": 79.499
},
"memory.ocean.rss_mb": {
"n": 3,
"mean": 1352.223,
"stdev": 51.951,
"min": 1301.492,
"max": 1405.314,
"ci95": 129.065
},
"tick.ocean.client_ms": {
"n": 3,
"mean": 0.512,
"stdev": 0.099,
"min": 0.406,
"max": 0.603,
"ci95": 0.246
},
"cpu.ocean.p50_ms": {
"n": 3,
"mean": 0.178,
"stdev": 0.038,
"min": 0.135,
"max": 0.206,
"ci95": 0.094
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 6260.675,
"stdev": 3077.348,
"min": 2976.342,
"max": 9077.55,
"ci95": 7645.173
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 16015.333,
"stdev": 209.872,
"min": 15826.0,
"max": 16241.0,
"ci95": 521.393
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1412.095,
"stdev": 962.145,
"min": 714.773,
"max": 2509.775,
"ci95": 2390.293
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 182.479,
"stdev": 177.563,
"min": 13.364,
"max": 367.429,
"ci95": 441.126
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 88.451,
"stdev": 87.578,
"min": 1.526,
"max": 176.667,
"ci95": 217.572
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 799.667,
"stdev": 18.475,
"min": 789.0,
"max": 821.0,
"ci95": 45.899
},
"memory.mountain.rss_mb": {
"n": 3,
"mean": 1491.572,
"stdev": 36.719,
"min": 1459.775,
"max": 1531.76,
"ci95": 91.221
},
"tick.mountain.client_ms": {
"n": 3,
"mean": 0.697,
"stdev": 0.155,
"min": 0.602,
"max": 0.876,
"ci95": 0.386
},
"cpu.mountain.p50_ms": {
"n": 3,
"mean": 0.325,
"stdev": 0.115,
"min": 0.239,
"max": 0.456,
"ci95": 0.286
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1545.896,
"stdev": 30.591,
"min": 1524.965,
"max": 1581.004,
"ci95": 75.999
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1348.389,
"stdev": 52.5,
"min": 1296.172,
"max": 1401.168,
"ci95": 130.428
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 37594,
"seconds": 12.0,
"avg_fps": 3132.783,
"low_1pct_fps": 471.438,
"low_01pct_fps": 124.796,
"p50_ms": 0.271,
"p95_ms": 0.494,
"p99_ms": 1.127,
"p999_ms": 2.15,
"max_ms": 155.204,
"cpu_p50_ms": 0.168,
"gpu_p50_ms": 0.104,
"histogram": [
37151,
394,
30,
8,
5,
2,
2,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 38598,
"seconds": 12.0,
"avg_fps": 3216.434,
"low_1pct_fps": 300.631,
"low_01pct_fps": 46.124,
"p50_ms": 0.267,
"p95_ms": 0.446,
"p99_ms": 1.034,
"p999_ms": 1.941,
"max_ms": 493.993,
"cpu_p50_ms": 0.162,
"gpu_p50_ms": 0.104,
"histogram": [
38177,
387,
16,
8,
4,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
1,
1,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 30318,
"seconds": 12.0,
"avg_fps": 2526.453,
"low_1pct_fps": 333.302,
"low_01pct_fps": 102.199,
"p50_ms": 0.305,
"p95_ms": 0.757,
"p99_ms": 1.69,
"p999_ms": 3.539,
"max_ms": 170.856,
"cpu_p50_ms": 0.224,
"gpu_p50_ms": 0.106,
"histogram": [
29368,
759,
139,
35,
11,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 34578,
"seconds": 12.0,
"avg_fps": 2881.457,
"low_1pct_fps": 549.487,
"low_01pct_fps": 294.05,
"p50_ms": 0.32,
"p95_ms": 0.483,
"p99_ms": 1.213,
"p999_ms": 2.5,
"max_ms": 5.358,
"cpu_p50_ms": 0.197,
"gpu_p50_ms": 0.154,
"histogram": [
34119,
379,
64,
9,
5,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 28489,
"seconds": 12.0,
"avg_fps": 2374.055,
"low_1pct_fps": 208.133,
"low_01pct_fps": 39.777,
"p50_ms": 0.313,
"p95_ms": 0.937,
"p99_ms": 1.863,
"p999_ms": 4.318,
"max_ms": 571.48,
"cpu_p50_ms": 0.211,
"gpu_p50_ms": 0.151,
"histogram": [
27193,
1071,
140,
51,
19,
9,
1,
2,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 20945,
"seconds": 12.0,
"avg_fps": 1745.365,
"low_1pct_fps": 252.226,
"low_01pct_fps": 148.375,
"p50_ms": 0.343,
"p95_ms": 1.505,
"p99_ms": 2.758,
"p999_ms": 5.341,
"max_ms": 11.642,
"cpu_p50_ms": 0.258,
"gpu_p50_ms": 0.162,
"histogram": [
18145,
2293,
337,
106,
38,
13,
6,
5,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 36856,
"seconds": 12.0,
"avg_fps": 3071.415,
"low_1pct_fps": 559.232,
"low_01pct_fps": 274.089,
"p50_ms": 0.286,
"p95_ms": 0.492,
"p99_ms": 1.179,
"p999_ms": 2.711,
"max_ms": 6.718,
"cpu_p50_ms": 0.135,
"gpu_p50_ms": 0.127,
"histogram": [
36366,
405,
60,
13,
10,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 34001,
"seconds": 12.0,
"avg_fps": 2833.377,
"low_1pct_fps": 468.008,
"low_01pct_fps": 242.591,
"p50_ms": 0.295,
"p95_ms": 0.589,
"p99_ms": 1.41,
"p999_ms": 3.048,
"max_ms": 7.7,
"cpu_p50_ms": 0.206,
"gpu_p50_ms": 0.126,
"histogram": [
33314,
544,
106,
23,
10,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 8086,
"seconds": 12.0,
"avg_fps": 673.829,
"low_1pct_fps": 8.977,
"low_01pct_fps": 1.038,
"p50_ms": 0.302,
"p95_ms": 0.665,
"p99_ms": 2.495,
"p999_ms": 15.716,
"max_ms": 4849.364,
"cpu_p50_ms": 0.194,
"gpu_p50_ms": 0.128,
"histogram": [
7808,
166,
50,
26,
8,
2,
3,
5,
2,
5,
1,
2,
2,
0,
0,
0,
0,
0,
0,
1,
1,
1,
1,
2
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 12141,
"seconds": 12.0,
"avg_fps": 1011.738,
"low_1pct_fps": 166.643,
"low_01pct_fps": 87.16,
"p50_ms": 0.641,
"p95_ms": 2.642,
"p99_ms": 4.468,
"p999_ms": 7.821,
"max_ms": 22.536,
"cpu_p50_ms": 0.456,
"gpu_p50_ms": 0.138,
"histogram": [
7840,
2996,
866,
278,
87,
38,
16,
11,
2,
5,
0,
0,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 30118,
"seconds": 12.0,
"avg_fps": 2509.775,
"low_1pct_fps": 367.429,
"low_01pct_fps": 176.667,
"p50_ms": 0.316,
"p95_ms": 0.771,
"p99_ms": 1.931,
"p999_ms": 4.2,
"max_ms": 14.633,
"cpu_p50_ms": 0.239,
"gpu_p50_ms": 0.127,
"histogram": [
29101,
775,
173,
38,
16,
8,
4,
1,
0,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 8577,
"seconds": 12.0,
"avg_fps": 714.773,
"low_1pct_fps": 13.364,
"low_01pct_fps": 1.526,
"p50_ms": 0.416,
"p95_ms": 2.257,
"p99_ms": 4.36,
"p999_ms": 212.217,
"max_ms": 1051.428,
"cpu_p50_ms": 0.28,
"gpu_p50_ms": 0.132,
"histogram": [
7017,
978,
377,
111,
39,
24,
10,
6,
1,
0,
2,
2,
0,
0,
0,
0,
0,
0,
1,
0,
1,
2,
4,
2
]
}
],
"label": "horizon-desktop"
}
-870
View File
@@ -1,870 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-04T14:47:19Z",
"app": {
"name": "horizon-client",
"version": "nightly-2026-11-04-67dad0d",
"commit": "67dad0d"
},
"hardware": {
"os": "linux",
"arch": "x86_64",
"cpu_threads": 24,
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.57)",
"cpu": "13th Gen Intel(R) Core(TM) i7-13700KF",
"ram_mb": 31904,
"gpu": {
"name": "NVIDIA GeForce RTX 4080",
"vendor": "nvidia",
"driver": "NVIDIA 595.71.05",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 29.665,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 45386.16,
"stdev": 38970.921,
"min": 1077.276,
"max": 74343.156,
"ci95": 96816.948
},
"startup.world_ms": {
"n": 3,
"mean": 86585.608,
"stdev": 71331.593,
"min": 11201.581,
"max": 153020.143,
"ci95": 177211.803
},
"chunks.village.load_ms": {
"n": 3,
"mean": 11082.587,
"stdev": 5351.291,
"min": 7620.454,
"max": 17246.096,
"ci95": 13294.417
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 48440.333,
"stdev": 656.16,
"min": 48060.0,
"max": 49198.0,
"ci95": 1630.124
},
"fps.village.avg_fps": {
"n": 3,
"mean": 853.775,
"stdev": 345.646,
"min": 524.913,
"max": 1214.059,
"ci95": 858.701
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 130.235,
"stdev": 117.39,
"min": 9.024,
"max": 243.388,
"ci95": 291.635
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 70.65,
"stdev": 68.628,
"min": 1.125,
"max": 138.345,
"ci95": 170.496
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 2202.333,
"stdev": 33.307,
"min": 2165.0,
"max": 2229.0,
"ci95": 82.745
},
"memory.village.rss_mb": {
"n": 3,
"mean": 1652.647,
"stdev": 73.452,
"min": 1568.063,
"max": 1700.344,
"ci95": 182.478
},
"tick.village.client_ms": {
"n": 3,
"mean": 0.493,
"stdev": 0.169,
"min": 0.319,
"max": 0.657,
"ci95": 0.42
},
"cpu.village.p50_ms": {
"n": 3,
"mean": 0.692,
"stdev": 0.207,
"min": 0.531,
"max": 0.925,
"ci95": 0.513
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 15019.921,
"stdev": 12855.4,
"min": 5613.942,
"max": 29668.11,
"ci95": 31937.162
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 80224.667,
"stdev": 678.709,
"min": 79626.0,
"max": 80962.0,
"ci95": 1686.142
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 898.208,
"stdev": 446.3,
"min": 382.944,
"max": 1163.61,
"ci95": 1108.759
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 178.513,
"stdev": 107.096,
"min": 54.933,
"max": 244.255,
"ci95": 266.064
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 103.369,
"stdev": 78.403,
"min": 12.974,
"max": 152.87,
"ci95": 194.779
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 2201.667,
"stdev": 34.429,
"min": 2163.0,
"max": 2229.0,
"ci95": 85.532
},
"memory.forest.rss_mb": {
"n": 3,
"mean": 1809.063,
"stdev": 93.103,
"min": 1707.98,
"max": 1891.305,
"ci95": 231.3
},
"tick.forest.client_ms": {
"n": 3,
"mean": 0.585,
"stdev": 0.147,
"min": 0.493,
"max": 0.754,
"ci95": 0.365
},
"cpu.forest.p50_ms": {
"n": 3,
"mean": 0.989,
"stdev": 0.725,
"min": 0.57,
"max": 1.826,
"ci95": 1.802
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 12399.047,
"stdev": 15052.073,
"min": 3174.115,
"max": 29768.486,
"ci95": 37394.441
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 64737.0,
"stdev": 16149.666,
"min": 46089.0,
"max": 74089.0,
"ci95": 40121.232
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 735.22,
"stdev": 601.75,
"min": 108.108,
"max": 1307.912,
"ci95": 1494.952
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 134.654,
"stdev": 147.391,
"min": 1.252,
"max": 292.881,
"ci95": 366.169
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 62.062,
"stdev": 66.294,
"min": 0.202,
"max": 132.042,
"ci95": 164.697
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 1942.333,
"stdev": 251.725,
"min": 1657.0,
"max": 2133.0,
"ci95": 625.369
},
"memory.ocean.rss_mb": {
"n": 3,
"mean": 1812.661,
"stdev": 102.065,
"min": 1697.912,
"max": 1893.313,
"ci95": 253.564
},
"tick.ocean.client_ms": {
"n": 3,
"mean": 0.421,
"stdev": 0.227,
"min": 0.193,
"max": 0.646,
"ci95": 0.563
},
"cpu.ocean.p50_ms": {
"n": 3,
"mean": 0.513,
"stdev": 0.194,
"min": 0.303,
"max": 0.686,
"ci95": 0.483
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 31815.539,
"stdev": 36758.245,
"min": 9582.166,
"max": 74243.988,
"ci95": 91319.913
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 90876.333,
"stdev": 1525.588,
"min": 89793.0,
"max": 92621.0,
"ci95": 3790.075
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 712.657,
"stdev": 252.815,
"min": 450.565,
"max": 955.041,
"ci95": 628.077
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 117.93,
"stdev": 85.922,
"min": 24.389,
"max": 193.34,
"ci95": 213.46
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 72.407,
"stdev": 65.468,
"min": 3.577,
"max": 133.895,
"ci95": 162.646
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 2783.667,
"stdev": 48.881,
"min": 2741.0,
"max": 2837.0,
"ci95": 121.436
},
"memory.mountain.rss_mb": {
"n": 3,
"mean": 1931.359,
"stdev": 106.681,
"min": 1808.184,
"max": 1994.242,
"ci95": 265.031
},
"tick.mountain.client_ms": {
"n": 3,
"mean": 1.03,
"stdev": 0.685,
"min": 0.564,
"max": 1.817,
"ci95": 1.702
},
"cpu.mountain.p50_ms": {
"n": 3,
"mean": 1.025,
"stdev": 0.315,
"min": 0.765,
"max": 1.376,
"ci95": 0.783
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2051.333,
"stdev": 147.151,
"min": 1884.176,
"max": 2161.316,
"ci95": 365.573
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1810.92,
"stdev": 94.623,
"min": 1707.342,
"max": 1892.828,
"ci95": 235.075
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 6301,
"seconds": 12.004,
"avg_fps": 524.913,
"low_1pct_fps": 9.024,
"low_01pct_fps": 1.125,
"p50_ms": 0.555,
"p95_ms": 2.072,
"p99_ms": 4.067,
"p999_ms": 282.455,
"max_ms": 3525.74,
"cpu_p50_ms": 0.531,
"gpu_p50_ms": 0.218,
"histogram": [
4878,
1060,
217,
86,
19,
15,
6,
2,
5,
0,
1,
2,
0,
0,
0,
0,
0,
0,
0,
2,
0,
4,
3,
1
]
},
{
"phase": "village",
"repeat": 1,
"frames": 9869,
"seconds": 12.001,
"avg_fps": 822.353,
"low_1pct_fps": 138.293,
"low_01pct_fps": 72.479,
"p50_ms": 0.976,
"p95_ms": 2.871,
"p99_ms": 5.065,
"p999_ms": 9.942,
"max_ms": 23.337,
"cpu_p50_ms": 0.925,
"gpu_p50_ms": 0.245,
"histogram": [
5262,
3404,
771,
248,
81,
49,
23,
11,
12,
3,
0,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 14570,
"seconds": 12.001,
"avg_fps": 1214.059,
"low_1pct_fps": 243.388,
"low_01pct_fps": 138.345,
"p50_ms": 0.655,
"p95_ms": 1.486,
"p99_ms": 2.883,
"p999_ms": 5.366,
"max_ms": 26.145,
"cpu_p50_ms": 0.62,
"gpu_p50_ms": 0.239,
"histogram": [
11502,
2683,
257,
77,
25,
23,
1,
1,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 4595,
"seconds": 11.999,
"avg_fps": 382.944,
"low_1pct_fps": 54.933,
"low_01pct_fps": 12.974,
"p50_ms": 2.269,
"p95_ms": 5.193,
"p99_ms": 8.719,
"p999_ms": 21.933,
"max_ms": 243.247,
"cpu_p50_ms": 1.826,
"gpu_p50_ms": 0.502,
"histogram": [
273,
1654,
1395,
836,
172,
122,
52,
38,
26,
10,
7,
5,
0,
2,
1,
0,
0,
0,
1,
0,
1,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 13964,
"seconds": 12.001,
"avg_fps": 1163.61,
"low_1pct_fps": 244.255,
"low_01pct_fps": 152.87,
"p50_ms": 0.682,
"p95_ms": 1.524,
"p99_ms": 2.877,
"p999_ms": 5.801,
"max_ms": 7.977,
"cpu_p50_ms": 0.57,
"gpu_p50_ms": 0.467,
"histogram": [
11067,
2532,
235,
82,
23,
16,
6,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 13777,
"seconds": 12.0,
"avg_fps": 1148.071,
"low_1pct_fps": 236.351,
"low_01pct_fps": 144.264,
"p50_ms": 0.691,
"p95_ms": 1.637,
"p99_ms": 3.131,
"p999_ms": 5.794,
"max_ms": 9.301,
"cpu_p50_ms": 0.57,
"gpu_p50_ms": 0.465,
"histogram": [
10826,
2511,
284,
106,
23,
16,
5,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 1224,
"seconds": 11.322,
"avg_fps": 108.108,
"low_1pct_fps": 1.252,
"low_01pct_fps": 0.202,
"p50_ms": 0.439,
"p95_ms": 3.015,
"p99_ms": 7.359,
"p999_ms": 2880.23,
"max_ms": 7005.436,
"cpu_p50_ms": 0.303,
"gpu_p50_ms": 0.258,
"histogram": [
1081,
31,
49,
10,
3,
7,
23,
12,
4,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
2
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 15695,
"seconds": 12.0,
"avg_fps": 1307.912,
"low_1pct_fps": 292.881,
"low_01pct_fps": 132.042,
"p50_ms": 0.624,
"p95_ms": 1.219,
"p99_ms": 2.173,
"p999_ms": 4.841,
"max_ms": 16.764,
"cpu_p50_ms": 0.551,
"gpu_p50_ms": 0.401,
"histogram": [
13558,
1932,
140,
37,
14,
6,
3,
1,
1,
1,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 9476,
"seconds": 12.0,
"avg_fps": 789.641,
"low_1pct_fps": 109.83,
"low_01pct_fps": 53.941,
"p50_ms": 0.802,
"p95_ms": 3.406,
"p99_ms": 5.844,
"p999_ms": 14.977,
"max_ms": 22.481,
"cpu_p50_ms": 0.686,
"gpu_p50_ms": 0.403,
"histogram": [
5819,
2151,
787,
455,
112,
65,
36,
18,
7,
7,
7,
5,
4,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 5352,
"seconds": 11.878,
"avg_fps": 450.565,
"low_1pct_fps": 24.389,
"low_01pct_fps": 3.577,
"p50_ms": 1.431,
"p95_ms": 4.548,
"p99_ms": 7.601,
"p999_ms": 96.086,
"max_ms": 714.027,
"cpu_p50_ms": 1.376,
"gpu_p50_ms": 0.398,
"histogram": [
1412,
2323,
811,
460,
138,
84,
49,
34,
13,
9,
8,
2,
1,
1,
0,
1,
0,
0,
1,
2,
1,
1,
1,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 8789,
"seconds": 12.001,
"avg_fps": 732.365,
"low_1pct_fps": 136.06,
"low_01pct_fps": 79.749,
"p50_ms": 0.972,
"p95_ms": 3.329,
"p99_ms": 5.68,
"p999_ms": 9.7,
"max_ms": 18.289,
"cpu_p50_ms": 0.935,
"gpu_p50_ms": 0.383,
"histogram": [
4529,
3007,
715,
287,
120,
61,
37,
16,
12,
1,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 11461,
"seconds": 12.001,
"avg_fps": 955.041,
"low_1pct_fps": 193.34,
"low_01pct_fps": 133.895,
"p50_ms": 0.787,
"p95_ms": 2.026,
"p99_ms": 3.953,
"p999_ms": 6.511,
"max_ms": 9.761,
"cpu_p50_ms": 0.765,
"gpu_p50_ms": 0.379,
"histogram": [
7754,
3114,
328,
165,
46,
33,
14,
5,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-desktop"
}
@@ -1,758 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T22:55:52Z",
"app": {
"name": "horizon-client",
"version": "0.1.0",
"commit": "1bd24e4042"
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "",
"driver": "",
"backend": "metal",
"kind": "integrated"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 113.978,
"window": [
1920,
1080
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 1524.333,
"stdev": 364.142,
"min": 1112.127,
"max": 1802.295,
"ci95": 904.651
},
"startup.world_ms": {
"n": 3,
"mean": 14001.774,
"stdev": 8119.623,
"min": 7466.904,
"max": 23091.569,
"ci95": 20171.889
},
"chunks.village.load_ms": {
"n": 3,
"mean": 8159.696,
"stdev": 3897.327,
"min": 5313.528,
"max": 12601.663,
"ci95": 9682.279
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 25431.333,
"stdev": 1389.728,
"min": 24149.0,
"max": 26908.0,
"ci95": 3452.555
},
"fps.village.avg_fps": {
"n": 3,
"mean": 568.391,
"stdev": 11.948,
"min": 558.517,
"max": 581.673,
"ci95": 29.684
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 270.644,
"stdev": 9.955,
"min": 259.156,
"max": 276.728,
"ci95": 24.732
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 209.951,
"stdev": 30.24,
"min": 191.819,
"max": 244.86,
"ci95": 75.126
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8879.707,
"stdev": 1758.259,
"min": 7205.686,
"max": 10711.562,
"ci95": 4368.11
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 29366.667,
"stdev": 273.35,
"min": 29053.0,
"max": 29554.0,
"ci95": 679.094
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 333.026,
"stdev": 1.469,
"min": 331.478,
"max": 334.4,
"ci95": 3.649
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 193.014,
"stdev": 11.504,
"min": 185.57,
"max": 206.264,
"ci95": 28.58
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 137.067,
"stdev": 28.701,
"min": 108.337,
"max": 165.738,
"ci95": 71.302
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 6399.435,
"stdev": 2268.592,
"min": 4346.111,
"max": 8834.786,
"ci95": 5635.949
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 25436.0,
"stdev": 813.154,
"min": 24718.0,
"max": 26319.0,
"ci95": 2020.149
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 411.986,
"stdev": 1.42,
"min": 410.981,
"max": 413.61,
"ci95": 3.527
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 209.307,
"stdev": 10.427,
"min": 197.391,
"max": 216.759,
"ci95": 25.905
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 126.88,
"stdev": 17.642,
"min": 106.534,
"max": 137.93,
"ci95": 43.829
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 14672.976,
"stdev": 9832.9,
"min": 8230.723,
"max": 25990.951,
"ci95": 24428.25
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 27715.0,
"stdev": 1706.033,
"min": 26572.0,
"max": 29676.0,
"ci95": 4238.362
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 357.149,
"stdev": 2.391,
"min": 355.009,
"max": 359.73,
"ci95": 5.941
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 199.658,
"stdev": 12.172,
"min": 187.504,
"max": 211.847,
"ci95": 30.238
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 154.465,
"stdev": 13.484,
"min": 144.923,
"max": 169.891,
"ci95": 33.498
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1892.0,
"stdev": 49.025,
"min": 1835.406,
"max": 1921.438,
"ci95": 121.794
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1616.755,
"stdev": 47.256,
"min": 1564.734,
"max": 1657.031,
"ci95": 117.4
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 3083.815,
"stdev": 25.002,
"min": 3062.456,
"max": 3111.315,
"ci95": 62.113
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 2495.271,
"stdev": 36.569,
"min": 2453.284,
"max": 2520.151,
"ci95": 90.85
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 6981,
"seconds": 12.002,
"avg_fps": 581.673,
"low_1pct_fps": 276.049,
"low_01pct_fps": 191.819,
"p50_ms": 1.655,
"p95_ms": 2.267,
"p99_ms": 3.277,
"p999_ms": 4.174,
"max_ms": 7.418,
"cpu_p50_ms": 0.955,
"gpu_p50_ms": 1.646,
"histogram": [
0,
6445,
353,
176,
3,
3,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 6703,
"seconds": 12.001,
"avg_fps": 558.517,
"low_1pct_fps": 276.728,
"low_01pct_fps": 244.86,
"p50_ms": 1.725,
"p95_ms": 2.341,
"p99_ms": 3.396,
"p999_ms": 3.939,
"max_ms": 4.185,
"cpu_p50_ms": 0.767,
"gpu_p50_ms": 1.708,
"histogram": [
17,
6041,
442,
202,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 6780,
"seconds": 12.0,
"avg_fps": 564.982,
"low_1pct_fps": 259.156,
"low_01pct_fps": 193.174,
"p50_ms": 1.707,
"p95_ms": 2.311,
"p99_ms": 3.407,
"p999_ms": 4.984,
"max_ms": 5.322,
"cpu_p50_ms": 0.923,
"gpu_p50_ms": 1.698,
"histogram": [
5,
6118,
480,
163,
8,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 3978,
"seconds": 12.001,
"avg_fps": 331.478,
"low_1pct_fps": 187.208,
"low_01pct_fps": 137.127,
"p50_ms": 2.93,
"p95_ms": 3.969,
"p99_ms": 4.719,
"p999_ms": 6.451,
"max_ms": 8.835,
"cpu_p50_ms": 0.857,
"gpu_p50_ms": 2.92,
"histogram": [
0,
29,
2279,
1541,
109,
14,
4,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 3999,
"seconds": 12.002,
"avg_fps": 333.199,
"low_1pct_fps": 185.57,
"low_01pct_fps": 108.337,
"p50_ms": 2.921,
"p95_ms": 3.862,
"p99_ms": 4.679,
"p999_ms": 6.817,
"max_ms": 11.683,
"cpu_p50_ms": 0.828,
"gpu_p50_ms": 2.903,
"histogram": [
3,
16,
2356,
1511,
97,
12,
1,
0,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 4013,
"seconds": 12.001,
"avg_fps": 334.4,
"low_1pct_fps": 206.264,
"low_01pct_fps": 165.738,
"p50_ms": 2.915,
"p95_ms": 3.809,
"p99_ms": 4.525,
"p999_ms": 5.078,
"max_ms": 8.228,
"cpu_p50_ms": 0.861,
"gpu_p50_ms": 2.899,
"histogram": [
1,
18,
2357,
1553,
77,
5,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 4932,
"seconds": 12.001,
"avg_fps": 410.981,
"low_1pct_fps": 216.759,
"low_01pct_fps": 137.93,
"p50_ms": 2.353,
"p95_ms": 3.181,
"p99_ms": 4.01,
"p999_ms": 5.767,
"max_ms": 9.713,
"cpu_p50_ms": 0.888,
"gpu_p50_ms": 2.343,
"histogram": [
0,
163,
4474,
264,
23,
4,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 4937,
"seconds": 12.001,
"avg_fps": 411.366,
"low_1pct_fps": 213.772,
"low_01pct_fps": 136.177,
"p50_ms": 2.349,
"p95_ms": 3.258,
"p99_ms": 4.015,
"p999_ms": 5.985,
"max_ms": 8.539,
"cpu_p50_ms": 0.871,
"gpu_p50_ms": 2.339,
"histogram": [
0,
202,
4406,
295,
25,
5,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 4963,
"seconds": 11.999,
"avg_fps": 413.61,
"low_1pct_fps": 197.391,
"low_01pct_fps": 106.534,
"p50_ms": 2.335,
"p95_ms": 3.214,
"p99_ms": 4.111,
"p999_ms": 7.65,
"max_ms": 11.399,
"cpu_p50_ms": 0.878,
"gpu_p50_ms": 2.323,
"histogram": [
2,
228,
4417,
274,
28,
7,
2,
2,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 4317,
"seconds": 12.001,
"avg_fps": 359.73,
"low_1pct_fps": 211.847,
"low_01pct_fps": 169.891,
"p50_ms": 2.7,
"p95_ms": 3.753,
"p99_ms": 4.366,
"p999_ms": 5.041,
"max_ms": 8.726,
"cpu_p50_ms": 1.113,
"gpu_p50_ms": 2.687,
"histogram": [
0,
68,
3445,
718,
80,
5,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 4281,
"seconds": 12.001,
"avg_fps": 356.707,
"low_1pct_fps": 199.624,
"low_01pct_fps": 144.923,
"p50_ms": 2.712,
"p95_ms": 3.806,
"p99_ms": 4.535,
"p999_ms": 5.453,
"max_ms": 8.354,
"cpu_p50_ms": 1.111,
"gpu_p50_ms": 2.694,
"histogram": [
0,
64,
3353,
717,
138,
5,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 4261,
"seconds": 12.003,
"avg_fps": 355.009,
"low_1pct_fps": 187.504,
"low_01pct_fps": 148.582,
"p50_ms": 2.727,
"p95_ms": 4.083,
"p99_ms": 4.744,
"p999_ms": 6.091,
"max_ms": 7.442,
"cpu_p50_ms": 1.126,
"gpu_p50_ms": 2.711,
"histogram": [
0,
155,
3198,
719,
165,
17,
6,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-m4pro-1080p"
}
@@ -1,758 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T23:17:23Z",
"app": {
"name": "horizon-client",
"version": "0.1.0",
"commit": "1bd24e4042"
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "",
"driver": "",
"backend": "metal",
"kind": "integrated"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 28.236,
"window": [
1920,
1080
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 2190.186,
"stdev": 1780.667,
"min": 906.481,
"max": 4223.028,
"ci95": 4423.779
},
"startup.world_ms": {
"n": 3,
"mean": 14254.027,
"stdev": 4604.903,
"min": 10130.899,
"max": 19223.339,
"ci95": 11440.138
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6509.117,
"stdev": 1616.283,
"min": 5457.214,
"max": 8370.17,
"ci95": 4015.394
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 48522.333,
"stdev": 1713.943,
"min": 46732.0,
"max": 50148.0,
"ci95": 4258.014
},
"fps.village.avg_fps": {
"n": 3,
"mean": 326.473,
"stdev": 6.811,
"min": 319.588,
"max": 333.208,
"ci95": 16.921
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 152.497,
"stdev": 34.142,
"min": 116.575,
"max": 184.525,
"ci95": 84.82
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 122.567,
"stdev": 33.914,
"min": 87.235,
"max": 154.858,
"ci95": 84.254
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 10363.339,
"stdev": 3122.178,
"min": 6760.012,
"max": 12265.121,
"ci95": 7756.547
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 82591.667,
"stdev": 1489.69,
"min": 81053.0,
"max": 84027.0,
"ci95": 3700.895
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 165.414,
"stdev": 4.368,
"min": 162.757,
"max": 170.455,
"ci95": 10.851
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 103.992,
"stdev": 20.41,
"min": 83.527,
"max": 124.346,
"ci95": 50.705
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 94.785,
"stdev": 24.344,
"min": 69.796,
"max": 118.428,
"ci95": 60.478
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 10450.642,
"stdev": 2599.832,
"min": 7510.454,
"max": 12445.71,
"ci95": 6458.863
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 76405.667,
"stdev": 649.936,
"min": 75681.0,
"max": 76937.0,
"ci95": 1614.662
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 210.179,
"stdev": 5.279,
"min": 205.559,
"max": 215.932,
"ci95": 13.114
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 128.777,
"stdev": 30.982,
"min": 93.017,
"max": 147.571,
"ci95": 76.97
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 115.916,
"stdev": 39.398,
"min": 70.443,
"max": 139.838,
"ci95": 97.879
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 17168.619,
"stdev": 3613.606,
"min": 13749.902,
"max": 20949.796,
"ci95": 8977.42
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 91622.667,
"stdev": 482.189,
"min": 91089.0,
"max": 92027.0,
"ci95": 1197.921
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 190.088,
"stdev": 2.175,
"min": 187.78,
"max": 192.099,
"ci95": 5.403
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 108.92,
"stdev": 14.244,
"min": 92.641,
"max": 119.094,
"ci95": 35.388
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 91.132,
"stdev": 12.82,
"min": 76.343,
"max": 99.09,
"ci95": 31.849
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2536.536,
"stdev": 40.31,
"min": 2506.281,
"max": 2582.297,
"ci95": 100.144
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2195.922,
"stdev": 37.757,
"min": 2167.078,
"max": 2238.656,
"ci95": 93.8
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 5559.837,
"stdev": 47.18,
"min": 5505.457,
"max": 5589.863,
"ci95": 117.211
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 4373.034,
"stdev": 82.743,
"min": 4311.909,
"max": 4467.191,
"ci95": 205.561
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 3920,
"seconds": 12.002,
"avg_fps": 326.623,
"low_1pct_fps": 116.575,
"low_01pct_fps": 87.235,
"p50_ms": 2.938,
"p95_ms": 4.356,
"p99_ms": 7.397,
"p999_ms": 9.497,
"max_ms": 13.274,
"cpu_p50_ms": 1.611,
"gpu_p50_ms": 2.871,
"histogram": [
0,
115,
2297,
1271,
149,
33,
10,
24,
18,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 3835,
"seconds": 12.0,
"avg_fps": 319.588,
"low_1pct_fps": 156.39,
"low_01pct_fps": 125.608,
"p50_ms": 3.04,
"p95_ms": 4.322,
"p99_ms": 5.265,
"p999_ms": 7.732,
"max_ms": 8.183,
"cpu_p50_ms": 1.733,
"gpu_p50_ms": 2.936,
"histogram": [
0,
1,
1610,
2006,
142,
51,
15,
10,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 3999,
"seconds": 12.002,
"avg_fps": 333.208,
"low_1pct_fps": 184.525,
"low_01pct_fps": 154.858,
"p50_ms": 2.9,
"p95_ms": 4.01,
"p99_ms": 4.955,
"p999_ms": 6.193,
"max_ms": 6.805,
"cpu_p50_ms": 1.51,
"gpu_p50_ms": 2.788,
"histogram": [
0,
8,
2499,
1328,
132,
25,
7,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 1957,
"seconds": 12.004,
"avg_fps": 163.03,
"low_1pct_fps": 83.527,
"low_01pct_fps": 69.796,
"p50_ms": 6.011,
"p95_ms": 8.032,
"p99_ms": 10.903,
"p999_ms": 14.322,
"max_ms": 14.333,
"cpu_p50_ms": 1.555,
"gpu_p50_ms": 5.857,
"histogram": [
0,
5,
26,
16,
36,
883,
739,
194,
29,
22,
5,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 1954,
"seconds": 12.006,
"avg_fps": 162.757,
"low_1pct_fps": 104.102,
"low_01pct_fps": 96.13,
"p50_ms": 5.98,
"p95_ms": 7.665,
"p99_ms": 9.049,
"p999_ms": 10.323,
"max_ms": 10.482,
"cpu_p50_ms": 1.584,
"gpu_p50_ms": 5.715,
"histogram": [
0,
0,
0,
1,
9,
1028,
737,
130,
46,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 2046,
"seconds": 12.003,
"avg_fps": 170.455,
"low_1pct_fps": 124.346,
"low_01pct_fps": 118.428,
"p50_ms": 5.719,
"p95_ms": 7.342,
"p99_ms": 7.815,
"p999_ms": 8.38,
"max_ms": 8.506,
"cpu_p50_ms": 1.384,
"gpu_p50_ms": 5.572,
"histogram": [
0,
0,
0,
0,
26,
1452,
396,
169,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 2467,
"seconds": 12.001,
"avg_fps": 205.559,
"low_1pct_fps": 93.017,
"low_01pct_fps": 70.443,
"p50_ms": 4.708,
"p95_ms": 6.6,
"p99_ms": 9.432,
"p999_ms": 13.566,
"max_ms": 15.228,
"cpu_p50_ms": 1.664,
"gpu_p50_ms": 4.483,
"histogram": [
0,
18,
55,
87,
1811,
290,
143,
27,
20,
13,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 2509,
"seconds": 12.002,
"avg_fps": 209.045,
"low_1pct_fps": 145.742,
"low_01pct_fps": 139.838,
"p50_ms": 4.642,
"p95_ms": 6.15,
"p99_ms": 6.72,
"p999_ms": 7.081,
"max_ms": 7.254,
"cpu_p50_ms": 1.74,
"gpu_p50_ms": 4.376,
"histogram": [
0,
0,
0,
78,
2008,
277,
142,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 2591,
"seconds": 11.999,
"avg_fps": 215.932,
"low_1pct_fps": 147.571,
"low_01pct_fps": 137.466,
"p50_ms": 4.466,
"p95_ms": 5.947,
"p99_ms": 6.512,
"p999_ms": 7.262,
"max_ms": 7.283,
"cpu_p50_ms": 1.422,
"gpu_p50_ms": 4.286,
"histogram": [
0,
2,
0,
111,
2114,
248,
111,
5,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 2285,
"seconds": 12.002,
"avg_fps": 190.386,
"low_1pct_fps": 92.641,
"low_01pct_fps": 76.343,
"p50_ms": 5.038,
"p95_ms": 7.005,
"p99_ms": 9.383,
"p999_ms": 11.731,
"max_ms": 15.266,
"cpu_p50_ms": 2.239,
"gpu_p50_ms": 4.915,
"histogram": [
0,
0,
54,
45,
916,
985,
168,
78,
22,
15,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 2306,
"seconds": 12.004,
"avg_fps": 192.099,
"low_1pct_fps": 119.094,
"low_01pct_fps": 99.09,
"p50_ms": 5.017,
"p95_ms": 6.58,
"p99_ms": 7.586,
"p999_ms": 9.195,
"max_ms": 11.428,
"cpu_p50_ms": 2.261,
"gpu_p50_ms": 4.823,
"histogram": [
0,
0,
3,
12,
1070,
1003,
165,
43,
9,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 2254,
"seconds": 12.003,
"avg_fps": 187.78,
"low_1pct_fps": 115.026,
"low_01pct_fps": 97.962,
"p50_ms": 5.175,
"p95_ms": 6.791,
"p99_ms": 7.916,
"p999_ms": 9.284,
"max_ms": 11.986,
"cpu_p50_ms": 2.158,
"gpu_p50_ms": 4.942,
"histogram": [
0,
0,
0,
17,
774,
1199,
182,
69,
12,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-m4pro-1080p"
}
@@ -1,758 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T22:55:52Z",
"app": {
"name": "horizon-client",
"version": "0.1.0",
"commit": "1bd24e4042"
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "",
"driver": "",
"backend": "metal",
"kind": "integrated"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 73.292,
"window": [
1920,
1080
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7204.239,
"stdev": 1586.64,
"min": 5409.683,
"max": 8421.051,
"ci95": 3941.749
},
"startup.world_ms": {
"n": 3,
"mean": 11407.51,
"stdev": 2085.692,
"min": 9027.004,
"max": 12913.993,
"ci95": 5181.563
},
"chunks.village.load_ms": {
"n": 3,
"mean": 5116.146,
"stdev": 744.611,
"min": 4548.096,
"max": 5959.13,
"ci95": 1849.865
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 7220.0,
"stdev": 137.292,
"min": 7092.0,
"max": 7365.0,
"ci95": 341.079
},
"fps.village.avg_fps": {
"n": 3,
"mean": 540.213,
"stdev": 182.263,
"min": 432.911,
"max": 750.659,
"ci95": 452.803
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 195.561,
"stdev": 75.888,
"min": 141.859,
"max": 282.38,
"ci95": 188.533
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 134.559,
"stdev": 74.125,
"min": 87.663,
"max": 220.016,
"ci95": 184.153
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 7185.67,
"stdev": 4093.192,
"min": 4323.79,
"max": 11874.133,
"ci95": 10168.874
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 8302.667,
"stdev": 538.894,
"min": 7853.0,
"max": 8900.0,
"ci95": 1338.794
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 364.068,
"stdev": 8.947,
"min": 355.173,
"max": 373.067,
"ci95": 22.229
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 145.064,
"stdev": 21.824,
"min": 121.995,
"max": 165.382,
"ci95": 54.218
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 97.299,
"stdev": 15.364,
"min": 81.075,
"max": 111.627,
"ci95": 38.17
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 4944.497,
"stdev": 1598.757,
"min": 3816.976,
"max": 6774.175,
"ci95": 3971.854
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 7041.0,
"stdev": 276.949,
"min": 6821.0,
"max": 7352.0,
"ci95": 688.036
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 410.281,
"stdev": 35.314,
"min": 370.881,
"max": 439.08,
"ci95": 87.731
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 161.057,
"stdev": 15.769,
"min": 144.003,
"max": 175.109,
"ci95": 39.175
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 111.852,
"stdev": 16.415,
"min": 93.045,
"max": 123.299,
"ci95": 40.78
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 8844.497,
"stdev": 4291.678,
"min": 5269.587,
"max": 13604.075,
"ci95": 10661.98
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 7381.0,
"stdev": 88.606,
"min": 7290.0,
"max": 7467.0,
"ci95": 220.127
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 466.294,
"stdev": 2.738,
"min": 464.707,
"max": 469.455,
"ci95": 6.801
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 164.71,
"stdev": 21.73,
"min": 141.437,
"max": 184.468,
"ci95": 53.984
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 90.643,
"stdev": 14.864,
"min": 76.851,
"max": 106.387,
"ci95": 36.928
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1536.969,
"stdev": 19.205,
"min": 1516.188,
"max": 1554.063,
"ci95": 47.711
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1379.422,
"stdev": 9.712,
"min": 1371.438,
"max": 1390.234,
"ci95": 24.129
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 2093.701,
"stdev": 13.714,
"min": 2078.862,
"max": 2105.909,
"ci95": 34.07
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 1883.495,
"stdev": 10.346,
"min": 1871.987,
"max": 1892.026,
"ci95": 25.702
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 9008,
"seconds": 12.0,
"avg_fps": 750.659,
"low_1pct_fps": 282.38,
"low_01pct_fps": 220.016,
"p50_ms": 1.047,
"p95_ms": 2.169,
"p99_ms": 3.163,
"p999_ms": 4.077,
"max_ms": 5.462,
"cpu_p50_ms": 0.447,
"gpu_p50_ms": 1.021,
"histogram": [
3979,
3877,
1033,
112,
6,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 5245,
"seconds": 12.0,
"avg_fps": 437.07,
"low_1pct_fps": 141.859,
"low_01pct_fps": 87.663,
"p50_ms": 1.822,
"p95_ms": 4.165,
"p99_ms": 5.399,
"p999_ms": 10.052,
"max_ms": 12.712,
"cpu_p50_ms": 0.652,
"gpu_p50_ms": 1.943,
"histogram": [
831,
1968,
606,
1582,
154,
82,
3,
3,
10,
4,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 5195,
"seconds": 12.0,
"avg_fps": 432.911,
"low_1pct_fps": 162.445,
"low_01pct_fps": 95.997,
"p50_ms": 1.806,
"p95_ms": 4.202,
"p99_ms": 5.259,
"p999_ms": 7.591,
"max_ms": 17.328,
"cpu_p50_ms": 0.636,
"gpu_p50_ms": 1.876,
"histogram": [
297,
2502,
650,
1469,
192,
73,
6,
1,
3,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 4262,
"seconds": 12.0,
"avg_fps": 355.173,
"low_1pct_fps": 165.382,
"low_01pct_fps": 111.627,
"p50_ms": 2.917,
"p95_ms": 3.854,
"p99_ms": 5.022,
"p999_ms": 7.808,
"max_ms": 9.769,
"cpu_p50_ms": 0.59,
"gpu_p50_ms": 2.91,
"histogram": [
98,
416,
2506,
1080,
118,
29,
10,
1,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 4477,
"seconds": 12.001,
"avg_fps": 373.067,
"low_1pct_fps": 121.995,
"low_01pct_fps": 81.075,
"p50_ms": 2.243,
"p95_ms": 4.687,
"p99_ms": 5.835,
"p999_ms": 11.831,
"max_ms": 13.367,
"cpu_p50_ms": 0.541,
"gpu_p50_ms": 2.346,
"histogram": [
183,
1975,
392,
887,
903,
99,
12,
6,
11,
6,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 4368,
"seconds": 12.001,
"avg_fps": 363.964,
"low_1pct_fps": 147.816,
"low_01pct_fps": 99.196,
"p50_ms": 2.264,
"p95_ms": 4.763,
"p99_ms": 5.86,
"p999_ms": 7.641,
"max_ms": 11.427,
"cpu_p50_ms": 0.562,
"gpu_p50_ms": 2.203,
"histogram": [
73,
1978,
472,
695,
1004,
104,
35,
3,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 4451,
"seconds": 12.001,
"avg_fps": 370.881,
"low_1pct_fps": 175.109,
"low_01pct_fps": 119.212,
"p50_ms": 2.858,
"p95_ms": 4.279,
"p99_ms": 5.045,
"p999_ms": 6.057,
"max_ms": 10.866,
"cpu_p50_ms": 0.609,
"gpu_p50_ms": 2.922,
"histogram": [
193,
1245,
964,
1732,
270,
42,
2,
0,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 5269,
"seconds": 12.0,
"avg_fps": 439.08,
"low_1pct_fps": 144.003,
"low_01pct_fps": 93.045,
"p50_ms": 1.767,
"p95_ms": 4.188,
"p99_ms": 5.191,
"p999_ms": 10.375,
"max_ms": 11.271,
"cpu_p50_ms": 0.545,
"gpu_p50_ms": 1.815,
"histogram": [
1239,
1550,
644,
1555,
201,
59,
1,
4,
10,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 5051,
"seconds": 12.001,
"avg_fps": 420.881,
"low_1pct_fps": 164.058,
"low_01pct_fps": 123.299,
"p50_ms": 1.872,
"p95_ms": 4.455,
"p99_ms": 5.38,
"p999_ms": 6.923,
"max_ms": 11.189,
"cpu_p50_ms": 0.548,
"gpu_p50_ms": 1.874,
"histogram": [
519,
2114,
673,
1086,
577,
65,
13,
3,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 5635,
"seconds": 12.003,
"avg_fps": 469.455,
"low_1pct_fps": 184.468,
"low_01pct_fps": 106.387,
"p50_ms": 1.626,
"p95_ms": 3.874,
"p99_ms": 4.614,
"p999_ms": 5.913,
"max_ms": 18.185,
"cpu_p50_ms": 0.564,
"gpu_p50_ms": 1.604,
"histogram": [
1491,
1599,
704,
1688,
128,
20,
1,
2,
0,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 5578,
"seconds": 12.003,
"avg_fps": 464.72,
"low_1pct_fps": 141.437,
"low_01pct_fps": 76.851,
"p50_ms": 1.587,
"p95_ms": 4.127,
"p99_ms": 5.085,
"p999_ms": 10.138,
"max_ms": 21.644,
"cpu_p50_ms": 0.588,
"gpu_p50_ms": 1.625,
"histogram": [
1321,
1798,
770,
1453,
173,
41,
2,
5,
7,
6,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 5576,
"seconds": 11.999,
"avg_fps": 464.707,
"low_1pct_fps": 168.226,
"low_01pct_fps": 88.692,
"p50_ms": 1.627,
"p95_ms": 4.154,
"p99_ms": 4.908,
"p999_ms": 9.555,
"max_ms": 14.784,
"cpu_p50_ms": 0.595,
"gpu_p50_ms": 1.64,
"histogram": [
1054,
2049,
837,
1377,
217,
32,
3,
0,
2,
4,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-m4pro-1080p"
}
-854
View File
@@ -1,854 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-04T12:44:40Z",
"app": {
"name": "horizon-client",
"version": "nightly-2026-11-04-67dad0d",
"commit": "67dad0d"
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "",
"driver": "",
"backend": "metal",
"kind": "integrated"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 12,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 17.39,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 1516.155,
"stdev": 34.227,
"min": 1476.782,
"max": 1538.813,
"ci95": 85.032
},
"startup.world_ms": {
"n": 3,
"mean": 4080.9,
"stdev": 748.712,
"min": 3273.516,
"max": 4752.301,
"ci95": 1860.054
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3363.749,
"stdev": 224.552,
"min": 3105.622,
"max": 3514.059,
"ci95": 557.864
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 15473.667,
"stdev": 339.602,
"min": 15123.0,
"max": 15801.0,
"ci95": 843.686
},
"fps.village.avg_fps": {
"n": 3,
"mean": 999.753,
"stdev": 41.901,
"min": 970.527,
"max": 1047.758,
"ci95": 104.095
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 336.821,
"stdev": 17.834,
"min": 318.068,
"max": 353.566,
"ci95": 44.306
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 263.782,
"stdev": 31.556,
"min": 227.767,
"max": 286.582,
"ci95": 78.395
},
"memory.village.rss_mb": {
"n": 3,
"mean": 1147.654,
"stdev": 31.82,
"min": 1116.875,
"max": 1180.422,
"ci95": 79.052
},
"tick.village.client_ms": {
"n": 3,
"mean": 0.414,
"stdev": 0.036,
"min": 0.373,
"max": 0.44,
"ci95": 0.089
},
"cpu.village.p50_ms": {
"n": 3,
"mean": 0.52,
"stdev": 0.071,
"min": 0.44,
"max": 0.577,
"ci95": 0.177
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 3685.368,
"stdev": 844.43,
"min": 2932.117,
"max": 4598.204,
"ci95": 2097.85
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 16795.333,
"stdev": 437.802,
"min": 16325.0,
"max": 17191.0,
"ci95": 1087.648
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 592.615,
"stdev": 20.339,
"min": 580.401,
"max": 616.094,
"ci95": 50.529
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 250.069,
"stdev": 31.227,
"min": 217.065,
"max": 279.149,
"ci95": 77.58
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 151.684,
"stdev": 77.064,
"min": 64.033,
"max": 208.811,
"ci95": 191.454
},
"memory.forest.rss_mb": {
"n": 3,
"mean": 1064.344,
"stdev": 148.185,
"min": 901.203,
"max": 1190.609,
"ci95": 368.142
},
"tick.forest.client_ms": {
"n": 3,
"mean": 0.424,
"stdev": 0.003,
"min": 0.422,
"max": 0.427,
"ci95": 0.007
},
"cpu.forest.p50_ms": {
"n": 3,
"mean": 0.617,
"stdev": 0.016,
"min": 0.599,
"max": 0.629,
"ci95": 0.039
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 3594.311,
"stdev": 1381.026,
"min": 2011.153,
"max": 4551.541,
"ci95": 3430.936
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 14527.0,
"stdev": 319.629,
"min": 14166.0,
"max": 14774.0,
"ci95": 794.068
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 789.003,
"stdev": 25.076,
"min": 766.83,
"max": 816.216,
"ci95": 62.297
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 276.887,
"stdev": 51.785,
"min": 217.373,
"max": 311.665,
"ci95": 128.651
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 169.278,
"stdev": 29.119,
"min": 137.156,
"max": 193.944,
"ci95": 72.341
},
"memory.ocean.rss_mb": {
"n": 3,
"mean": 982.995,
"stdev": 287.819,
"min": 651.109,
"max": 1164.063,
"ci95": 715.039
},
"tick.ocean.client_ms": {
"n": 3,
"mean": 0.397,
"stdev": 0.028,
"min": 0.38,
"max": 0.429,
"ci95": 0.069
},
"cpu.ocean.p50_ms": {
"n": 3,
"mean": 0.569,
"stdev": 0.045,
"min": 0.533,
"max": 0.62,
"ci95": 0.112
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 5682.993,
"stdev": 1272.063,
"min": 4476.873,
"max": 7012.069,
"ci95": 3160.234
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 15640.0,
"stdev": 204.105,
"min": 15507.0,
"max": 15875.0,
"ci95": 507.067
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 716.475,
"stdev": 8.139,
"min": 707.613,
"max": 723.617,
"ci95": 20.221
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 313.736,
"stdev": 3.759,
"min": 311.007,
"max": 318.023,
"ci95": 9.338
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 270.042,
"stdev": 19.572,
"min": 247.802,
"max": 284.642,
"ci95": 48.624
},
"memory.mountain.rss_mb": {
"n": 3,
"mean": 997.474,
"stdev": 231.733,
"min": 790.266,
"max": 1247.703,
"ci95": 575.704
},
"tick.mountain.client_ms": {
"n": 3,
"mean": 0.385,
"stdev": 0.023,
"min": 0.359,
"max": 0.401,
"ci95": 0.057
},
"cpu.mountain.p50_ms": {
"n": 3,
"mean": 0.609,
"stdev": 0.044,
"min": 0.56,
"max": 0.645,
"ci95": 0.109
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1213.255,
"stdev": 81.897,
"min": 1136.078,
"max": 1299.172,
"ci95": 203.461
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1034.279,
"stdev": 188.802,
"min": 816.453,
"max": 1150.961,
"ci95": 469.049
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 2167.747,
"stdev": 14.039,
"min": 2153.721,
"max": 2181.799,
"ci95": 34.878
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 1889.468,
"stdev": 24.444,
"min": 1862.799,
"max": 1910.807,
"ci95": 60.727
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 12573,
"seconds": 12.0,
"avg_fps": 1047.758,
"low_1pct_fps": 353.566,
"low_01pct_fps": 286.582,
"p50_ms": 0.921,
"p95_ms": 1.077,
"p99_ms": 2.675,
"p999_ms": 2.946,
"max_ms": 5.36,
"cpu_p50_ms": 0.44,
"gpu_p50_ms": 0.914,
"histogram": [
11056,
1291,
218,
6,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 11646,
"seconds": 12.0,
"avg_fps": 970.527,
"low_1pct_fps": 318.068,
"low_01pct_fps": 227.767,
"p50_ms": 0.973,
"p95_ms": 1.264,
"p99_ms": 2.805,
"p999_ms": 3.808,
"max_ms": 5.305,
"cpu_p50_ms": 0.577,
"gpu_p50_ms": 0.969,
"histogram": [
7035,
4334,
230,
39,
6,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 11771,
"seconds": 11.999,
"avg_fps": 980.973,
"low_1pct_fps": 338.828,
"low_01pct_fps": 276.996,
"p50_ms": 0.985,
"p95_ms": 1.157,
"p99_ms": 2.779,
"p999_ms": 3.068,
"max_ms": 4.308,
"cpu_p50_ms": 0.542,
"gpu_p50_ms": 0.98,
"histogram": [
6880,
4663,
208,
18,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 7394,
"seconds": 12.001,
"avg_fps": 616.094,
"low_1pct_fps": 253.993,
"low_01pct_fps": 182.207,
"p50_ms": 1.565,
"p95_ms": 1.915,
"p99_ms": 3.329,
"p999_ms": 4.661,
"max_ms": 7.32,
"cpu_p50_ms": 0.599,
"gpu_p50_ms": 1.551,
"histogram": [
82,
6988,
144,
162,
14,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 6965,
"seconds": 12.0,
"avg_fps": 580.401,
"low_1pct_fps": 217.065,
"low_01pct_fps": 64.033,
"p50_ms": 1.654,
"p95_ms": 1.983,
"p99_ms": 3.233,
"p999_ms": 3.893,
"max_ms": 81.863,
"cpu_p50_ms": 0.629,
"gpu_p50_ms": 1.645,
"histogram": [
11,
6622,
153,
174,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 6977,
"seconds": 12.001,
"avg_fps": 581.35,
"low_1pct_fps": 279.149,
"low_01pct_fps": 208.811,
"p50_ms": 1.659,
"p95_ms": 2.006,
"p99_ms": 3.273,
"p999_ms": 3.982,
"max_ms": 5.889,
"cpu_p50_ms": 0.623,
"gpu_p50_ms": 1.653,
"histogram": [
17,
6606,
164,
186,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 9793,
"seconds": 11.998,
"avg_fps": 816.216,
"low_1pct_fps": 301.624,
"low_01pct_fps": 176.734,
"p50_ms": 1.171,
"p95_ms": 1.499,
"p99_ms": 2.928,
"p999_ms": 3.333,
"max_ms": 23.472,
"cpu_p50_ms": 0.533,
"gpu_p50_ms": 1.163,
"histogram": [
259,
9293,
182,
56,
2,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 9202,
"seconds": 12.0,
"avg_fps": 766.83,
"low_1pct_fps": 217.373,
"low_01pct_fps": 137.156,
"p50_ms": 1.226,
"p95_ms": 1.912,
"p99_ms": 3.396,
"p999_ms": 6.003,
"max_ms": 13.562,
"cpu_p50_ms": 0.62,
"gpu_p50_ms": 1.219,
"histogram": [
635,
8130,
261,
124,
27,
15,
7,
2,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 9408,
"seconds": 12.001,
"avg_fps": 783.964,
"low_1pct_fps": 311.665,
"low_01pct_fps": 193.944,
"p50_ms": 1.22,
"p95_ms": 1.541,
"p99_ms": 2.865,
"p999_ms": 3.298,
"max_ms": 13.861,
"cpu_p50_ms": 0.555,
"gpu_p50_ms": 1.214,
"histogram": [
100,
9066,
204,
34,
2,
0,
1,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 8618,
"seconds": 12.0,
"avg_fps": 718.194,
"low_1pct_fps": 312.177,
"low_01pct_fps": 277.681,
"p50_ms": 1.338,
"p95_ms": 1.664,
"p99_ms": 3.068,
"p999_ms": 3.364,
"max_ms": 4.008,
"cpu_p50_ms": 0.622,
"gpu_p50_ms": 1.329,
"histogram": [
62,
8301,
130,
125,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 8492,
"seconds": 12.001,
"avg_fps": 707.613,
"low_1pct_fps": 311.007,
"low_01pct_fps": 284.642,
"p50_ms": 1.35,
"p95_ms": 1.707,
"p99_ms": 3.08,
"p999_ms": 3.329,
"max_ms": 3.796,
"cpu_p50_ms": 0.645,
"gpu_p50_ms": 1.342,
"histogram": [
32,
8216,
111,
133,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 8684,
"seconds": 12.001,
"avg_fps": 723.617,
"low_1pct_fps": 318.023,
"low_01pct_fps": 247.802,
"p50_ms": 1.318,
"p95_ms": 1.699,
"p99_ms": 2.875,
"p999_ms": 3.573,
"max_ms": 5.186,
"cpu_p50_ms": 0.56,
"gpu_p50_ms": 1.306,
"histogram": [
46,
8389,
199,
49,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-macmini"
}
-854
View File
@@ -1,854 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-04T13:17:26Z",
"app": {
"name": "horizon-client",
"version": "nightly-2026-11-04-67dad0d",
"commit": "67dad0d"
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "",
"driver": "",
"backend": "metal",
"kind": "integrated"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 17.712,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 1528.444,
"stdev": 181.21,
"min": 1319.58,
"max": 1643.783,
"ci95": 450.188
},
"startup.world_ms": {
"n": 3,
"mean": 12368.716,
"stdev": 6475.37,
"min": 7168.429,
"max": 19621.62,
"ci95": 16087.01
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6926.208,
"stdev": 3712.691,
"min": 4208.555,
"max": 11156.419,
"ci95": 9223.581
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 46790.333,
"stdev": 1381.89,
"min": 45236.0,
"max": 47880.0,
"ci95": 3433.082
},
"fps.village.avg_fps": {
"n": 3,
"mean": 453.505,
"stdev": 14.384,
"min": 441.517,
"max": 469.455,
"ci95": 35.735
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 222.897,
"stdev": 10.449,
"min": 212.173,
"max": 233.048,
"ci95": 25.96
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 173.55,
"stdev": 31.798,
"min": 142.967,
"max": 206.437,
"ci95": 78.996
},
"memory.village.rss_mb": {
"n": 3,
"mean": 1544.565,
"stdev": 26.422,
"min": 1516.75,
"max": 1569.328,
"ci95": 65.64
},
"tick.village.client_ms": {
"n": 3,
"mean": 0.528,
"stdev": 0.018,
"min": 0.511,
"max": 0.548,
"ci95": 0.045
},
"cpu.village.p50_ms": {
"n": 3,
"mean": 1.496,
"stdev": 0.049,
"min": 1.453,
"max": 1.549,
"ci95": 0.121
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8297.116,
"stdev": 689.909,
"min": 7716.447,
"max": 9059.776,
"ci95": 1713.966
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 78617.667,
"stdev": 494.532,
"min": 78228.0,
"max": 79174.0,
"ci95": 1228.586
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 216.265,
"stdev": 6.372,
"min": 209.501,
"max": 222.156,
"ci95": 15.831
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 137.027,
"stdev": 3.787,
"min": 133.07,
"max": 140.617,
"ci95": 9.408
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 114.683,
"stdev": 7.985,
"min": 106.585,
"max": 122.551,
"ci95": 19.839
},
"memory.forest.rss_mb": {
"n": 3,
"mean": 1420.638,
"stdev": 247.812,
"min": 1266.047,
"max": 1706.469,
"ci95": 615.649
},
"tick.forest.client_ms": {
"n": 3,
"mean": 0.423,
"stdev": 0.059,
"min": 0.355,
"max": 0.458,
"ci95": 0.148
},
"cpu.forest.p50_ms": {
"n": 3,
"mean": 1.605,
"stdev": 0.269,
"min": 1.295,
"max": 1.779,
"ci95": 0.668
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 8354.659,
"stdev": 2877.749,
"min": 5454.947,
"max": 11209.93,
"ci95": 7149.301
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 74315.667,
"stdev": 63.106,
"min": 74246.0,
"max": 74369.0,
"ci95": 156.776
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 274.828,
"stdev": 4.993,
"min": 270.041,
"max": 280.004,
"ci95": 12.404
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 165.765,
"stdev": 7.21,
"min": 157.538,
"max": 170.985,
"ci95": 17.912
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 140.185,
"stdev": 9.74,
"min": 129.976,
"max": 149.377,
"ci95": 24.198
},
"memory.ocean.rss_mb": {
"n": 3,
"mean": 1306.578,
"stdev": 97.011,
"min": 1199.094,
"max": 1387.641,
"ci95": 241.007
},
"tick.ocean.client_ms": {
"n": 3,
"mean": 0.416,
"stdev": 0.029,
"min": 0.392,
"max": 0.448,
"ci95": 0.071
},
"cpu.ocean.p50_ms": {
"n": 3,
"mean": 1.636,
"stdev": 0.121,
"min": 1.496,
"max": 1.708,
"ci95": 0.301
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 17124.873,
"stdev": 3461.08,
"min": 13599.217,
"max": 20517.544,
"ci95": 8598.494
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 91909.0,
"stdev": 815.603,
"min": 91157.0,
"max": 92776.0,
"ci95": 2026.235
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 242.657,
"stdev": 3.405,
"min": 240.66,
"max": 246.588,
"ci95": 8.459
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 110.95,
"stdev": 14.622,
"min": 99.907,
"max": 127.532,
"ci95": 36.325
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 51.329,
"stdev": 28.253,
"min": 30.391,
"max": 83.465,
"ci95": 70.191
},
"memory.mountain.rss_mb": {
"n": 3,
"mean": 1451.688,
"stdev": 127.471,
"min": 1310.969,
"max": 1559.43,
"ci95": 316.681
},
"tick.mountain.client_ms": {
"n": 3,
"mean": 0.614,
"stdev": 0.017,
"min": 0.594,
"max": 0.627,
"ci95": 0.043
},
"cpu.mountain.p50_ms": {
"n": 3,
"mean": 2.233,
"stdev": 0.05,
"min": 2.194,
"max": 2.289,
"ci95": 0.124
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1697.984,
"stdev": 29.735,
"min": 1667.406,
"max": 1726.797,
"ci95": 73.871
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1428.19,
"stdev": 120.658,
"min": 1333.0,
"max": 1563.891,
"ci95": 299.756
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 4921.753,
"stdev": 30.436,
"min": 4890.971,
"max": 4951.831,
"ci95": 75.613
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 4079.43,
"stdev": 10.978,
"min": 4068.049,
"max": 4089.956,
"ci95": 27.273
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 5634,
"seconds": 12.001,
"avg_fps": 469.455,
"low_1pct_fps": 223.469,
"low_01pct_fps": 171.246,
"p50_ms": 2.054,
"p95_ms": 2.492,
"p99_ms": 3.993,
"p999_ms": 5.425,
"max_ms": 6.645,
"cpu_p50_ms": 1.453,
"gpu_p50_ms": 1.965,
"histogram": [
0,
2082,
3324,
193,
24,
9,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 5395,
"seconds": 12.001,
"avg_fps": 449.543,
"low_1pct_fps": 233.048,
"low_01pct_fps": 206.437,
"p50_ms": 2.155,
"p95_ms": 2.611,
"p99_ms": 4.068,
"p999_ms": 4.456,
"max_ms": 5.305,
"cpu_p50_ms": 1.549,
"gpu_p50_ms": 2.043,
"histogram": [
0,
902,
4255,
206,
29,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 5298,
"seconds": 12.0,
"avg_fps": 441.517,
"low_1pct_fps": 212.173,
"low_01pct_fps": 142.967,
"p50_ms": 2.171,
"p95_ms": 3.249,
"p99_ms": 4.138,
"p999_ms": 5.591,
"max_ms": 9.676,
"cpu_p50_ms": 1.485,
"gpu_p50_ms": 2.092,
"histogram": [
0,
709,
4314,
229,
36,
6,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 2606,
"seconds": 12.002,
"avg_fps": 217.137,
"low_1pct_fps": 140.617,
"low_01pct_fps": 106.585,
"p50_ms": 4.471,
"p95_ms": 5.926,
"p99_ms": 6.575,
"p999_ms": 7.735,
"max_ms": 12.187,
"cpu_p50_ms": 1.74,
"gpu_p50_ms": 4.238,
"histogram": [
0,
0,
0,
221,
2094,
172,
110,
8,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 2514,
"seconds": 12.0,
"avg_fps": 209.501,
"low_1pct_fps": 137.393,
"low_01pct_fps": 122.551,
"p50_ms": 4.627,
"p95_ms": 6.304,
"p99_ms": 6.93,
"p999_ms": 7.971,
"max_ms": 8.506,
"cpu_p50_ms": 1.779,
"gpu_p50_ms": 4.409,
"histogram": [
0,
0,
2,
35,
2097,
200,
162,
17,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 2667,
"seconds": 12.005,
"avg_fps": 222.156,
"low_1pct_fps": 133.07,
"low_01pct_fps": 114.913,
"p50_ms": 4.37,
"p95_ms": 5.756,
"p99_ms": 6.742,
"p999_ms": 8.444,
"max_ms": 8.984,
"cpu_p50_ms": 1.295,
"gpu_p50_ms": 4.152,
"histogram": [
0,
0,
0,
440,
1978,
144,
85,
16,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 3294,
"seconds": 12.003,
"avg_fps": 274.44,
"low_1pct_fps": 157.538,
"low_01pct_fps": 129.976,
"p50_ms": 3.521,
"p95_ms": 4.814,
"p99_ms": 5.592,
"p999_ms": 7.491,
"max_ms": 7.868,
"cpu_p50_ms": 1.703,
"gpu_p50_ms": 3.277,
"histogram": [
0,
4,
55,
2967,
130,
122,
10,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 3241,
"seconds": 12.002,
"avg_fps": 270.041,
"low_1pct_fps": 168.771,
"low_01pct_fps": 141.201,
"p50_ms": 3.578,
"p95_ms": 4.942,
"p99_ms": 5.631,
"p999_ms": 6.498,
"max_ms": 7.601,
"cpu_p50_ms": 1.708,
"gpu_p50_ms": 3.323,
"histogram": [
0,
2,
8,
2970,
110,
144,
5,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 3360,
"seconds": 12.0,
"avg_fps": 280.004,
"low_1pct_fps": 170.985,
"low_01pct_fps": 149.377,
"p50_ms": 3.477,
"p95_ms": 4.678,
"p99_ms": 5.462,
"p999_ms": 6.626,
"max_ms": 6.788,
"cpu_p50_ms": 1.496,
"gpu_p50_ms": 3.23,
"histogram": [
0,
1,
5,
3125,
129,
90,
10,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 2888,
"seconds": 12.0,
"avg_fps": 240.66,
"low_1pct_fps": 127.532,
"low_01pct_fps": 83.465,
"p50_ms": 3.984,
"p95_ms": 5.171,
"p99_ms": 6.823,
"p999_ms": 9.168,
"max_ms": 15.341,
"cpu_p50_ms": 2.194,
"gpu_p50_ms": 3.786,
"histogram": [
0,
0,
4,
2048,
638,
144,
33,
16,
3,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 2889,
"seconds": 12.001,
"avg_fps": 240.722,
"low_1pct_fps": 99.907,
"low_01pct_fps": 40.132,
"p50_ms": 3.961,
"p95_ms": 5.706,
"p99_ms": 7.364,
"p999_ms": 12.597,
"max_ms": 49.156,
"cpu_p50_ms": 2.289,
"gpu_p50_ms": 3.802,
"histogram": [
0,
5,
126,
1859,
599,
194,
72,
22,
8,
1,
2,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 2959,
"seconds": 12.0,
"avg_fps": 246.588,
"low_1pct_fps": 105.411,
"low_01pct_fps": 30.391,
"p50_ms": 3.851,
"p95_ms": 5.353,
"p99_ms": 6.248,
"p999_ms": 9.05,
"max_ms": 54.378,
"cpu_p50_ms": 2.216,
"gpu_p50_ms": 3.667,
"histogram": [
0,
2,
58,
2196,
462,
196,
32,
10,
1,
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-macmini"
}
@@ -1,773 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:13:01Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 3158.208,
"stdev": 215.77,
"min": 3023.046,
"max": 3407.049,
"ci95": 536.046
},
"startup.world_ms": {
"n": 3,
"mean": 7170.164,
"stdev": 557.958,
"min": 6703.242,
"max": 7788.08,
"ci95": 1386.157
},
"chunks.village.load_ms": {
"n": 3,
"mean": 1694.647,
"stdev": 41.285,
"min": 1653.122,
"max": 1735.687,
"ci95": 102.566
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 23544.0,
"stdev": 47.127,
"min": 23495.0,
"max": 23589.0,
"ci95": 117.081
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1711.652,
"stdev": 77.412,
"min": 1623.403,
"max": 1768.093,
"ci95": 192.317
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 593.407,
"stdev": 32.279,
"min": 571.485,
"max": 630.473,
"ci95": 80.191
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 358.473,
"stdev": 27.832,
"min": 326.35,
"max": 375.353,
"ci95": 69.143
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 631.813,
"stdev": 8.381,
"min": 626.803,
"max": 641.489,
"ci95": 20.822
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 2521.363,
"stdev": 291.624,
"min": 2336.881,
"max": 2857.57,
"ci95": 724.493
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 27346.667,
"stdev": 57.934,
"min": 27306.0,
"max": 27413.0,
"ci95": 143.927
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2423.783,
"stdev": 55.746,
"min": 2386.353,
"max": 2487.851,
"ci95": 138.493
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 709.165,
"stdev": 66.309,
"min": 638.666,
"max": 770.286,
"ci95": 164.734
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 401.039,
"stdev": 40.413,
"min": 354.472,
"max": 426.929,
"ci95": 100.399
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 705.008,
"stdev": 6.062,
"min": 698.466,
"max": 710.435,
"ci95": 15.06
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2094.909,
"stdev": 521.225,
"min": 1758.024,
"max": 2695.274,
"ci95": 1294.9
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 24312.667,
"stdev": 261.934,
"min": 24116.0,
"max": 24610.0,
"ci95": 650.732
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2201.125,
"stdev": 73.595,
"min": 2153.6,
"max": 2285.897,
"ci95": 182.834
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 553.968,
"stdev": 100.955,
"min": 445.599,
"max": 645.355,
"ci95": 250.806
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 325.078,
"stdev": 131.154,
"min": 177.4,
"max": 427.981,
"ci95": 325.831
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 652.944,
"stdev": 5.177,
"min": 647.51,
"max": 657.818,
"ci95": 12.862
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 3304.58,
"stdev": 134.041,
"min": 3155.996,
"max": 3416.409,
"ci95": 333.003
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 26591.0,
"stdev": 57.472,
"min": 26525.0,
"max": 26630.0,
"ci95": 142.779
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1961.086,
"stdev": 39.725,
"min": 1921.241,
"max": 2000.69,
"ci95": 98.69
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 536.973,
"stdev": 38.828,
"min": 495.61,
"max": 572.635,
"ci95": 96.461
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 296.586,
"stdev": 30.236,
"min": 275.859,
"max": 331.281,
"ci95": 75.118
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 896.941,
"stdev": 5.177,
"min": 891.506,
"max": 901.814,
"ci95": 12.862
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1458.578,
"stdev": 3.229,
"min": 1455.32,
"max": 1461.777,
"ci95": 8.022
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1236.28,
"stdev": 4.869,
"min": 1230.701,
"max": 1239.672,
"ci95": 12.096
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 21218,
"seconds": 12.0,
"avg_fps": 1768.093,
"low_1pct_fps": 630.473,
"low_01pct_fps": 375.353,
"p50_ms": 0.537,
"p95_ms": 0.846,
"p99_ms": 1.222,
"p999_ms": 2.081,
"max_ms": 6.256,
"cpu_p50_ms": 0.38,
"gpu_p50_ms": 0.104,
"histogram": [
20695,
499,
22,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 19482,
"seconds": 12.001,
"avg_fps": 1623.403,
"low_1pct_fps": 578.262,
"low_01pct_fps": 373.717,
"p50_ms": 0.586,
"p95_ms": 0.95,
"p99_ms": 1.357,
"p999_ms": 2.158,
"max_ms": 7.35,
"cpu_p50_ms": 0.445,
"gpu_p50_ms": 0.104,
"histogram": [
18729,
713,
38,
1,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 20922,
"seconds": 12.0,
"avg_fps": 1743.46,
"low_1pct_fps": 571.485,
"low_01pct_fps": 326.35,
"p50_ms": 0.526,
"p95_ms": 0.883,
"p99_ms": 1.313,
"p999_ms": 2.14,
"max_ms": 12.027,
"cpu_p50_ms": 0.379,
"gpu_p50_ms": 0.104,
"histogram": [
20308,
582,
28,
2,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 29855,
"seconds": 12.0,
"avg_fps": 2487.851,
"low_1pct_fps": 770.286,
"low_01pct_fps": 421.717,
"p50_ms": 0.365,
"p95_ms": 0.619,
"p99_ms": 0.948,
"p999_ms": 1.717,
"max_ms": 9.446,
"cpu_p50_ms": 0.232,
"gpu_p50_ms": 0.157,
"histogram": [
29607,
233,
13,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 28766,
"seconds": 12.0,
"avg_fps": 2397.145,
"low_1pct_fps": 718.542,
"low_01pct_fps": 426.929,
"p50_ms": 0.374,
"p95_ms": 0.662,
"p99_ms": 1.016,
"p999_ms": 1.858,
"max_ms": 6.379,
"cpu_p50_ms": 0.239,
"gpu_p50_ms": 0.157,
"histogram": [
28457,
293,
14,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 28637,
"seconds": 12.0,
"avg_fps": 2386.353,
"low_1pct_fps": 638.666,
"low_01pct_fps": 354.472,
"p50_ms": 0.373,
"p95_ms": 0.704,
"p99_ms": 1.159,
"p999_ms": 1.952,
"max_ms": 14.388,
"cpu_p50_ms": 0.233,
"gpu_p50_ms": 0.157,
"histogram": [
28198,
415,
18,
5,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 25968,
"seconds": 12.001,
"avg_fps": 2163.877,
"low_1pct_fps": 570.949,
"low_01pct_fps": 369.854,
"p50_ms": 0.408,
"p95_ms": 0.767,
"p99_ms": 1.339,
"p999_ms": 2.38,
"max_ms": 3.329,
"cpu_p50_ms": 0.271,
"gpu_p50_ms": 0.141,
"histogram": [
25383,
534,
47,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 25843,
"seconds": 12.0,
"avg_fps": 2153.6,
"low_1pct_fps": 445.599,
"low_01pct_fps": 177.4,
"p50_ms": 0.413,
"p95_ms": 0.725,
"p99_ms": 1.309,
"p999_ms": 3.451,
"max_ms": 12.071,
"cpu_p50_ms": 0.275,
"gpu_p50_ms": 0.142,
"histogram": [
25375,
373,
56,
22,
1,
7,
3,
5,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 27431,
"seconds": 12.0,
"avg_fps": 2285.897,
"low_1pct_fps": 645.355,
"low_01pct_fps": 427.981,
"p50_ms": 0.388,
"p95_ms": 0.693,
"p99_ms": 1.163,
"p999_ms": 2.041,
"max_ms": 3.068,
"cpu_p50_ms": 0.254,
"gpu_p50_ms": 0.141,
"histogram": [
27036,
363,
31,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 23536,
"seconds": 12.0,
"avg_fps": 1961.326,
"low_1pct_fps": 542.675,
"low_01pct_fps": 331.281,
"p50_ms": 0.436,
"p95_ms": 0.856,
"p99_ms": 1.425,
"p999_ms": 2.22,
"max_ms": 9.458,
"cpu_p50_ms": 0.296,
"gpu_p50_ms": 0.14,
"histogram": [
22819,
671,
40,
5,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 23056,
"seconds": 12.001,
"avg_fps": 1921.241,
"low_1pct_fps": 495.61,
"low_01pct_fps": 282.617,
"p50_ms": 0.44,
"p95_ms": 0.911,
"p99_ms": 1.516,
"p999_ms": 2.543,
"max_ms": 14.342,
"cpu_p50_ms": 0.299,
"gpu_p50_ms": 0.141,
"histogram": [
22198,
783,
65,
7,
0,
2,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 24009,
"seconds": 12.0,
"avg_fps": 2000.69,
"low_1pct_fps": 572.635,
"low_01pct_fps": 275.859,
"p50_ms": 0.436,
"p95_ms": 0.819,
"p99_ms": 1.274,
"p999_ms": 2.085,
"max_ms": 14.003,
"cpu_p50_ms": 0.297,
"gpu_p50_ms": 0.14,
"histogram": [
23421,
560,
22,
0,
1,
0,
2,
2,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -1,773 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:55:59Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 2897.812,
"stdev": 82.455,
"min": 2844.677,
"max": 2992.8,
"ci95": 204.846
},
"startup.world_ms": {
"n": 3,
"mean": 10503.454,
"stdev": 493.592,
"min": 9934.491,
"max": 10816.992,
"ci95": 1226.251
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3717.47,
"stdev": 1330.649,
"min": 2942.872,
"max": 5253.954,
"ci95": 3305.783
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 47525.333,
"stdev": 1369.122,
"min": 46133.0,
"max": 48870.0,
"ci95": 3401.363
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1163.541,
"stdev": 3.485,
"min": 1159.52,
"max": 1165.693,
"ci95": 8.658
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 415.992,
"stdev": 15.742,
"min": 399.062,
"max": 430.188,
"ci95": 39.108
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 215.549,
"stdev": 34.678,
"min": 177.857,
"max": 246.101,
"ci95": 86.151
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 1833.432,
"stdev": 4.415,
"min": 1828.334,
"max": 1835.997,
"ci95": 10.969
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 5455.049,
"stdev": 593.897,
"min": 4973.659,
"max": 6118.723,
"ci95": 1475.44
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 82758.0,
"stdev": 413.547,
"min": 82487.0,
"max": 83234.0,
"ci95": 1027.39
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1389.46,
"stdev": 28.128,
"min": 1371.096,
"max": 1421.843,
"ci95": 69.88
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 504.377,
"stdev": 13.251,
"min": 492.444,
"max": 518.637,
"ci95": 32.919
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 329.682,
"stdev": 47.451,
"min": 294.872,
"max": 383.732,
"ci95": 117.885
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 1985.175,
"stdev": 6.354,
"min": 1978.907,
"max": 1991.612,
"ci95": 15.786
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 4504.145,
"stdev": 745.393,
"min": 3650.589,
"max": 5026.8,
"ci95": 1851.809
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 74927.333,
"stdev": 399.375,
"min": 74476.0,
"max": 75235.0,
"ci95": 992.182
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1188.639,
"stdev": 10.807,
"min": 1177.005,
"max": 1198.365,
"ci95": 26.849
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 154.773,
"stdev": 3.712,
"min": 151.38,
"max": 158.738,
"ci95": 9.223
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 67.809,
"stdev": 3.032,
"min": 65.903,
"max": 71.305,
"ci95": 7.532
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 2084.845,
"stdev": 9.674,
"min": 2074.009,
"max": 2092.615,
"ci95": 24.034
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 9048.431,
"stdev": 344.33,
"min": 8821.41,
"max": 9444.624,
"ci95": 855.432
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 89659.667,
"stdev": 87.123,
"min": 89561.0,
"max": 89726.0,
"ci95": 216.442
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1085.847,
"stdev": 13.819,
"min": 1070.378,
"max": 1096.974,
"ci95": 34.332
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 257.7,
"stdev": 31.437,
"min": 224.366,
"max": 286.814,
"ci95": 78.101
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 107.783,
"stdev": 14.883,
"min": 91.541,
"max": 120.768,
"ci95": 36.975
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 2708.833,
"stdev": 9.674,
"min": 2697.997,
"max": 2716.603,
"ci95": 24.034
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2044.895,
"stdev": 10.562,
"min": 2032.875,
"max": 2052.691,
"ci95": 26.238
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1730.107,
"stdev": 14.764,
"min": 1717.52,
"max": 1746.357,
"ci95": 36.678
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 13986,
"seconds": 12.001,
"avg_fps": 1165.41,
"low_1pct_fps": 399.062,
"low_01pct_fps": 177.857,
"p50_ms": 0.822,
"p95_ms": 1.21,
"p99_ms": 1.691,
"p999_ms": 3.499,
"max_ms": 13.089,
"cpu_p50_ms": 0.665,
"gpu_p50_ms": 0.193,
"histogram": [
12380,
1527,
58,
13,
2,
2,
1,
2,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 13915,
"seconds": 12.001,
"avg_fps": 1159.52,
"low_1pct_fps": 430.188,
"low_01pct_fps": 246.101,
"p50_ms": 0.809,
"p95_ms": 1.212,
"p99_ms": 1.702,
"p999_ms": 3.291,
"max_ms": 6.894,
"cpu_p50_ms": 0.648,
"gpu_p50_ms": 0.192,
"histogram": [
11996,
1839,
59,
17,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 13989,
"seconds": 12.001,
"avg_fps": 1165.693,
"low_1pct_fps": 418.725,
"low_01pct_fps": 222.688,
"p50_ms": 0.816,
"p95_ms": 1.22,
"p99_ms": 1.692,
"p999_ms": 3.264,
"max_ms": 7.781,
"cpu_p50_ms": 0.669,
"gpu_p50_ms": 0.193,
"histogram": [
12321,
1578,
72,
11,
2,
4,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 17063,
"seconds": 12.001,
"avg_fps": 1421.843,
"low_1pct_fps": 502.049,
"low_01pct_fps": 294.872,
"p50_ms": 0.666,
"p95_ms": 0.935,
"p99_ms": 1.549,
"p999_ms": 2.692,
"max_ms": 6.831,
"cpu_p50_ms": 0.533,
"gpu_p50_ms": 0.362,
"histogram": [
16417,
596,
40,
7,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 16454,
"seconds": 12.001,
"avg_fps": 1371.096,
"low_1pct_fps": 492.444,
"low_01pct_fps": 310.443,
"p50_ms": 0.688,
"p95_ms": 0.979,
"p99_ms": 1.556,
"p999_ms": 2.693,
"max_ms": 4.596,
"cpu_p50_ms": 0.553,
"gpu_p50_ms": 0.36,
"histogram": [
15692,
697,
57,
7,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 16506,
"seconds": 12.001,
"avg_fps": 1375.442,
"low_1pct_fps": 518.637,
"low_01pct_fps": 383.732,
"p50_ms": 0.675,
"p95_ms": 1.017,
"p99_ms": 1.605,
"p999_ms": 2.369,
"max_ms": 3.201,
"cpu_p50_ms": 0.54,
"gpu_p50_ms": 0.364,
"histogram": [
15628,
833,
44,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 14381,
"seconds": 12.001,
"avg_fps": 1198.365,
"low_1pct_fps": 154.2,
"low_01pct_fps": 66.219,
"p50_ms": 0.731,
"p95_ms": 1.023,
"p99_ms": 3.003,
"p999_ms": 12.478,
"max_ms": 18.863,
"cpu_p50_ms": 0.596,
"gpu_p50_ms": 0.315,
"histogram": [
13586,
546,
105,
60,
13,
13,
8,
16,
8,
11,
5,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 14287,
"seconds": 12.0,
"avg_fps": 1190.548,
"low_1pct_fps": 158.738,
"low_01pct_fps": 71.305,
"p50_ms": 0.741,
"p95_ms": 1.041,
"p99_ms": 2.9,
"p999_ms": 12.0,
"max_ms": 17.23,
"cpu_p50_ms": 0.602,
"gpu_p50_ms": 0.315,
"histogram": [
13450,
591,
109,
57,
13,
8,
10,
11,
14,
10,
7,
6,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 14124,
"seconds": 12.0,
"avg_fps": 1177.005,
"low_1pct_fps": 151.38,
"low_01pct_fps": 65.903,
"p50_ms": 0.739,
"p95_ms": 1.096,
"p99_ms": 2.928,
"p999_ms": 11.884,
"max_ms": 19.375,
"cpu_p50_ms": 0.604,
"gpu_p50_ms": 0.314,
"histogram": [
13052,
822,
114,
54,
16,
8,
4,
14,
11,
15,
3,
8,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 12845,
"seconds": 12.0,
"avg_fps": 1070.378,
"low_1pct_fps": 224.366,
"low_01pct_fps": 91.541,
"p50_ms": 0.825,
"p95_ms": 1.362,
"p99_ms": 2.325,
"p999_ms": 9.621,
"max_ms": 13.309,
"cpu_p50_ms": 0.689,
"gpu_p50_ms": 0.307,
"histogram": [
10937,
1699,
143,
25,
5,
6,
4,
8,
8,
8,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 13083,
"seconds": 12.001,
"avg_fps": 1090.188,
"low_1pct_fps": 261.921,
"low_01pct_fps": 111.039,
"p50_ms": 0.816,
"p95_ms": 1.335,
"p99_ms": 2.14,
"p999_ms": 8.222,
"max_ms": 11.95,
"cpu_p50_ms": 0.681,
"gpu_p50_ms": 0.306,
"histogram": [
11105,
1812,
112,
21,
5,
5,
5,
6,
11,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 13164,
"seconds": 12.0,
"avg_fps": 1096.974,
"low_1pct_fps": 286.814,
"low_01pct_fps": 120.768,
"p50_ms": 0.822,
"p95_ms": 1.307,
"p99_ms": 1.989,
"p999_ms": 7.701,
"max_ms": 9.429,
"cpu_p50_ms": 0.687,
"gpu_p50_ms": 0.307,
"histogram": [
11280,
1758,
82,
13,
5,
5,
6,
8,
7,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -1,773 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:37:03Z",
"app": {
"name": "horizon-client",
"version": "v0.2.97",
"commit": null
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080",
"vendor": "nvidia",
"driver": "NVIDIA 617.42",
"backend": "vulkan",
"kind": "discrete"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 2950.343,
"stdev": 162.732,
"min": 2822.925,
"max": 3133.657,
"ci95": 404.282
},
"startup.world_ms": {
"n": 3,
"mean": 6135.1,
"stdev": 863.831,
"min": 5547.861,
"max": 7126.98,
"ci95": 2146.05
},
"chunks.village.load_ms": {
"n": 3,
"mean": 985.519,
"stdev": 28.184,
"min": 956.244,
"max": 1012.469,
"ci95": 70.019
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 6908.0,
"stdev": 9.849,
"min": 6897.0,
"max": 6916.0,
"ci95": 24.468
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2941.608,
"stdev": 79.283,
"min": 2850.416,
"max": 2994.188,
"ci95": 196.965
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 1271.318,
"stdev": 58.797,
"min": 1203.758,
"max": 1310.917,
"ci95": 146.072
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 839.261,
"stdev": 37.982,
"min": 795.415,
"max": 862.067,
"ci95": 94.36
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 363.984,
"stdev": 0.926,
"min": 363.449,
"max": 365.053,
"ci95": 2.299
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 1350.752,
"stdev": 293.128,
"min": 1106.764,
"max": 1675.913,
"ci95": 728.23
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 8066.0,
"stdev": 108.885,
"min": 7986.0,
"max": 8190.0,
"ci95": 270.508
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 3494.693,
"stdev": 110.997,
"min": 3389.894,
"max": 3610.993,
"ci95": 275.755
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 1295.14,
"stdev": 159.638,
"min": 1148.468,
"max": 1465.171,
"ci95": 396.596
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 859.326,
"stdev": 119.845,
"min": 721.994,
"max": 942.743,
"ci95": 297.734
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 381.189,
"stdev": 5.222,
"min": 376.423,
"max": 386.77,
"ci95": 12.972
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 1123.386,
"stdev": 604.175,
"min": 734.302,
"max": 1819.414,
"ci95": 1500.976
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 6613.0,
"stdev": 135.632,
"min": 6523.0,
"max": 6769.0,
"ci95": 336.955
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 3346.003,
"stdev": 59.312,
"min": 3303.535,
"max": 3413.77,
"ci95": 147.352
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 1357.991,
"stdev": 95.364,
"min": 1248.887,
"max": 1425.451,
"ci95": 236.917
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 911.822,
"stdev": 94.717,
"min": 802.528,
"max": 969.989,
"ci95": 235.308
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 391.636,
"stdev": 5.435,
"min": 387.424,
"max": 397.771,
"ci95": 13.502
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 1588.954,
"stdev": 458.924,
"min": 1309.548,
"max": 2118.606,
"ci95": 1140.123
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 7887.667,
"stdev": 41.041,
"min": 7862.0,
"max": 7935.0,
"ci95": 101.959
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 3375.623,
"stdev": 26.82,
"min": 3352.309,
"max": 3404.933,
"ci95": 66.629
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 1383.745,
"stdev": 67.628,
"min": 1324.071,
"max": 1457.203,
"ci95": 168.01
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 850.213,
"stdev": 103.149,
"min": 790.305,
"max": 969.318,
"ci95": 256.256
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 448.64,
"stdev": 5.435,
"min": 444.428,
"max": 454.775,
"ci95": 13.502
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1171.629,
"stdev": 0.337,
"min": 1171.398,
"max": 1172.016,
"ci95": 0.837
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1082.078,
"stdev": 12.494,
"min": 1069.117,
"max": 1094.047,
"ci95": 31.04
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 35764,
"seconds": 12.0,
"avg_fps": 2980.22,
"low_1pct_fps": 1310.917,
"low_01pct_fps": 860.301,
"p50_ms": 0.317,
"p95_ms": 0.433,
"p99_ms": 0.633,
"p999_ms": 0.937,
"max_ms": 2.807,
"cpu_p50_ms": 0.192,
"gpu_p50_ms": 0.063,
"histogram": [
35741,
22,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 35931,
"seconds": 12.0,
"avg_fps": 2994.188,
"low_1pct_fps": 1299.278,
"low_01pct_fps": 795.415,
"p50_ms": 0.318,
"p95_ms": 0.428,
"p99_ms": 0.62,
"p999_ms": 0.943,
"max_ms": 5.797,
"cpu_p50_ms": 0.191,
"gpu_p50_ms": 0.063,
"histogram": [
35906,
23,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 34206,
"seconds": 12.0,
"avg_fps": 2850.416,
"low_1pct_fps": 1203.758,
"low_01pct_fps": 862.067,
"p50_ms": 0.326,
"p95_ms": 0.508,
"p99_ms": 0.705,
"p999_ms": 1.025,
"max_ms": 1.398,
"cpu_p50_ms": 0.196,
"gpu_p50_ms": 0.063,
"histogram": [
34167,
39,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 41800,
"seconds": 12.0,
"avg_fps": 3483.193,
"low_1pct_fps": 1271.782,
"low_01pct_fps": 913.242,
"p50_ms": 0.264,
"p95_ms": 0.415,
"p99_ms": 0.657,
"p999_ms": 0.935,
"max_ms": 1.482,
"cpu_p50_ms": 0.144,
"gpu_p50_ms": 0.075,
"histogram": [
41768,
32,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 43333,
"seconds": 12.0,
"avg_fps": 3610.993,
"low_1pct_fps": 1465.171,
"low_01pct_fps": 942.743,
"p50_ms": 0.263,
"p95_ms": 0.355,
"p99_ms": 0.561,
"p999_ms": 0.859,
"max_ms": 1.694,
"cpu_p50_ms": 0.143,
"gpu_p50_ms": 0.074,
"histogram": [
43310,
23,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 40679,
"seconds": 12.0,
"avg_fps": 3389.894,
"low_1pct_fps": 1148.468,
"low_01pct_fps": 721.994,
"p50_ms": 0.267,
"p95_ms": 0.446,
"p99_ms": 0.67,
"p999_ms": 1.183,
"max_ms": 2.434,
"cpu_p50_ms": 0.146,
"gpu_p50_ms": 0.076,
"histogram": [
40590,
87,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 39849,
"seconds": 12.0,
"avg_fps": 3320.704,
"low_1pct_fps": 1399.634,
"low_01pct_fps": 969.989,
"p50_ms": 0.281,
"p95_ms": 0.404,
"p99_ms": 0.601,
"p999_ms": 0.875,
"max_ms": 1.494,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
39831,
18,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 40966,
"seconds": 12.0,
"avg_fps": 3413.77,
"low_1pct_fps": 1425.451,
"low_01pct_fps": 962.948,
"p50_ms": 0.278,
"p95_ms": 0.372,
"p99_ms": 0.581,
"p999_ms": 0.857,
"max_ms": 1.576,
"cpu_p50_ms": 0.157,
"gpu_p50_ms": 0.069,
"histogram": [
40947,
19,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 39643,
"seconds": 12.0,
"avg_fps": 3303.535,
"low_1pct_fps": 1248.887,
"low_01pct_fps": 802.528,
"p50_ms": 0.28,
"p95_ms": 0.413,
"p99_ms": 0.642,
"p999_ms": 1.087,
"max_ms": 2.215,
"cpu_p50_ms": 0.158,
"gpu_p50_ms": 0.069,
"histogram": [
39582,
60,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 40436,
"seconds": 12.0,
"avg_fps": 3369.626,
"low_1pct_fps": 1369.962,
"low_01pct_fps": 790.305,
"p50_ms": 0.279,
"p95_ms": 0.403,
"p99_ms": 0.604,
"p999_ms": 0.812,
"max_ms": 10.313,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
40425,
8,
1,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 40860,
"seconds": 12.0,
"avg_fps": 3404.933,
"low_1pct_fps": 1457.203,
"low_01pct_fps": 969.318,
"p50_ms": 0.278,
"p95_ms": 0.389,
"p99_ms": 0.574,
"p999_ms": 0.814,
"max_ms": 2.714,
"cpu_p50_ms": 0.158,
"gpu_p50_ms": 0.069,
"histogram": [
40847,
11,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 40229,
"seconds": 12.0,
"avg_fps": 3352.309,
"low_1pct_fps": 1324.071,
"low_01pct_fps": 791.016,
"p50_ms": 0.28,
"p95_ms": 0.402,
"p99_ms": 0.617,
"p999_ms": 0.968,
"max_ms": 3.746,
"cpu_p50_ms": 0.159,
"gpu_p50_ms": 0.069,
"histogram": [
40192,
35,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-win-9800x3d-5080"
}
@@ -1,68 +0,0 @@
# Horizon main vs vanilla 26.3 on a loaded Mac mini M4 Pro (2026-11-01)
`tools/perf_bench.py` reports for Horizon main 1bd24e4042, vanilla 26.3 and vanilla with performance mods, render distances
8, 16 and 32, three games each, every game from a fresh world. **These are not player numbers:** the Mac mini was running other
work the whole time (1-minute load average 15-114 on 14 cores), and every game rendered without presenting. They are kept because
they're the first Mac reports and show how each game behaves under heavy CPU contention. The reports are the
`*-m4pro-1080p-rd*.json` files next to this page.
## Setup
- **Machine:** Mac mini, Apple M4 Pro (10 performance + 4 efficiency cores, 20-core GPU), 64 GB, macOS 27.0.1. Screen
1920x1080; the session was locked, so every game ran `offscreen` (Horizon renders into a texture; vanilla draws into a
window nobody sees).
- **Horizon:** main 1bd24e4042, `cargo build --release -p horizon-client`, Metal.
- **Vanilla:** the 26.3 client jar with Fabric Loader and the driver mod, Java 25 (Temurin), `-Xmx2G` with the launcher's G1
flags, OpenGL 4.1 (Apple's Metal-backed driver).
- **Vanilla + mods:** the same with Sodium mc26.3-0.9.2, Lithium mc26.3-0.26.2, FerriteCore 9.0.0, ImmediatelyFast 1.17.1 and
Fabric API (`tools/benchmark-deps/mods.lock.json`).
- **Order:** the games took turns inside each repeat (vanilla, mods, Horizon; then mods, Horizon, vanilla; then Horizon, vanilla,
mods), one game at a time.
## Why these numbers aren't a fair comparison
- **Load.** Load before each game: RD 8 46-82, RD 16 43-114, RD 32 15-59 (the later RD 32 games ran at 15-30). Horizon raises its
render and server threads to the user-interactive QoS class, and the JVM's threads stay at the default, so under this
contention macOS runs Horizon's frame first. Vanilla's 1% lows of 2-15 FPS show that, more than how fast either game is.
- **Resolution.** Vanilla's 1920x1080 window was shrunk to **1920x930** by the 1080p screen (menu and title bars); Horizon,
offscreen, rendered 1920x1080. Vanilla drew 14% fewer pixels, which favours it in GPU-bound scenes. Because the recorded
windows differ, `perf_bench.py --table` refuses to compare these reports. The tables below were made with the window check
skipped. A rerun at a size that fits the screen (1600x900) would avoid it.
## Results
Mean of 3 games ± 95% confidence interval. FPS over the 12-second flight (average / 1% low / 0.1% low):
| RD | scene | Horizon main | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- | --- |
| 8 | forest | 364 ±22 / 145 ±54 / 97 ±38 | 66 ±22 / 8 ±10 / 6 ±8 | 47 ±42 / 4 ±2 / 3 ±1 |
| 8 | mountain | 466 ±7 / 165 ±54 / 91 ±37 | 46 ±37 / 5 ±7 / 4 ±6 | 67 ±35 / 8 ±5 / 4 ±4 |
| 8 | ocean | 410 ±88 / 161 ±39 / 112 ±41 | 50 ±44 / 6 ±13 / 5 ±11 | 35 ±16 / 5 ±4 / 4 ±4 |
| 8 | village | 540 ±453 / 196 ±189 / 135 ±184 | 42 ±16 / 4 ±3 / 3 ±0 | 33 ±46 / 4 ±6 / 3 ±4 |
| 16 | forest | 333 ±4 / 193 ±29 / 137 ±71 | 23 ±9 / 2 ±1 / 2 ±1 | 62 ±94 / 10 ±21 / 7 ±14 |
| 16 | mountain | 357 ±6 / 200 ±30 / 154 ±33 | 44 ±12 / 7 ±2 / 4 ±1 | 36 ±44 / 4 ±5 / 3 ±5 |
| 16 | ocean | 412 ±4 / 209 ±26 / 127 ±44 | 51 ±16 / 6 ±8 / 4 ±7 | 75 ±159 / 13 ±33 / 7 ±16 |
| 16 | village | 568 ±30 / 271 ±25 / 210 ±75 | 46 ±110 / 9 ±27 / 7 ±17 | 46 ±45 / 7 ±9 / 5 ±4 |
| 32 | forest | 165 ±11 / 104 ±51 / 95 ±60 | 68 ±122 / 18 ±49 / 14 ±42 | 114 ±132 / 29 ±44 / 21 ±34 |
| 32 | mountain | 190 ±5 / 109 ±35 / 91 ±32 | 29 ±19 / 3 ±2 / 2 ±3 | 57 ±49 / 7 ±2 / 3 ±3 |
| 32 | ocean | 210 ±13 / 129 ±77 / 116 ±98 | 44 ±80 / 5 ±4 / 2 ±1 | 119 ±176 / 32 ±66 / 17 ±37 |
| 32 | village | 326 ±17 / 152 ±85 / 123 ±84 | 81 ±99 / 15 ±20 / 11 ±11 | 92 ±28 / 13 ±6 / 9 ±3 |
Start to title / in-world, mean scene load, RSS steady / peak, physical footprint steady / peak (macOS `footprint`, which on Apple
Silicon includes the game's GPU allocations), load average before the first game:
| RD | Horizon main | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- |
| 8 | 7.2 / 11.4, 6.5, 1379 / 1537, 1883 / 2094, 73 | 49.9 / 89.1, 18.2, 1811 / 1877, 2635 / 2709, 66 | 56.3 / 97.7, 17.9, 1927 / 2019, 2396 / 2441, 74 |
| 16 | 1.5 / 14.0, 9.5, 1617 / 1892, 2495 / 3084, 114 | 19.4 / 78.9, 33.3, 2431 / 2555, 3450 / 3713, 76 | 23.8 / 76.9, 27.1, 2355 / 2566, 3212 / 3431, 70 |
| 32 | 2.2 / 14.3, 11.1, 2196 / 2537, 4373 / 5560, 28 | 20.6 / 124.2, 44.2, 2838 / 2958, 4265 / 4618, 59 | 13.5 / 63.2, 29.9, 3705 / 4078, 4647 / 5250, 37 |
## What it shows
- Horizon is GPU-bound on the M4 Pro in every scene at every distance: its median GPU time per frame is 1.6-2.5 ms at RD 8 and
2.9-5.7 ms at RD 32, its render thread's CPU time 0.6-2.2 ms.
- In the least-loaded RD 32 games (load 15-22), vanilla + mods matched Horizon's average FPS over the forest (162 vs 163-170) and
the ocean (187 vs 209-216); Horizon's 1% lows were 2.4 times the modded client's there, and 15-17 times over the village and the
mountain.
- Horizon's resident memory is 1.2-1.7 times smaller than both clients'. Its physical footprint, GPU memory included, is level
with vanilla's at RD 32 (4373 vs 4265 MB steady) and above it at peak (5560 vs 4618 MB).
-42
View File
@@ -1,42 +0,0 @@
# Horizon Nightly 2026-11-04 vs vanilla 26.3 on the Mac mini and the desktop (2026-11-04)
> **The Mac mini reports (`*-macmini-rd*.json`) are offscreen, not comparable.** Horizon rendered offscreen (no presenting)
> while vanilla drew into a window, so the Mac rows below are not a Horizon-vs-vanilla comparison, and the website does not
> publish them: it keeps the Mac mini's 2026-10-22 runs. They are kept here only as a record of that day's offscreen run.
The desktop runs are the ones behind playhorizon.dev/benchmarks as of 2026-11-04 (horizon-rust/horizon-site#58): `tools/perf_bench.py`
reports, render distance 12 and 32, three games each, every game from a fresh world (seed 1234), 1280x720, VSync off. Files:
`{horizon,vanilla}-macmini-rd{12,32}.json` and `{horizon,vanilla}-desktop-rd{12,32}.json` next to this page.
## Builds
- **Horizon:** the Nightly 2026-11-04 release bundle (67dad0d), as the launcher installs it: macOS arm64 on the Mac,
Linux x86_64 on the desktop. Its resources come from the bundle's own `horizon-prepare`. perf_bench ran from main
a55b9a2e52 and wrote that checkout's crate version and commit; `app.version` and `app.commit` were set to the bundle's by hand.
- **Vanilla:** the 26.3 client jar with Fabric Loader and the driver mod, Java 25, the official launcher's JVM flags.
## Machines and conditions
- **Mac mini, Apple M4 Pro (14 cores, 20-core GPU), macOS: offscreen, not comparable.** The session was locked, so Horizon ran `--offscreen`: a locked
session gives a window no frames, and windowed Horizon measured 0 FPS. Vanilla drew into its window. Offscreen skips
presenting, which favours Horizon, so its averages aren't held at the 144 Hz display as they were on 2026-10-22. Each game
started at a 1-minute load of 17-18 (the run waited for < 20); during the runs the load rose to 30-50.
- **Desktop, RTX 4080 + i7-13700KF, NixOS, NVIDIA 595.** Both games drew into a headless sway rendering on the RTX 4080 (never
on its screen), both through Wayland on Vulkan. Vanilla went through SDL's Wayland driver: through Xwayland each of its
frames took about 2 s on this day. The machine also runs CI and agents' builds: each game started at a load of 28-30 on
24 threads (the run waited for < 30) and the load rose to 40-70 during the runs. Vanilla at RD 32 crashed twice under that
load (`GPU timeout attempting to acquire next frame`) before a third series finished. Start-up times are inflated by the
load and by a cold disk on each series' first start (start → world up to 130-235 s).
- **Order:** RD 12 ran Horizon then vanilla; RD 32 ran vanilla then Horizon.
## Results (average over the flights of the three games; FPS average / 1% low)
| Machine | RD | Horizon | Vanilla | Horizon ÷ vanilla (avg, 1% low) |
|---|---|---|---|---|
| Mac mini (**offscreen, not comparable**) | 12 | 774 / 294 | 132 / 27 | — |
| Mac mini (**offscreen, not comparable**) | 32 | 297 / 159 | 68 / 16 | — |
| Desktop | 12 | 2,224 / 308 | 1,824 / 201 | 1.2x, 1.5x |
| Desktop | 32 | 800 / 140 | 696 / 64 | 1.15x, 2.2x |
Neither machine was idle, so these say how both games behave on busy machines. They are not clean ratios; for those, see
`windows-9800x3d-5080-2026-10-29.md`. The scripts and logs are in horizon-parity `shots/site-bench-2026-11-04/`.
-787
View File
@@ -1,787 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-04T13:28:55Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "linux",
"arch": "x86_64",
"cpu_threads": 24,
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.57)",
"cpu": "13th Gen Intel(R) Core(TM) i7-13700KF",
"ram_mb": 31904,
"gpu": {
"name": "NVIDIA GeForce RTX 4080",
"vendor": "NVIDIA",
"driver": "1.4.329 NVIDIA 595.71.05",
"backend": "vulkan",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 12,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 28.788,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 121075.467,
"stdev": 30646.429,
"min": 93272.034,
"max": 153935.834,
"ci95": 76136.094
},
"startup.world_ms": {
"n": 3,
"mean": 191063.8,
"stdev": 34368.16,
"min": 153895.533,
"max": 221692.034,
"ci95": 85382.134
},
"chunks.village.load_ms": {
"n": 3,
"mean": 18724.322,
"stdev": 9639.048,
"min": 13107.549,
"max": 29854.376,
"ci95": 23946.655
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2161.532,
"stdev": 287.109,
"min": 1830.069,
"max": 2332.81,
"ci95": 713.276
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 162.575,
"stdev": 29.799,
"min": 136.023,
"max": 194.804,
"ci95": 74.03
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 27.752,
"stdev": 3.186,
"min": 24.403,
"max": 30.746,
"ci95": 7.916
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 13.0,
"stdev": 0.0,
"min": 13.0,
"max": 13.0,
"ci95": 0.0
},
"memory.village.rss_mb": {
"n": 3,
"mean": 2902.054,
"stdev": 158.708,
"min": 2747.244,
"max": 3064.393,
"ci95": 394.285
},
"tick.village.client_ms": {
"n": 3,
"mean": 0.691,
"stdev": 0.097,
"min": 0.601,
"max": 0.794,
"ci95": 0.241
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8852.332,
"stdev": 530.809,
"min": 8350.811,
"max": 9408.241,
"ci95": 1318.709
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1841.581,
"stdev": 1186.781,
"min": 471.209,
"max": 2529.847,
"ci95": 2948.365
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 255.087,
"stdev": 217.128,
"min": 5.798,
"max": 402.877,
"ci95": 539.419
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 92.882,
"stdev": 81.722,
"min": 0.61,
"max": 156.13,
"ci95": 203.024
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 13.0,
"stdev": 0.0,
"min": 13.0,
"max": 13.0,
"ci95": 0.0
},
"memory.forest.rss_mb": {
"n": 3,
"mean": 3008.178,
"stdev": 149.07,
"min": 2875.082,
"max": 3169.256,
"ci95": 370.34
},
"tick.forest.client_ms": {
"n": 3,
"mean": 0.687,
"stdev": 0.132,
"min": 0.562,
"max": 0.825,
"ci95": 0.327
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 21215.745,
"stdev": 22821.547,
"min": 8034.077,
"max": 47567.797,
"ci95": 56696.441
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1525.483,
"stdev": 1472.651,
"min": 633.984,
"max": 3225.275,
"ci95": 3658.563
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 191.398,
"stdev": 272.942,
"min": 10.346,
"max": 505.335,
"ci95": 678.08
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 49.506,
"stdev": 66.145,
"min": 1.104,
"max": 124.874,
"ci95": 164.326
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 13.0,
"stdev": 0.0,
"min": 13.0,
"max": 13.0,
"ci95": 0.0
},
"memory.ocean.rss_mb": {
"n": 3,
"mean": 3062.136,
"stdev": 128.594,
"min": 2962.541,
"max": 3207.313,
"ci95": 319.472
},
"tick.ocean.client_ms": {
"n": 3,
"mean": 1.029,
"stdev": 0.573,
"min": 0.595,
"max": 1.678,
"ci95": 1.423
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 33255.15,
"stdev": 20451.99,
"min": 16655.144,
"max": 56102.104,
"ci95": 50809.661
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1769.12,
"stdev": 996.863,
"min": 818.512,
"max": 2806.557,
"ci95": 2476.544
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 195.916,
"stdev": 195.285,
"min": 11.586,
"max": 400.567,
"ci95": 485.154
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 52.202,
"stdev": 51.208,
"min": 1.461,
"max": 103.864,
"ci95": 127.217
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 13.0,
"stdev": 0.0,
"min": 13.0,
"max": 13.0,
"ci95": 0.0
},
"memory.mountain.rss_mb": {
"n": 3,
"mean": 2957.948,
"stdev": 340.935,
"min": 2586.504,
"max": 3256.625,
"ci95": 846.998
},
"tick.mountain.client_ms": {
"n": 3,
"mean": 1.436,
"stdev": 1.406,
"min": 0.462,
"max": 3.048,
"ci95": 3.494
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 3107.953,
"stdev": 141.888,
"min": 3020.105,
"max": 3271.645,
"ci95": 352.498
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2985.317,
"stdev": 210.327,
"min": 2761.164,
"max": 3178.35,
"ci95": 522.523
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 21963,
"seconds": 12.001,
"avg_fps": 1830.069,
"low_1pct_fps": 136.023,
"low_01pct_fps": 30.746,
"p50_ms": 0.331,
"p95_ms": 1.153,
"p99_ms": 3.001,
"p999_ms": 9.997,
"max_ms": 326.36,
"histogram": [
20336,
1238,
167,
108,
40,
23,
12,
10,
8,
7,
5,
1,
2,
1,
2,
0,
1,
1,
0,
0,
0,
1,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 27994,
"seconds": 12.0,
"avg_fps": 2332.81,
"low_1pct_fps": 194.804,
"low_01pct_fps": 28.107,
"p50_ms": 0.321,
"p95_ms": 0.771,
"p99_ms": 1.356,
"p999_ms": 3.836,
"max_ms": 331.931,
"histogram": [
27270,
652,
34,
13,
5,
2,
3,
4,
3,
2,
0,
1,
0,
0,
0,
0,
1,
0,
1,
1,
0,
2,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 27861,
"seconds": 12.0,
"avg_fps": 2321.717,
"low_1pct_fps": 156.898,
"low_01pct_fps": 24.403,
"p50_ms": 0.281,
"p95_ms": 0.852,
"p99_ms": 1.73,
"p999_ms": 6.331,
"max_ms": 301.991,
"histogram": [
26932,
726,
124,
42,
4,
4,
3,
2,
9,
2,
3,
1,
1,
1,
0,
1,
1,
0,
2,
0,
2,
1,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 30359,
"seconds": 12.0,
"avg_fps": 2529.847,
"low_1pct_fps": 356.586,
"low_01pct_fps": 121.906,
"p50_ms": 0.353,
"p95_ms": 0.563,
"p99_ms": 1.405,
"p999_ms": 4.444,
"max_ms": 20.302,
"histogram": [
29756,
460,
57,
52,
11,
5,
3,
5,
2,
4,
1,
0,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 5781,
"seconds": 12.268,
"avg_fps": 471.209,
"low_1pct_fps": 5.798,
"low_01pct_fps": 0.61,
"p50_ms": 0.359,
"p95_ms": 0.77,
"p99_ms": 1.78,
"p999_ms": 20.826,
"max_ms": 5834.37,
"histogram": [
5586,
158,
12,
11,
0,
1,
3,
2,
0,
0,
2,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
2,
3
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 30285,
"seconds": 12.0,
"avg_fps": 2523.687,
"low_1pct_fps": 402.877,
"low_01pct_fps": 156.13,
"p50_ms": 0.351,
"p95_ms": 0.539,
"p99_ms": 1.574,
"p999_ms": 3.683,
"max_ms": 16.022,
"histogram": [
29666,
490,
74,
35,
7,
2,
2,
1,
1,
5,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 11685,
"seconds": 16.293,
"avg_fps": 717.19,
"low_1pct_fps": 10.346,
"low_01pct_fps": 1.104,
"p50_ms": 0.299,
"p95_ms": 1.119,
"p99_ms": 2.568,
"p999_ms": 12.054,
"max_ms": 5250.537,
"histogram": [
10993,
502,
102,
39,
14,
8,
6,
4,
4,
1,
1,
2,
0,
2,
0,
0,
0,
0,
1,
0,
0,
2,
0,
4
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 38704,
"seconds": 12.0,
"avg_fps": 3225.275,
"low_1pct_fps": 505.335,
"low_01pct_fps": 124.874,
"p50_ms": 0.283,
"p95_ms": 0.36,
"p99_ms": 0.909,
"p999_ms": 2.015,
"max_ms": 132.801,
"histogram": [
38385,
279,
25,
3,
2,
0,
1,
0,
2,
4,
1,
0,
0,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 7609,
"seconds": 12.002,
"avg_fps": 633.984,
"low_1pct_fps": 58.513,
"low_01pct_fps": 22.539,
"p50_ms": 1.141,
"p95_ms": 4.438,
"p99_ms": 9.113,
"p999_ms": 27.496,
"max_ms": 64.424,
"histogram": [
3452,
2398,
956,
368,
143,
96,
61,
46,
20,
29,
6,
10,
6,
7,
7,
0,
1,
3,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 10168,
"seconds": 12.423,
"avg_fps": 818.512,
"low_1pct_fps": 11.586,
"low_01pct_fps": 1.461,
"p50_ms": 0.267,
"p95_ms": 0.892,
"p99_ms": 3.176,
"p999_ms": 198.757,
"max_ms": 1882.64,
"histogram": [
9733,
205,
121,
49,
12,
9,
8,
2,
2,
0,
1,
2,
1,
3,
0,
3,
2,
0,
0,
2,
4,
5,
1,
3
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 33679,
"seconds": 12.0,
"avg_fps": 2806.557,
"low_1pct_fps": 400.567,
"low_01pct_fps": 103.864,
"p50_ms": 0.324,
"p95_ms": 0.411,
"p99_ms": 1.167,
"p999_ms": 3.508,
"max_ms": 83.572,
"histogram": [
33272,
315,
46,
22,
2,
1,
4,
2,
9,
1,
2,
2,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 20188,
"seconds": 12.0,
"avg_fps": 1682.292,
"low_1pct_fps": 175.594,
"low_01pct_fps": 51.28,
"p50_ms": 0.374,
"p95_ms": 1.37,
"p99_ms": 2.821,
"p999_ms": 8.947,
"max_ms": 53.494,
"histogram": [
18109,
1592,
314,
83,
36,
19,
7,
6,
6,
1,
3,
1,
4,
2,
3,
0,
1,
1,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-desktop"
}
-787
View File
@@ -1,787 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-04T15:46:25Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "linux",
"arch": "x86_64",
"cpu_threads": 24,
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.57)",
"cpu": "13th Gen Intel(R) Core(TM) i7-13700KF",
"ram_mb": 31904,
"gpu": {
"name": "NVIDIA GeForce RTX 4080",
"vendor": "NVIDIA",
"driver": "1.4.329 NVIDIA 595.71.05",
"backend": "vulkan",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 28.799,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 112742.179,
"stdev": 27885.804,
"min": 81110.692,
"max": 133773.748,
"ci95": 69277.769
},
"startup.world_ms": {
"n": 3,
"mean": 235049.179,
"stdev": 31461.958,
"min": 203486.748,
"max": 266409.692,
"ci95": 78162.144
},
"chunks.village.load_ms": {
"n": 3,
"mean": 79828.05,
"stdev": 109107.365,
"min": 16685.972,
"max": 205814.266,
"ci95": 271059.597
},
"fps.village.avg_fps": {
"n": 3,
"mean": 525.667,
"stdev": 390.505,
"min": 101.073,
"max": 869.433,
"ci95": 970.146
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 44.209,
"stdev": 39.491,
"min": 1.644,
"max": 79.658,
"ci95": 98.109
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 10.635,
"stdev": 9.759,
"min": 0.686,
"max": 20.192,
"ci95": 24.244
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 34.333,
"stdev": 36.95,
"min": 13.0,
"max": 77.0,
"ci95": 91.797
},
"memory.village.rss_mb": {
"n": 3,
"mean": 3296.229,
"stdev": 296.464,
"min": 2956.234,
"max": 3500.773,
"ci95": 736.517
},
"tick.village.client_ms": {
"n": 3,
"mean": 1.23,
"stdev": 0.449,
"min": 0.897,
"max": 1.74,
"ci95": 1.114
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 61172.527,
"stdev": 17434.762,
"min": 46779.931,
"max": 80559.429,
"ci95": 43313.846
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 599.408,
"stdev": 492.96,
"min": 64.173,
"max": 1034.813,
"ci95": 1224.68
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 61.923,
"stdev": 56.534,
"min": 0.737,
"max": 112.221,
"ci95": 140.449
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 15.771,
"stdev": 14.11,
"min": 0.098,
"max": 27.463,
"ci95": 35.054
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 34.333,
"stdev": 36.95,
"min": 13.0,
"max": 77.0,
"ci95": 91.797
},
"memory.forest.rss_mb": {
"n": 3,
"mean": 3278.964,
"stdev": 301.465,
"min": 2947.268,
"max": 3536.27,
"ci95": 748.941
},
"tick.forest.client_ms": {
"n": 3,
"mean": 5.908,
"stdev": 8.813,
"min": 0.693,
"max": 16.084,
"ci95": 21.895
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 61762.098,
"stdev": 3569.17,
"min": 57919.097,
"max": 64972.973,
"ci95": 8867.026
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1175.044,
"stdev": 131.94,
"min": 1093.801,
"max": 1327.281,
"ci95": 327.785
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 119.758,
"stdev": 68.419,
"min": 77.252,
"max": 198.683,
"ci95": 169.976
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 24.224,
"stdev": 18.982,
"min": 12.308,
"max": 46.114,
"ci95": 47.159
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 34.333,
"stdev": 36.95,
"min": 13.0,
"max": 77.0,
"ci95": 91.797
},
"memory.ocean.rss_mb": {
"n": 3,
"mean": 3275.349,
"stdev": 317.864,
"min": 2924.113,
"max": 3543.23,
"ci95": 789.681
},
"tick.ocean.client_ms": {
"n": 3,
"mean": 0.654,
"stdev": 0.066,
"min": 0.579,
"max": 0.699,
"ci95": 0.163
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 113439.529,
"stdev": 64746.162,
"min": 55046.887,
"max": 183068.386,
"ci95": 160851.363
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 485.832,
"stdev": 317.254,
"min": 121.312,
"max": 699.617,
"ci95": 788.165
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 28.612,
"stdev": 23.334,
"min": 2.229,
"max": 46.542,
"ci95": 57.97
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 4.56,
"stdev": 3.408,
"min": 0.75,
"max": 7.318,
"ci95": 8.467
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 34.333,
"stdev": 36.95,
"min": 13.0,
"max": 77.0,
"ci95": 91.797
},
"memory.mountain.rss_mb": {
"n": 3,
"mean": 3248.708,
"stdev": 480.962,
"min": 2700.645,
"max": 3600.484,
"ci95": 1194.873
},
"tick.mountain.client_ms": {
"n": 3,
"mean": 1.832,
"stdev": 2.076,
"min": 0.618,
"max": 4.229,
"ci95": 5.157
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 3371.958,
"stdev": 280.347,
"min": 3059.926,
"max": 3602.613,
"ci95": 696.476
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 3291.249,
"stdev": 316.441,
"min": 2935.975,
"max": 3542.848,
"ci95": 786.147
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 1248,
"seconds": 12.348,
"avg_fps": 101.073,
"low_1pct_fps": 1.644,
"low_01pct_fps": 0.686,
"p50_ms": 1.225,
"p95_ms": 11.161,
"p99_ms": 226.597,
"p999_ms": 1457.605,
"max_ms": 1457.874,
"histogram": [
548,
232,
97,
118,
51,
49,
24,
39,
16,
17,
13,
6,
3,
2,
5,
3,
2,
2,
4,
2,
3,
8,
1,
3
]
},
{
"phase": "village",
"repeat": 1,
"frames": 10435,
"seconds": 12.002,
"avg_fps": 869.433,
"low_1pct_fps": 79.658,
"low_01pct_fps": 20.192,
"p50_ms": 0.813,
"p95_ms": 2.537,
"p99_ms": 5.685,
"p999_ms": 13.292,
"max_ms": 351.251,
"histogram": [
7598,
2071,
393,
171,
55,
53,
22,
24,
20,
10,
9,
2,
2,
1,
3,
0,
0,
0,
0,
0,
0,
1,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 7278,
"seconds": 12.0,
"avg_fps": 606.495,
"low_1pct_fps": 51.326,
"low_01pct_fps": 11.026,
"p50_ms": 1.105,
"p95_ms": 3.854,
"p99_ms": 7.067,
"p999_ms": 24.484,
"max_ms": 443.95,
"histogram": [
3206,
2621,
800,
360,
112,
71,
34,
26,
12,
13,
3,
5,
2,
7,
2,
1,
1,
0,
1,
0,
0,
1,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 975,
"seconds": 15.193,
"avg_fps": 64.173,
"low_1pct_fps": 0.737,
"low_01pct_fps": 0.098,
"p50_ms": 0.864,
"p95_ms": 5.083,
"p99_ms": 77.689,
"p999_ms": 10225.237,
"max_ms": 10225.237,
"histogram": [
680,
99,
104,
12,
30,
13,
8,
4,
2,
3,
2,
1,
1,
3,
0,
1,
0,
1,
2,
3,
1,
2,
1,
2
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 8391,
"seconds": 12.0,
"avg_fps": 699.237,
"low_1pct_fps": 72.811,
"low_01pct_fps": 19.752,
"p50_ms": 1.087,
"p95_ms": 2.927,
"p99_ms": 5.909,
"p999_ms": 15.268,
"max_ms": 252.557,
"histogram": [
3342,
3998,
656,
231,
56,
26,
15,
19,
12,
12,
11,
6,
1,
3,
1,
0,
0,
1,
0,
0,
0,
1,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 12418,
"seconds": 12.0,
"avg_fps": 1034.813,
"low_1pct_fps": 112.221,
"low_01pct_fps": 27.463,
"p50_ms": 0.837,
"p95_ms": 1.394,
"p99_ms": 3.491,
"p999_ms": 13.217,
"max_ms": 245.59,
"histogram": [
11155,
1004,
100,
64,
29,
16,
16,
10,
2,
6,
5,
3,
4,
2,
0,
1,
0,
0,
0,
0,
1,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 13126,
"seconds": 12.0,
"avg_fps": 1093.801,
"low_1pct_fps": 77.252,
"low_01pct_fps": 12.308,
"p50_ms": 0.714,
"p95_ms": 1.402,
"p99_ms": 2.875,
"p999_ms": 12.067,
"max_ms": 238.158,
"histogram": [
11605,
1207,
191,
54,
17,
11,
7,
12,
4,
4,
2,
3,
2,
1,
0,
0,
0,
0,
1,
2,
3,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 15928,
"seconds": 12.0,
"avg_fps": 1327.281,
"low_1pct_fps": 198.683,
"low_01pct_fps": 46.114,
"p50_ms": 0.67,
"p95_ms": 1.172,
"p99_ms": 2.111,
"p999_ms": 7.067,
"max_ms": 193.743,
"histogram": [
14891,
856,
102,
42,
7,
6,
8,
6,
0,
5,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 13249,
"seconds": 12.0,
"avg_fps": 1104.051,
"low_1pct_fps": 83.338,
"low_01pct_fps": 14.249,
"p50_ms": 0.711,
"p95_ms": 1.403,
"p99_ms": 3.046,
"p999_ms": 10.625,
"max_ms": 451.869,
"histogram": [
11824,
1165,
124,
58,
13,
16,
11,
14,
7,
7,
3,
2,
1,
0,
0,
0,
1,
0,
0,
1,
1,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 1469,
"seconds": 12.109,
"avg_fps": 121.312,
"low_1pct_fps": 2.229,
"low_01pct_fps": 0.75,
"p50_ms": 2.12,
"p95_ms": 13.173,
"p99_ms": 125.189,
"p999_ms": 727.045,
"max_ms": 1939.723,
"histogram": [
196,
440,
438,
136,
50,
36,
24,
25,
22,
17,
17,
13,
6,
11,
8,
4,
1,
4,
4,
4,
3,
7,
2,
1
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 8396,
"seconds": 12.001,
"avg_fps": 699.617,
"low_1pct_fps": 46.542,
"low_01pct_fps": 7.318,
"p50_ms": 1.022,
"p95_ms": 2.433,
"p99_ms": 5.528,
"p999_ms": 13.993,
"max_ms": 305.464,
"histogram": [
3248,
4509,
361,
113,
52,
37,
21,
26,
14,
4,
3,
2,
0,
0,
0,
0,
0,
0,
1,
1,
2,
2,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 7639,
"seconds": 12.0,
"avg_fps": 636.566,
"low_1pct_fps": 37.064,
"low_01pct_fps": 5.612,
"p50_ms": 1.023,
"p95_ms": 3.0,
"p99_ms": 7.11,
"p999_ms": 17.521,
"max_ms": 268.877,
"histogram": [
3051,
3794,
413,
158,
66,
43,
35,
21,
31,
12,
3,
4,
1,
1,
0,
0,
0,
0,
0,
0,
4,
2,
0,
0
]
}
],
"label": "vanilla-desktop"
}
@@ -1,707 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T22:55:52Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "Apple",
"driver": "4.1 Metal - 91.7",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 75.854,
"window": [
1920,
930
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 19419.646,
"stdev": 10653.755,
"min": 8240.178,
"max": 29455.403,
"ci95": 26467.531
},
"startup.world_ms": {
"n": 3,
"mean": 78944.646,
"stdev": 36737.011,
"min": 51495.357,
"max": 120678.403,
"ci95": 91267.16
},
"chunks.village.load_ms": {
"n": 3,
"mean": 21947.683,
"stdev": 10432.774,
"min": 10427.451,
"max": 30758.368,
"ci95": 25918.54
},
"fps.village.avg_fps": {
"n": 3,
"mean": 45.524,
"stdev": 44.352,
"min": 15.749,
"max": 96.497,
"ci95": 110.185
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 9.141,
"stdev": 10.812,
"min": 1.86,
"max": 21.565,
"ci95": 26.862
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 6.727,
"stdev": 6.902,
"min": 1.785,
"max": 14.612,
"ci95": 17.146
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 39062.839,
"stdev": 21806.234,
"min": 15037.908,
"max": 57603.235,
"ci95": 54174.061
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 23.347,
"stdev": 3.693,
"min": 20.154,
"max": 27.392,
"ci95": 9.175
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 2.419,
"stdev": 0.269,
"min": 2.196,
"max": 2.718,
"ci95": 0.669
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 1.972,
"stdev": 0.438,
"min": 1.551,
"max": 2.425,
"ci95": 1.088
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 22097.713,
"stdev": 4463.742,
"min": 18053.319,
"max": 26887.066,
"ci95": 11089.445
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 51.279,
"stdev": 6.309,
"min": 44.786,
"max": 57.386,
"ci95": 15.673
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 6.15,
"stdev": 3.128,
"min": 3.444,
"max": 9.574,
"ci95": 7.77
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 4.161,
"stdev": 2.739,
"min": 2.487,
"max": 7.321,
"ci95": 6.803
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 50257.39,
"stdev": 34798.619,
"min": 25919.598,
"max": 90115.595,
"ci95": 86451.54
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 44.346,
"stdev": 4.882,
"min": 39.246,
"max": 48.975,
"ci95": 12.128
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 6.637,
"stdev": 0.69,
"min": 5.888,
"max": 7.246,
"ci95": 1.714
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 4.444,
"stdev": 0.6,
"min": 3.937,
"max": 5.107,
"ci95": 1.491
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2554.552,
"stdev": 218.163,
"min": 2328.859,
"max": 2764.313,
"ci95": 541.991
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2430.854,
"stdev": 230.654,
"min": 2186.328,
"max": 2644.531,
"ci95": 573.023
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 3713.311,
"stdev": 191.61,
"min": 3512.331,
"max": 3893.925,
"ci95": 476.025
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 3450.061,
"stdev": 76.233,
"min": 3370.253,
"max": 3522.128,
"ci95": 189.388
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 189,
"seconds": 12.001,
"avg_fps": 15.749,
"low_1pct_fps": 1.86,
"low_01pct_fps": 1.785,
"p50_ms": 22.219,
"p95_ms": 254.854,
"p99_ms": 515.134,
"p999_ms": 560.079,
"max_ms": 560.079,
"histogram": [
0,
0,
0,
1,
2,
2,
7,
9,
10,
16,
12,
11,
14,
16,
22,
9,
6,
5,
11,
10,
15,
9,
2,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 292,
"seconds": 12.004,
"avg_fps": 24.325,
"low_1pct_fps": 3.998,
"low_01pct_fps": 3.783,
"p50_ms": 15.477,
"p95_ms": 165.164,
"p99_ms": 242.898,
"p999_ms": 264.363,
"max_ms": 264.363,
"histogram": [
0,
0,
0,
0,
4,
26,
20,
32,
20,
20,
17,
12,
12,
10,
17,
14,
20,
14,
16,
16,
21,
1,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 1158,
"seconds": 12.0,
"avg_fps": 96.497,
"low_1pct_fps": 21.565,
"low_01pct_fps": 14.612,
"p50_ms": 7.567,
"p95_ms": 22.849,
"p99_ms": 34.806,
"p999_ms": 62.86,
"max_ms": 74.011,
"histogram": [
0,
0,
0,
0,
1,
38,
452,
158,
102,
94,
125,
68,
34,
42,
30,
7,
4,
2,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 271,
"seconds": 12.047,
"avg_fps": 22.496,
"low_1pct_fps": 2.196,
"low_01pct_fps": 1.551,
"p50_ms": 18.263,
"p95_ms": 189.284,
"p99_ms": 317.823,
"p999_ms": 644.675,
"max_ms": 644.675,
"histogram": [
0,
0,
0,
0,
0,
3,
22,
23,
13,
22,
22,
19,
24,
22,
14,
17,
12,
13,
9,
16,
15,
4,
1,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 242,
"seconds": 12.007,
"avg_fps": 20.154,
"low_1pct_fps": 2.343,
"low_01pct_fps": 1.939,
"p50_ms": 19.762,
"p95_ms": 157.949,
"p99_ms": 376.292,
"p999_ms": 515.678,
"max_ms": 515.678,
"histogram": [
0,
0,
0,
0,
0,
13,
17,
16,
26,
19,
11,
9,
10,
11,
9,
16,
17,
12,
21,
19,
9,
6,
1,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 331,
"seconds": 12.084,
"avg_fps": 27.392,
"low_1pct_fps": 2.718,
"low_01pct_fps": 2.425,
"p50_ms": 13.0,
"p95_ms": 162.797,
"p99_ms": 325.827,
"p999_ms": 412.349,
"max_ms": 412.349,
"histogram": [
0,
0,
0,
0,
3,
5,
26,
34,
42,
41,
24,
27,
15,
21,
22,
8,
10,
11,
10,
14,
10,
8,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 620,
"seconds": 12.001,
"avg_fps": 51.664,
"low_1pct_fps": 3.444,
"low_01pct_fps": 2.487,
"p50_ms": 10.358,
"p95_ms": 54.808,
"p99_ms": 181.112,
"p999_ms": 402.152,
"max_ms": 402.152,
"histogram": [
0,
0,
0,
0,
0,
30,
79,
95,
86,
90,
60,
54,
28,
28,
19,
9,
8,
8,
6,
8,
7,
5,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 689,
"seconds": 12.006,
"avg_fps": 57.386,
"low_1pct_fps": 9.574,
"low_01pct_fps": 7.321,
"p50_ms": 12.76,
"p95_ms": 43.737,
"p99_ms": 69.357,
"p999_ms": 136.592,
"max_ms": 136.592,
"histogram": [
0,
0,
0,
0,
0,
23,
46,
80,
80,
92,
68,
74,
54,
49,
51,
24,
22,
16,
6,
4,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 538,
"seconds": 12.013,
"avg_fps": 44.786,
"low_1pct_fps": 5.431,
"low_01pct_fps": 2.674,
"p50_ms": 14.011,
"p95_ms": 71.23,
"p99_ms": 141.423,
"p999_ms": 373.913,
"max_ms": 373.913,
"histogram": [
0,
0,
0,
0,
0,
1,
8,
38,
84,
83,
53,
62,
52,
54,
29,
12,
15,
13,
22,
10,
1,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 588,
"seconds": 12.006,
"avg_fps": 48.975,
"low_1pct_fps": 6.777,
"low_01pct_fps": 3.937,
"p50_ms": 12.678,
"p95_ms": 64.221,
"p99_ms": 108.513,
"p999_ms": 254.008,
"max_ms": 254.008,
"histogram": [
0,
0,
0,
0,
4,
34,
21,
41,
108,
62,
55,
55,
40,
45,
41,
21,
16,
17,
18,
9,
0,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 471,
"seconds": 12.001,
"avg_fps": 39.246,
"low_1pct_fps": 5.888,
"low_01pct_fps": 4.289,
"p50_ms": 17.65,
"p95_ms": 73.314,
"p99_ms": 135.3,
"p999_ms": 233.136,
"max_ms": 233.136,
"histogram": [
0,
0,
0,
0,
3,
12,
22,
29,
36,
40,
43,
38,
41,
46,
62,
29,
20,
19,
22,
5,
4,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 538,
"seconds": 12.004,
"avg_fps": 44.818,
"low_1pct_fps": 7.246,
"low_01pct_fps": 5.107,
"p50_ms": 15.959,
"p95_ms": 63.485,
"p99_ms": 109.639,
"p999_ms": 195.815,
"max_ms": 195.815,
"histogram": [
0,
0,
0,
0,
0,
4,
6,
21,
75,
56,
54,
78,
54,
69,
36,
23,
22,
18,
14,
7,
1,
0,
0,
0
]
}
],
"label": "vanilla-m4pro-1080p"
}
@@ -1,707 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T23:17:23Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "Apple",
"driver": "4.1 Metal - 91.7",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 58.672,
"window": [
1920,
930
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 20579.57,
"stdev": 13060.035,
"min": 10660.541,
"max": 35376.471,
"ci95": 32445.545
},
"startup.world_ms": {
"n": 3,
"mean": 124180.237,
"stdev": 102723.197,
"min": 51510.541,
"max": 241702.471,
"ci95": 255199.163
},
"chunks.village.load_ms": {
"n": 3,
"mean": 23389.087,
"stdev": 5416.249,
"min": 18538.934,
"max": 29233.632,
"ci95": 13455.795
},
"fps.village.avg_fps": {
"n": 3,
"mean": 81.421,
"stdev": 39.944,
"min": 35.32,
"max": 105.706,
"ci95": 99.234
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 15.288,
"stdev": 8.185,
"min": 5.907,
"max": 20.97,
"ci95": 20.334
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 10.544,
"stdev": 4.403,
"min": 5.804,
"max": 14.505,
"ci95": 10.938
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 48382.741,
"stdev": 32005.955,
"min": 26161.003,
"max": 85067.56,
"ci95": 79513.616
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 67.82,
"stdev": 49.16,
"min": 12.722,
"max": 107.193,
"ci95": 122.129
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 17.769,
"stdev": 19.765,
"min": 2.609,
"max": 40.124,
"ci95": 49.103
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 13.876,
"stdev": 16.851,
"min": 2.308,
"max": 33.21,
"ci95": 41.864
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 37630.797,
"stdev": 33094.177,
"min": 14901.304,
"max": 75599.155,
"ci95": 82217.128
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 44.125,
"stdev": 32.286,
"min": 13.532,
"max": 77.872,
"ci95": 80.209
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 4.619,
"stdev": 1.694,
"min": 3.247,
"max": 6.512,
"ci95": 4.208
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 2.354,
"stdev": 0.44,
"min": 1.967,
"max": 2.833,
"ci95": 1.094
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 67242.203,
"stdev": 17886.115,
"min": 56354.348,
"max": 87884.948,
"ci95": 44435.159
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 28.745,
"stdev": 7.77,
"min": 20.237,
"max": 35.466,
"ci95": 19.304
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 3.013,
"stdev": 0.849,
"min": 2.273,
"max": 3.94,
"ci95": 2.109
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 2.377,
"stdev": 1.031,
"min": 1.246,
"max": 3.263,
"ci95": 2.561
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2958.083,
"stdev": 22.425,
"min": 2937.734,
"max": 2982.125,
"ci95": 55.71
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2837.568,
"stdev": 165.159,
"min": 2648.188,
"max": 2951.719,
"ci95": 410.31
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 4618.337,
"stdev": 95.349,
"min": 4543.347,
"max": 4725.644,
"ci95": 236.878
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 4264.652,
"stdev": 87.422,
"min": 4173.589,
"max": 4347.91,
"ci95": 217.185
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 424,
"seconds": 12.004,
"avg_fps": 35.32,
"low_1pct_fps": 5.907,
"low_01pct_fps": 5.804,
"p50_ms": 17.195,
"p95_ms": 95.694,
"p99_ms": 164.6,
"p999_ms": 172.292,
"max_ms": 172.292,
"histogram": [
0,
0,
0,
0,
0,
0,
5,
19,
39,
45,
44,
52,
42,
41,
38,
21,
20,
21,
19,
12,
6,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 1239,
"seconds": 12.001,
"avg_fps": 103.238,
"low_1pct_fps": 20.97,
"low_01pct_fps": 14.505,
"p50_ms": 8.073,
"p95_ms": 18.618,
"p99_ms": 34.704,
"p999_ms": 53.789,
"max_ms": 84.091,
"histogram": [
0,
0,
0,
0,
0,
2,
275,
412,
260,
117,
60,
35,
25,
23,
14,
7,
5,
3,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 1271,
"seconds": 12.024,
"avg_fps": 105.706,
"low_1pct_fps": 18.988,
"low_01pct_fps": 11.324,
"p50_ms": 7.988,
"p95_ms": 16.852,
"p99_ms": 35.156,
"p999_ms": 65.46,
"max_ms": 111.155,
"histogram": [
0,
0,
0,
0,
0,
3,
220,
500,
318,
101,
43,
21,
15,
19,
17,
4,
4,
5,
0,
1,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 154,
"seconds": 12.105,
"avg_fps": 12.722,
"low_1pct_fps": 2.609,
"low_01pct_fps": 2.308,
"p50_ms": 54.156,
"p95_ms": 216.738,
"p99_ms": 333.238,
"p999_ms": 433.205,
"max_ms": 433.205,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
1,
4,
5,
7,
9,
13,
25,
5,
7,
12,
17,
26,
18,
5,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 1287,
"seconds": 12.006,
"avg_fps": 107.193,
"low_1pct_fps": 40.124,
"low_01pct_fps": 33.21,
"p50_ms": 8.355,
"p95_ms": 14.243,
"p99_ms": 22.791,
"p999_ms": 29.023,
"max_ms": 31.2,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
627,
416,
130,
43,
33,
16,
18,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 1003,
"seconds": 12.005,
"avg_fps": 83.546,
"low_1pct_fps": 10.575,
"low_01pct_fps": 6.111,
"p50_ms": 9.556,
"p95_ms": 22.218,
"p99_ms": 53.64,
"p999_ms": 143.879,
"max_ms": 183.38,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
57,
515,
239,
64,
45,
21,
30,
12,
4,
4,
5,
3,
3,
1,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 163,
"seconds": 12.045,
"avg_fps": 13.532,
"low_1pct_fps": 3.247,
"low_01pct_fps": 2.833,
"p50_ms": 52.758,
"p95_ms": 208.496,
"p99_ms": 263.074,
"p999_ms": 352.963,
"max_ms": 352.963,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
1,
2,
4,
5,
5,
14,
23,
10,
12,
17,
30,
18,
20,
2,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 951,
"seconds": 12.212,
"avg_fps": 77.872,
"low_1pct_fps": 6.512,
"low_01pct_fps": 1.967,
"p50_ms": 8.895,
"p95_ms": 31.377,
"p99_ms": 72.053,
"p999_ms": 508.284,
"max_ms": 508.284,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
359,
306,
104,
47,
29,
31,
16,
18,
10,
9,
10,
6,
2,
3,
0,
1,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 495,
"seconds": 12.082,
"avg_fps": 40.97,
"low_1pct_fps": 4.097,
"low_01pct_fps": 2.261,
"p50_ms": 11.995,
"p95_ms": 80.625,
"p99_ms": 146.758,
"p999_ms": 442.316,
"max_ms": 442.316,
"histogram": [
0,
0,
0,
0,
0,
0,
3,
80,
109,
56,
31,
37,
24,
30,
28,
17,
17,
27,
24,
8,
2,
2,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 243,
"seconds": 12.008,
"avg_fps": 20.237,
"low_1pct_fps": 2.827,
"low_01pct_fps": 2.623,
"p50_ms": 21.487,
"p95_ms": 177.814,
"p99_ms": 315.716,
"p999_ms": 381.289,
"max_ms": 381.289,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
5,
20,
26,
24,
32,
25,
21,
8,
19,
13,
18,
15,
10,
7,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 431,
"seconds": 12.153,
"avg_fps": 35.466,
"low_1pct_fps": 2.273,
"low_01pct_fps": 1.246,
"p50_ms": 12.232,
"p95_ms": 106.955,
"p99_ms": 249.209,
"p999_ms": 802.438,
"max_ms": 802.438,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
66,
137,
62,
41,
21,
23,
20,
3,
14,
11,
11,
10,
8,
2,
2,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 367,
"seconds": 12.02,
"avg_fps": 30.533,
"low_1pct_fps": 3.94,
"low_01pct_fps": 3.263,
"p50_ms": 21.354,
"p95_ms": 104.182,
"p99_ms": 218.448,
"p999_ms": 306.483,
"max_ms": 306.483,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
4,
52,
38,
32,
44,
53,
47,
20,
21,
19,
16,
15,
4,
2,
0,
0
]
}
],
"label": "vanilla-m4pro-1080p"
}
@@ -1,707 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T22:55:52Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "Apple",
"driver": "4.1 Metal - 91.7",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 66.477,
"window": [
1920,
930
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 49895.61,
"stdev": 37830.08,
"min": 27410.441,
"max": 93571.631,
"ci95": 93982.713
},
"startup.world_ms": {
"n": 3,
"mean": 89080.61,
"stdev": 49418.222,
"min": 59243.441,
"max": 146123.631,
"ci95": 122771.577
},
"chunks.village.load_ms": {
"n": 3,
"mean": 29280.957,
"stdev": 15620.91,
"min": 18681.538,
"max": 47219.974,
"ci95": 38807.625
},
"fps.village.avg_fps": {
"n": 3,
"mean": 42.245,
"stdev": 6.241,
"min": 36.416,
"max": 48.83,
"ci95": 15.506
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 4.137,
"stdev": 1.2,
"min": 3.03,
"max": 5.412,
"ci95": 2.981
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 2.891,
"stdev": 0.196,
"min": 2.777,
"max": 3.118,
"ci95": 0.488
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 16212.425,
"stdev": 1584.155,
"min": 14800.702,
"max": 17925.672,
"ci95": 3935.576
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 66.392,
"stdev": 8.735,
"min": 57.65,
"max": 75.119,
"ci95": 21.699
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 8.327,
"stdev": 3.862,
"min": 6.012,
"max": 12.786,
"ci95": 9.595
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 6.154,
"stdev": 3.399,
"min": 3.873,
"max": 10.06,
"ci95": 8.444
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 9054.345,
"stdev": 1795.805,
"min": 7429.882,
"max": 10982.71,
"ci95": 4461.386
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 49.891,
"stdev": 17.901,
"min": 29.637,
"max": 63.591,
"ci95": 44.472
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 6.144,
"stdev": 5.262,
"min": 2.948,
"max": 12.218,
"ci95": 13.074
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 5.254,
"stdev": 4.377,
"min": 2.603,
"max": 10.306,
"ci95": 10.874
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 18171.415,
"stdev": 5234.921,
"min": 12170.39,
"max": 21800.551,
"ci95": 13005.314
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 45.9,
"stdev": 14.757,
"min": 29.055,
"max": 56.549,
"ci95": 36.661
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 4.83,
"stdev": 2.815,
"min": 2.104,
"max": 7.727,
"ci95": 6.994
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 3.986,
"stdev": 2.385,
"min": 1.597,
"max": 6.366,
"ci95": 5.924
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1877.276,
"stdev": 149.19,
"min": 1724.094,
"max": 2022.125,
"ci95": 370.639
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1810.693,
"stdev": 157.376,
"min": 1671.984,
"max": 1981.719,
"ci95": 390.975
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 2708.649,
"stdev": 200.476,
"min": 2538.472,
"max": 2929.644,
"ci95": 498.051
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 2635.498,
"stdev": 241.221,
"min": 2402.909,
"max": 2884.511,
"ci95": 599.274
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 586,
"seconds": 12.001,
"avg_fps": 48.83,
"low_1pct_fps": 3.03,
"low_01pct_fps": 2.779,
"p50_ms": 6.298,
"p95_ms": 102.59,
"p99_ms": 300.171,
"p999_ms": 359.883,
"max_ms": 359.883,
"histogram": [
0,
0,
30,
105,
67,
75,
58,
68,
43,
24,
14,
15,
8,
4,
8,
4,
7,
8,
18,
11,
11,
8,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 498,
"seconds": 12.003,
"avg_fps": 41.489,
"low_1pct_fps": 5.412,
"low_01pct_fps": 3.118,
"p50_ms": 14.331,
"p95_ms": 81.666,
"p99_ms": 133.706,
"p999_ms": 320.73,
"max_ms": 320.73,
"histogram": [
0,
0,
0,
5,
15,
23,
21,
37,
43,
41,
55,
45,
50,
45,
30,
15,
18,
18,
21,
13,
2,
1,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 437,
"seconds": 12.0,
"avg_fps": 36.416,
"low_1pct_fps": 3.97,
"low_01pct_fps": 2.777,
"p50_ms": 12.279,
"p95_ms": 133.385,
"p99_ms": 203.287,
"p999_ms": 360.113,
"max_ms": 360.113,
"histogram": [
0,
0,
0,
9,
28,
21,
19,
41,
52,
41,
31,
33,
27,
25,
27,
13,
13,
17,
12,
14,
12,
2,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 797,
"seconds": 12.002,
"avg_fps": 66.407,
"low_1pct_fps": 6.184,
"low_01pct_fps": 4.528,
"p50_ms": 7.208,
"p95_ms": 64.408,
"p99_ms": 126.005,
"p999_ms": 220.846,
"max_ms": 220.846,
"histogram": [
0,
0,
2,
22,
78,
137,
123,
136,
77,
58,
31,
14,
13,
14,
13,
10,
11,
21,
20,
13,
4,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 692,
"seconds": 12.003,
"avg_fps": 57.65,
"low_1pct_fps": 6.012,
"low_01pct_fps": 3.873,
"p50_ms": 10.783,
"p95_ms": 48.101,
"p99_ms": 121.963,
"p999_ms": 258.167,
"max_ms": 258.167,
"histogram": [
0,
0,
0,
13,
52,
52,
41,
62,
83,
88,
50,
46,
40,
50,
43,
20,
23,
9,
11,
6,
2,
1,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 902,
"seconds": 12.008,
"avg_fps": 75.119,
"low_1pct_fps": 12.786,
"low_01pct_fps": 10.06,
"p50_ms": 9.096,
"p95_ms": 38.051,
"p99_ms": 67.18,
"p999_ms": 99.405,
"max_ms": 99.405,
"histogram": [
0,
0,
0,
26,
115,
72,
65,
100,
118,
90,
66,
51,
54,
50,
39,
16,
17,
13,
10,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 687,
"seconds": 12.171,
"avg_fps": 56.446,
"low_1pct_fps": 3.267,
"low_01pct_fps": 2.853,
"p50_ms": 4.938,
"p95_ms": 103.639,
"p99_ms": 257.27,
"p999_ms": 350.504,
"max_ms": 350.504,
"histogram": [
0,
0,
28,
199,
125,
99,
51,
35,
32,
18,
5,
7,
11,
10,
11,
6,
2,
4,
7,
14,
15,
8,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 764,
"seconds": 12.014,
"avg_fps": 63.591,
"low_1pct_fps": 12.218,
"low_01pct_fps": 10.306,
"p50_ms": 11.549,
"p95_ms": 45.214,
"p99_ms": 67.493,
"p999_ms": 97.034,
"max_ms": 97.034,
"histogram": [
0,
0,
0,
9,
45,
41,
59,
58,
87,
101,
71,
85,
42,
56,
49,
15,
16,
22,
8,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 357,
"seconds": 12.046,
"avg_fps": 29.637,
"low_1pct_fps": 2.948,
"low_01pct_fps": 2.603,
"p50_ms": 10.947,
"p95_ms": 173.526,
"p99_ms": 311.134,
"p999_ms": 384.135,
"max_ms": 384.135,
"histogram": [
0,
0,
0,
11,
47,
31,
12,
34,
32,
23,
15,
26,
21,
14,
15,
7,
6,
16,
18,
6,
15,
8,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 355,
"seconds": 12.218,
"avg_fps": 29.055,
"low_1pct_fps": 2.104,
"low_01pct_fps": 1.597,
"p50_ms": 7.766,
"p95_ms": 145.251,
"p99_ms": 368.51,
"p999_ms": 626.205,
"max_ms": 626.205,
"histogram": [
0,
0,
41,
53,
25,
28,
15,
22,
7,
16,
10,
15,
17,
12,
15,
11,
11,
8,
14,
19,
7,
7,
2,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 680,
"seconds": 12.025,
"avg_fps": 56.549,
"low_1pct_fps": 7.727,
"low_01pct_fps": 6.366,
"p50_ms": 11.633,
"p95_ms": 52.508,
"p99_ms": 111.848,
"p999_ms": 157.09,
"max_ms": 157.09,
"histogram": [
0,
0,
1,
32,
38,
43,
49,
71,
55,
55,
54,
57,
53,
41,
54,
21,
19,
17,
12,
7,
1,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 632,
"seconds": 12.132,
"avg_fps": 52.095,
"low_1pct_fps": 4.66,
"low_01pct_fps": 3.995,
"p50_ms": 8.601,
"p95_ms": 78.611,
"p99_ms": 172.405,
"p999_ms": 250.342,
"max_ms": 250.342,
"histogram": [
0,
0,
9,
75,
86,
53,
32,
57,
40,
41,
30,
38,
25,
24,
39,
16,
23,
7,
12,
15,
9,
1,
0,
0
]
}
],
"label": "vanilla-m4pro-1080p"
}
-771
View File
@@ -1,771 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-04T12:54:29Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "Apple",
"driver": "4.1 Metal - 91.7",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 12,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 17.628,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 12652.182,
"stdev": 3231.342,
"min": 9526.004,
"max": 15979.309,
"ci95": 8027.746
},
"startup.world_ms": {
"n": 3,
"mean": 31329.516,
"stdev": 10016.129,
"min": 20777.004,
"max": 40705.309,
"ci95": 24883.451
},
"chunks.village.load_ms": {
"n": 3,
"mean": 12623.084,
"stdev": 4958.724,
"min": 7251.986,
"max": 17026.894,
"ci95": 12319.147
},
"fps.village.avg_fps": {
"n": 3,
"mean": 92.729,
"stdev": 58.527,
"min": 28.713,
"max": 143.496,
"ci95": 145.402
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 16.52,
"stdev": 10.949,
"min": 4.001,
"max": 24.308,
"ci95": 27.2
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 11.515,
"stdev": 7.155,
"min": 3.549,
"max": 17.394,
"ci95": 17.774
},
"memory.village.rss_mb": {
"n": 3,
"mean": 1834.469,
"stdev": 329.198,
"min": 1455.688,
"max": 2051.516,
"ci95": 817.839
},
"tick.village.client_ms": {
"n": 3,
"mean": 1.903,
"stdev": 1.114,
"min": 1.131,
"max": 3.18,
"ci95": 2.767
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 14590.694,
"stdev": 3260.615,
"min": 10942.613,
"max": 17221.108,
"ci95": 8100.47
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 106.817,
"stdev": 104.558,
"min": 43.857,
"max": 227.513,
"ci95": 259.759
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 23.0,
"stdev": 26.394,
"min": 7.338,
"max": 53.473,
"ci95": 65.57
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 14.841,
"stdev": 15.773,
"min": 5.157,
"max": 33.042,
"ci95": 39.186
},
"memory.forest.rss_mb": {
"n": 3,
"mean": 1814.245,
"stdev": 154.974,
"min": 1635.453,
"max": 1910.125,
"ci95": 385.007
},
"tick.forest.client_ms": {
"n": 3,
"mean": 1.452,
"stdev": 0.542,
"min": 0.826,
"max": 1.777,
"ci95": 1.346
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 13309.049,
"stdev": 8088.157,
"min": 6394.403,
"max": 22203.194,
"ci95": 20093.716
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 164.405,
"stdev": 141.4,
"min": 62.162,
"max": 325.77,
"ci95": 351.285
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 36.242,
"stdev": 37.051,
"min": 14.736,
"max": 79.024,
"ci95": 92.047
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 20.898,
"stdev": 17.899,
"min": 8.855,
"max": 41.466,
"ci95": 44.468
},
"memory.ocean.rss_mb": {
"n": 3,
"mean": 1724.453,
"stdev": 347.692,
"min": 1323.5,
"max": 1942.734,
"ci95": 863.784
},
"tick.ocean.client_ms": {
"n": 3,
"mean": 0.866,
"stdev": 0.248,
"min": 0.58,
"max": 1.03,
"ci95": 0.617
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 17989.43,
"stdev": 3969.295,
"min": 13444.423,
"max": 20774.261,
"ci95": 9861.07
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 162.084,
"stdev": 160.057,
"min": 68.44,
"max": 346.896,
"ci95": 397.635
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 33.74,
"stdev": 36.146,
"min": 11.158,
"max": 75.429,
"ci95": 89.798
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 17.194,
"stdev": 14.232,
"min": 7.903,
"max": 33.579,
"ci95": 35.357
},
"memory.mountain.rss_mb": {
"n": 3,
"mean": 1977.602,
"stdev": 108.627,
"min": 1872.516,
"max": 2089.453,
"ci95": 269.866
},
"tick.mountain.client_ms": {
"n": 3,
"mean": 0.772,
"stdev": 0.332,
"min": 0.463,
"max": 1.123,
"ci95": 0.825
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2135.313,
"stdev": 54.617,
"min": 2072.344,
"max": 2169.828,
"ci95": 135.686
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1836.44,
"stdev": 223.694,
"min": 1578.141,
"max": 1965.781,
"ci95": 555.732
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 3253.456,
"stdev": 68.323,
"min": 3178.409,
"max": 3312.05,
"ci95": 169.737
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 3144.363,
"stdev": 69.868,
"min": 3069.394,
"max": 3207.66,
"ci95": 173.576
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 1272,
"seconds": 12.002,
"avg_fps": 105.979,
"low_1pct_fps": 21.25,
"low_01pct_fps": 17.394,
"p50_ms": 7.527,
"p95_ms": 22.34,
"p99_ms": 40.461,
"p999_ms": 54.28,
"max_ms": 60.7,
"histogram": [
0,
0,
0,
56,
161,
170,
180,
167,
184,
118,
71,
61,
26,
25,
28,
12,
10,
3,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 1723,
"seconds": 12.007,
"avg_fps": 143.496,
"low_1pct_fps": 24.308,
"low_01pct_fps": 13.602,
"p50_ms": 5.317,
"p95_ms": 16.181,
"p99_ms": 29.771,
"p999_ms": 56.832,
"max_ms": 90.209,
"histogram": [
0,
0,
63,
413,
271,
273,
185,
148,
112,
89,
47,
43,
28,
22,
18,
4,
5,
1,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 346,
"seconds": 12.05,
"avg_fps": 28.713,
"low_1pct_fps": 4.001,
"low_01pct_fps": 3.549,
"p50_ms": 23.684,
"p95_ms": 110.057,
"p99_ms": 225.236,
"p999_ms": 281.776,
"max_ms": 281.776,
"histogram": [
0,
0,
0,
4,
8,
6,
6,
12,
21,
20,
23,
25,
24,
32,
50,
28,
24,
27,
17,
11,
6,
2,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 589,
"seconds": 12.001,
"avg_fps": 49.081,
"low_1pct_fps": 8.19,
"low_01pct_fps": 6.325,
"p50_ms": 15.443,
"p95_ms": 53.004,
"p99_ms": 94.69,
"p999_ms": 158.114,
"max_ms": 158.114,
"histogram": [
0,
0,
0,
2,
7,
20,
30,
46,
53,
48,
56,
62,
61,
66,
63,
18,
22,
20,
10,
4,
1,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 2731,
"seconds": 12.004,
"avg_fps": 227.513,
"low_1pct_fps": 53.473,
"low_01pct_fps": 33.042,
"p50_ms": 3.801,
"p95_ms": 7.955,
"p99_ms": 14.177,
"p999_ms": 24.479,
"max_ms": 38.218,
"histogram": [
0,
0,
526,
1157,
432,
257,
151,
88,
46,
32,
13,
15,
6,
6,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 527,
"seconds": 12.016,
"avg_fps": 43.857,
"low_1pct_fps": 7.338,
"low_01pct_fps": 5.157,
"p50_ms": 16.784,
"p95_ms": 57.353,
"p99_ms": 111.984,
"p999_ms": 193.905,
"max_ms": 193.905,
"histogram": [
0,
0,
0,
1,
2,
9,
16,
29,
40,
61,
50,
54,
52,
62,
57,
35,
28,
11,
13,
5,
2,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 746,
"seconds": 12.001,
"avg_fps": 62.162,
"low_1pct_fps": 14.965,
"low_01pct_fps": 12.372,
"p50_ms": 13.76,
"p95_ms": 35.144,
"p99_ms": 57.63,
"p999_ms": 80.827,
"max_ms": 80.827,
"histogram": [
0,
0,
2,
13,
12,
41,
37,
53,
67,
82,
80,
99,
82,
76,
58,
17,
11,
12,
4,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 3910,
"seconds": 12.002,
"avg_fps": 325.77,
"low_1pct_fps": 79.024,
"low_01pct_fps": 41.466,
"p50_ms": 2.685,
"p95_ms": 5.125,
"p99_ms": 8.342,
"p999_ms": 20.782,
"max_ms": 30.571,
"histogram": [
0,
78,
2526,
892,
203,
95,
49,
27,
13,
11,
7,
3,
2,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 1268,
"seconds": 12.044,
"avg_fps": 105.282,
"low_1pct_fps": 14.736,
"low_01pct_fps": 8.855,
"p50_ms": 6.542,
"p95_ms": 25.245,
"p99_ms": 50.465,
"p999_ms": 79.065,
"max_ms": 146.809,
"histogram": [
0,
0,
4,
171,
190,
169,
167,
133,
105,
93,
53,
43,
35,
41,
26,
13,
11,
10,
3,
1,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 822,
"seconds": 12.011,
"avg_fps": 68.44,
"low_1pct_fps": 14.632,
"low_01pct_fps": 10.101,
"p50_ms": 11.396,
"p95_ms": 35.387,
"p99_ms": 55.24,
"p999_ms": 99.005,
"max_ms": 99.005,
"histogram": [
0,
0,
0,
10,
42,
62,
55,
86,
89,
97,
64,
73,
74,
72,
48,
21,
17,
8,
4,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 4165,
"seconds": 12.006,
"avg_fps": 346.896,
"low_1pct_fps": 75.429,
"low_01pct_fps": 33.579,
"p50_ms": 2.541,
"p95_ms": 4.838,
"p99_ms": 7.998,
"p999_ms": 20.209,
"max_ms": 53.577,
"histogram": [
0,
640,
2307,
855,
178,
93,
34,
22,
15,
3,
3,
8,
2,
2,
2,
0,
0,
1,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 851,
"seconds": 12.0,
"avg_fps": 70.916,
"low_1pct_fps": 11.158,
"low_01pct_fps": 7.903,
"p50_ms": 10.128,
"p95_ms": 36.498,
"p99_ms": 68.434,
"p999_ms": 126.541,
"max_ms": 126.541,
"histogram": [
0,
0,
0,
38,
61,
87,
76,
69,
84,
100,
63,
53,
68,
53,
48,
15,
9,
16,
9,
2,
0,
0,
0,
0
]
}
],
"label": "vanilla-macmini"
}
-771
View File
@@ -1,771 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-04T13:08:37Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "Apple",
"driver": "4.1 Metal - 91.7",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 18.159,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 8607.88,
"stdev": 949.596,
"min": 8010.323,
"max": 9702.854,
"ci95": 2359.119
},
"startup.world_ms": {
"n": 3,
"mean": 44182.88,
"stdev": 4031.933,
"min": 40172.323,
"max": 48235.854,
"ci95": 10016.685
},
"chunks.village.load_ms": {
"n": 3,
"mean": 17227.171,
"stdev": 5351.234,
"min": 13892.279,
"max": 23399.566,
"ci95": 13294.274
},
"fps.village.avg_fps": {
"n": 3,
"mean": 76.237,
"stdev": 45.661,
"min": 28.922,
"max": 120.041,
"ci95": 113.437
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 17.319,
"stdev": 12.99,
"min": 5.593,
"max": 31.283,
"ci95": 32.272
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 12.642,
"stdev": 9.463,
"min": 4.308,
"max": 22.93,
"ci95": 23.51
},
"memory.village.rss_mb": {
"n": 3,
"mean": 2674.987,
"stdev": 30.71,
"min": 2641.352,
"max": 2701.531,
"ci95": 76.294
},
"tick.village.client_ms": {
"n": 3,
"mean": 1.341,
"stdev": 0.407,
"min": 0.924,
"max": 1.737,
"ci95": 1.012
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 34112.389,
"stdev": 19305.487,
"min": 22633.094,
"max": 56401.09,
"ci95": 47961.36
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 72.848,
"stdev": 51.859,
"min": 18.307,
"max": 121.528,
"ci95": 128.837
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 22.096,
"stdev": 20.299,
"min": 3.973,
"max": 44.03,
"ci95": 50.429
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 16.093,
"stdev": 14.608,
"min": 3.688,
"max": 32.194,
"ci95": 36.291
},
"memory.forest.rss_mb": {
"n": 3,
"mean": 2519.729,
"stdev": 82.097,
"min": 2426.297,
"max": 2580.328,
"ci95": 203.957
},
"tick.forest.client_ms": {
"n": 3,
"mean": 1.115,
"stdev": 0.411,
"min": 0.726,
"max": 1.545,
"ci95": 1.02
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 19066.516,
"stdev": 9372.33,
"min": 13163.781,
"max": 29873.383,
"ci95": 23284.039
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 69.735,
"stdev": 46.656,
"min": 16.524,
"max": 103.635,
"ci95": 115.909
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 17.636,
"stdev": 13.616,
"min": 3.179,
"max": 30.217,
"ci95": 33.827
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 12.115,
"stdev": 10.798,
"min": 2.584,
"max": 23.842,
"ci95": 26.825
},
"memory.ocean.rss_mb": {
"n": 3,
"mean": 2557.143,
"stdev": 84.375,
"min": 2463.781,
"max": 2627.945,
"ci95": 209.616
},
"tick.ocean.client_ms": {
"n": 3,
"mean": 1.056,
"stdev": 0.233,
"min": 0.815,
"max": 1.281,
"ci95": 0.58
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 62214.712,
"stdev": 4884.13,
"min": 58937.045,
"max": 67828.14,
"ci95": 12133.83
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 51.809,
"stdev": 7.118,
"min": 45.235,
"max": 59.369,
"ci95": 17.685
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 6.336,
"stdev": 1.097,
"min": 5.545,
"max": 7.588,
"ci95": 2.725
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 3.458,
"stdev": 0.756,
"min": 2.827,
"max": 4.296,
"ci95": 1.878
},
"memory.mountain.rss_mb": {
"n": 3,
"mean": 2604.227,
"stdev": 15.043,
"min": 2588.234,
"max": 2618.094,
"ci95": 37.371
},
"tick.mountain.client_ms": {
"n": 3,
"mean": 0.957,
"stdev": 0.03,
"min": 0.926,
"max": 0.985,
"ci95": 0.074
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2731.016,
"stdev": 56.767,
"min": 2688.516,
"max": 2795.484,
"ci95": 141.03
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2577.339,
"stdev": 77.295,
"min": 2488.203,
"max": 2625.875,
"ci95": 192.028
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 4612.571,
"stdev": 20.245,
"min": 4595.363,
"max": 4634.879,
"ci95": 50.296
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 4271.925,
"stdev": 26.308,
"min": 4246.207,
"max": 4298.785,
"ci95": 65.357
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 349,
"seconds": 12.067,
"avg_fps": 28.922,
"low_1pct_fps": 5.593,
"low_01pct_fps": 4.308,
"p50_ms": 24.684,
"p95_ms": 92.764,
"p99_ms": 150.338,
"p999_ms": 232.136,
"max_ms": 232.136,
"histogram": [
0,
0,
0,
0,
0,
0,
2,
3,
13,
17,
26,
31,
40,
46,
50,
32,
25,
20,
30,
10,
4,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 1441,
"seconds": 12.004,
"avg_fps": 120.041,
"low_1pct_fps": 31.283,
"low_01pct_fps": 22.93,
"p50_ms": 7.267,
"p95_ms": 13.622,
"p99_ms": 23.449,
"p999_ms": 42.564,
"max_ms": 44.657,
"histogram": [
0,
0,
0,
0,
0,
64,
543,
397,
218,
100,
54,
24,
20,
7,
7,
5,
2,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 958,
"seconds": 12.013,
"avg_fps": 79.749,
"low_1pct_fps": 15.082,
"low_01pct_fps": 10.689,
"p50_ms": 9.138,
"p95_ms": 28.848,
"p99_ms": 52.247,
"p999_ms": 93.55,
"max_ms": 93.55,
"histogram": [
0,
0,
0,
0,
0,
19,
144,
232,
148,
111,
64,
56,
59,
57,
32,
17,
9,
6,
4,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 220,
"seconds": 12.017,
"avg_fps": 18.307,
"low_1pct_fps": 3.973,
"low_01pct_fps": 3.688,
"p50_ms": 40.635,
"p95_ms": 157.363,
"p99_ms": 235.253,
"p999_ms": 271.153,
"max_ms": 271.153,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
1,
3,
6,
14,
17,
25,
25,
17,
27,
33,
24,
16,
11,
1,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 1459,
"seconds": 12.005,
"avg_fps": 121.528,
"low_1pct_fps": 44.03,
"low_01pct_fps": 32.194,
"p50_ms": 7.381,
"p95_ms": 12.386,
"p99_ms": 18.354,
"p999_ms": 29.428,
"max_ms": 32.695,
"histogram": [
0,
0,
0,
0,
0,
0,
357,
642,
300,
80,
28,
28,
14,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 945,
"seconds": 12.006,
"avg_fps": 78.709,
"low_1pct_fps": 18.284,
"low_01pct_fps": 12.397,
"p50_ms": 9.554,
"p95_ms": 27.657,
"p99_ms": 43.253,
"p999_ms": 80.663,
"max_ms": 80.663,
"histogram": [
0,
0,
0,
0,
0,
0,
18,
304,
191,
119,
67,
59,
53,
70,
34,
15,
10,
3,
2,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 201,
"seconds": 12.164,
"avg_fps": 16.524,
"low_1pct_fps": 3.179,
"low_01pct_fps": 2.584,
"p50_ms": 44.933,
"p95_ms": 155.019,
"p99_ms": 263.919,
"p999_ms": 387.044,
"max_ms": 387.044,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
1,
2,
4,
7,
20,
8,
29,
18,
25,
29,
29,
17,
8,
4,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 1244,
"seconds": 12.004,
"avg_fps": 103.635,
"low_1pct_fps": 30.217,
"low_01pct_fps": 23.842,
"p50_ms": 8.349,
"p95_ms": 17.445,
"p99_ms": 28.276,
"p999_ms": 38.037,
"max_ms": 45.85,
"histogram": [
0,
0,
0,
0,
0,
0,
162,
453,
312,
129,
71,
47,
32,
18,
16,
3,
1,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 1069,
"seconds": 12.005,
"avg_fps": 89.047,
"low_1pct_fps": 19.512,
"low_01pct_fps": 9.92,
"p50_ms": 9.541,
"p95_ms": 20.819,
"p99_ms": 31.409,
"p999_ms": 99.668,
"max_ms": 101.942,
"histogram": [
0,
0,
0,
0,
0,
0,
64,
266,
283,
160,
112,
74,
51,
33,
17,
4,
1,
2,
1,
1,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 551,
"seconds": 12.181,
"avg_fps": 45.235,
"low_1pct_fps": 5.545,
"low_01pct_fps": 3.252,
"p50_ms": 14.921,
"p95_ms": 50.78,
"p99_ms": 105.154,
"p999_ms": 307.476,
"max_ms": 307.476,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
42,
137,
72,
58,
50,
47,
57,
39,
20,
15,
8,
3,
2,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 718,
"seconds": 12.094,
"avg_fps": 59.369,
"low_1pct_fps": 7.588,
"low_01pct_fps": 4.296,
"p50_ms": 12.178,
"p95_ms": 38.273,
"p99_ms": 65.694,
"p999_ms": 232.789,
"max_ms": 232.789,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
140,
202,
115,
72,
48,
47,
43,
19,
19,
6,
4,
0,
3,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 610,
"seconds": 12.003,
"avg_fps": 50.822,
"low_1pct_fps": 5.875,
"low_01pct_fps": 2.827,
"p50_ms": 13.894,
"p95_ms": 44.208,
"p99_ms": 75.023,
"p999_ms": 353.686,
"max_ms": 353.686,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
67,
115,
125,
88,
55,
54,
43,
25,
13,
15,
7,
0,
1,
2,
0,
0
]
}
],
"label": "vanilla-macmini"
}
@@ -1,723 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T22:55:52Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "Apple",
"driver": "4.1 Metal - 91.7",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 69.878,
"window": [
1920,
930
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 23843.039,
"stdev": 7524.513,
"min": 16643.766,
"max": 31655.294,
"ci95": 18693.436
},
"startup.world_ms": {
"n": 3,
"mean": 76908.706,
"stdev": 36719.804,
"min": 45051.766,
"max": 117069.294,
"ci95": 91224.412
},
"chunks.village.load_ms": {
"n": 3,
"mean": 19608.485,
"stdev": 9449.035,
"min": 13510.149,
"max": 30492.958,
"ci95": 23474.6
},
"fps.village.avg_fps": {
"n": 3,
"mean": 46.159,
"stdev": 17.954,
"min": 31.413,
"max": 66.152,
"ci95": 44.604
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 6.544,
"stdev": 3.452,
"min": 3.225,
"max": 10.116,
"ci95": 8.577
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 4.689,
"stdev": 1.808,
"min": 2.647,
"max": 6.085,
"ci95": 4.491
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 23344.685,
"stdev": 5493.145,
"min": 17038.709,
"max": 27089.821,
"ci95": 13646.83
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 61.892,
"stdev": 37.939,
"min": 22.354,
"max": 98.0,
"ci95": 94.254
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 9.736,
"stdev": 8.597,
"min": 2.296,
"max": 19.148,
"ci95": 21.359
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 6.651,
"stdev": 5.574,
"min": 2.202,
"max": 12.903,
"ci95": 13.847
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 27267.052,
"stdev": 21587.34,
"min": 10640.097,
"max": 51663.797,
"ci95": 53630.253
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 75.493,
"stdev": 63.885,
"min": 25.975,
"max": 147.604,
"ci95": 158.711
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 13.396,
"stdev": 13.366,
"min": 4.634,
"max": 28.78,
"ci95": 33.205
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 7.356,
"stdev": 6.609,
"min": 3.26,
"max": 14.98,
"ci95": 16.418
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 38284.867,
"stdev": 4237.4,
"min": 33922.931,
"max": 42385.65,
"ci95": 10527.134
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 35.918,
"stdev": 17.742,
"min": 18.135,
"max": 53.619,
"ci95": 44.077
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 3.999,
"stdev": 2.132,
"min": 2.453,
"max": 6.431,
"ci95": 5.297
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 3.017,
"stdev": 1.831,
"min": 1.775,
"max": 5.12,
"ci95": 4.549
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2566.339,
"stdev": 218.228,
"min": 2339.094,
"max": 2774.266,
"ci95": 542.153
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2354.852,
"stdev": 240.105,
"min": 2082.984,
"max": 2537.867,
"ci95": 596.503
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 3430.598,
"stdev": 77.063,
"min": 3345.395,
"max": 3495.426,
"ci95": 191.451
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 3211.931,
"stdev": 99.984,
"min": 3124.098,
"max": 3320.738,
"ci95": 248.394
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 377,
"seconds": 12.002,
"avg_fps": 31.413,
"low_1pct_fps": 3.225,
"low_01pct_fps": 2.647,
"p50_ms": 15.145,
"p95_ms": 128.716,
"p99_ms": 276.685,
"p999_ms": 377.749,
"max_ms": 377.749,
"histogram": [
0,
0,
0,
0,
17,
26,
21,
30,
23,
43,
16,
25,
28,
28,
35,
16,
14,
12,
15,
13,
11,
4,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 794,
"seconds": 12.003,
"avg_fps": 66.152,
"low_1pct_fps": 10.116,
"low_01pct_fps": 6.085,
"p50_ms": 11.137,
"p95_ms": 41.078,
"p99_ms": 73.531,
"p999_ms": 164.347,
"max_ms": 164.347,
"histogram": [
0,
0,
0,
20,
57,
65,
52,
70,
82,
88,
80,
67,
53,
48,
52,
18,
20,
10,
10,
1,
1,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 491,
"seconds": 12.001,
"avg_fps": 40.912,
"low_1pct_fps": 6.291,
"low_01pct_fps": 5.336,
"p50_ms": 15.962,
"p95_ms": 71.604,
"p99_ms": 137.056,
"p999_ms": 187.401,
"max_ms": 187.401,
"histogram": [
0,
0,
0,
0,
1,
5,
10,
32,
54,
60,
45,
49,
44,
49,
50,
22,
21,
20,
18,
8,
3,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 784,
"seconds": 12.002,
"avg_fps": 65.322,
"low_1pct_fps": 7.763,
"low_01pct_fps": 4.848,
"p50_ms": 10.024,
"p95_ms": 43.46,
"p99_ms": 94.19,
"p999_ms": 206.286,
"max_ms": 206.286,
"histogram": [
0,
0,
0,
17,
40,
69,
80,
95,
88,
82,
53,
58,
39,
49,
52,
21,
15,
14,
7,
3,
2,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 1176,
"seconds": 12.0,
"avg_fps": 98.0,
"low_1pct_fps": 19.148,
"low_01pct_fps": 12.903,
"p50_ms": 6.214,
"p95_ms": 30.125,
"p99_ms": 41.199,
"p999_ms": 73.675,
"max_ms": 81.328,
"histogram": [
0,
0,
3,
179,
205,
175,
115,
111,
79,
39,
21,
25,
34,
79,
77,
20,
10,
2,
2,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 271,
"seconds": 12.123,
"avg_fps": 22.354,
"low_1pct_fps": 2.296,
"low_01pct_fps": 2.202,
"p50_ms": 14.989,
"p95_ms": 214.282,
"p99_ms": 404.5,
"p999_ms": 454.134,
"max_ms": 454.134,
"histogram": [
0,
0,
0,
1,
5,
18,
19,
19,
28,
28,
13,
19,
20,
12,
21,
14,
5,
8,
4,
9,
18,
10,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 1772,
"seconds": 12.005,
"avg_fps": 147.604,
"low_1pct_fps": 28.78,
"low_01pct_fps": 14.98,
"p50_ms": 5.328,
"p95_ms": 15.261,
"p99_ms": 25.961,
"p999_ms": 58.586,
"max_ms": 74.925,
"histogram": [
0,
0,
34,
421,
331,
290,
175,
163,
122,
83,
44,
33,
26,
28,
16,
2,
2,
1,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 635,
"seconds": 12.004,
"avg_fps": 52.9,
"low_1pct_fps": 6.774,
"low_01pct_fps": 3.828,
"p50_ms": 9.578,
"p95_ms": 61.195,
"p99_ms": 112.118,
"p999_ms": 261.228,
"max_ms": 261.228,
"histogram": [
0,
0,
0,
38,
49,
64,
63,
63,
51,
60,
27,
29,
27,
17,
37,
28,
30,
27,
15,
8,
1,
1,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 312,
"seconds": 12.011,
"avg_fps": 25.975,
"low_1pct_fps": 4.634,
"low_01pct_fps": 3.26,
"p50_ms": 24.629,
"p95_ms": 115.493,
"p99_ms": 169.827,
"p999_ms": 306.737,
"max_ms": 306.737,
"histogram": [
0,
0,
0,
0,
0,
0,
2,
5,
10,
10,
24,
39,
33,
38,
47,
16,
16,
21,
23,
22,
5,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 218,
"seconds": 12.021,
"avg_fps": 18.135,
"low_1pct_fps": 2.453,
"low_01pct_fps": 2.156,
"p50_ms": 17.447,
"p95_ms": 257.129,
"p99_ms": 377.63,
"p999_ms": 463.802,
"max_ms": 463.802,
"histogram": [
0,
0,
7,
13,
15,
18,
11,
9,
7,
16,
8,
3,
11,
10,
14,
7,
5,
16,
8,
15,
12,
13,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 653,
"seconds": 12.179,
"avg_fps": 53.619,
"low_1pct_fps": 6.431,
"low_01pct_fps": 5.12,
"p50_ms": 8.427,
"p95_ms": 58.968,
"p99_ms": 141.423,
"p999_ms": 195.302,
"max_ms": 195.302,
"histogram": [
0,
0,
0,
38,
66,
76,
79,
64,
45,
38,
20,
29,
22,
24,
46,
25,
36,
17,
14,
10,
4,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 432,
"seconds": 12.0,
"avg_fps": 35.999,
"low_1pct_fps": 3.112,
"low_01pct_fps": 1.775,
"p50_ms": 16.724,
"p95_ms": 83.13,
"p99_ms": 220.496,
"p999_ms": 563.493,
"max_ms": 563.493,
"histogram": [
0,
0,
0,
7,
14,
17,
23,
35,
35,
26,
24,
33,
34,
43,
43,
23,
27,
20,
15,
5,
4,
3,
1,
0
]
}
],
"label": "vanilla-mods-m4pro-1080p"
}
@@ -1,723 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T23:17:23Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "Apple",
"driver": "4.1 Metal - 91.7",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 37.185,
"window": [
1920,
930
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 13521.661,
"stdev": 573.908,
"min": 13085.942,
"max": 14171.935,
"ci95": 1425.783
},
"startup.world_ms": {
"n": 3,
"mean": 63229.994,
"stdev": 6185.388,
"min": 56237.942,
"max": 67987.935,
"ci95": 15366.595
},
"chunks.village.load_ms": {
"n": 3,
"mean": 14807.746,
"stdev": 2020.358,
"min": 13414.414,
"max": 17124.849,
"ci95": 5019.253
},
"fps.village.avg_fps": {
"n": 3,
"mean": 91.753,
"stdev": 11.452,
"min": 78.555,
"max": 99.067,
"ci95": 28.451
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 13.105,
"stdev": 2.514,
"min": 10.354,
"max": 15.282,
"ci95": 6.244
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 9.203,
"stdev": 1.382,
"min": 7.689,
"max": 10.396,
"ci95": 3.432
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 24180.167,
"stdev": 777.711,
"min": 23290.973,
"max": 24733.555,
"ci95": 1932.098
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 113.871,
"stdev": 53.305,
"min": 56.542,
"max": 161.938,
"ci95": 132.428
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 28.748,
"stdev": 17.633,
"min": 12.823,
"max": 47.698,
"ci95": 43.807
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 20.551,
"stdev": 13.547,
"min": 8.937,
"max": 35.433,
"ci95": 33.655
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 19314.547,
"stdev": 5853.258,
"min": 15068.272,
"max": 25991.528,
"ci95": 14541.473
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 119.116,
"stdev": 71.027,
"min": 45.498,
"max": 187.231,
"ci95": 176.454
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 31.721,
"stdev": 26.624,
"min": 8.585,
"max": 60.822,
"ci95": 66.144
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 16.8,
"stdev": 14.937,
"min": 5.876,
"max": 33.821,
"ci95": 37.109
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 61290.415,
"stdev": 15063.343,
"min": 49453.256,
"max": 78246.003,
"ci95": 37422.439
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 56.905,
"stdev": 19.674,
"min": 37.807,
"max": 77.109,
"ci95": 48.878
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 7.384,
"stdev": 0.62,
"min": 6.67,
"max": 7.784,
"ci95": 1.539
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 3.48,
"stdev": 1.306,
"min": 2.526,
"max": 4.968,
"ci95": 3.244
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 4078.01,
"stdev": 79.593,
"min": 4020.391,
"max": 4168.828,
"ci95": 197.735
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 3705.104,
"stdev": 149.415,
"min": 3541.375,
"max": 3834.078,
"ci95": 371.196
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 5250.386,
"stdev": 54.229,
"min": 5200.662,
"max": 5308.208,
"ci95": 134.723
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 4647.172,
"stdev": 29.179,
"min": 4613.552,
"max": 4665.911,
"ci95": 72.492
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 1172,
"seconds": 12.004,
"avg_fps": 97.637,
"low_1pct_fps": 15.282,
"low_01pct_fps": 10.396,
"p50_ms": 7.394,
"p95_ms": 26.126,
"p99_ms": 50.505,
"p999_ms": 79.614,
"max_ms": 112.764,
"histogram": [
0,
0,
0,
24,
111,
198,
193,
170,
129,
109,
59,
42,
43,
23,
38,
10,
10,
7,
5,
1,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 1189,
"seconds": 12.002,
"avg_fps": 99.067,
"low_1pct_fps": 13.678,
"low_01pct_fps": 9.523,
"p50_ms": 7.061,
"p95_ms": 28.029,
"p99_ms": 58.166,
"p999_ms": 103.326,
"max_ms": 106.682,
"histogram": [
0,
0,
0,
59,
132,
223,
172,
171,
107,
90,
59,
39,
36,
28,
35,
14,
9,
9,
4,
2,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 943,
"seconds": 12.004,
"avg_fps": 78.555,
"low_1pct_fps": 10.354,
"low_01pct_fps": 7.689,
"p50_ms": 8.817,
"p95_ms": 31.62,
"p99_ms": 70.58,
"p999_ms": 130.049,
"max_ms": 130.049,
"histogram": [
0,
0,
0,
8,
34,
97,
118,
168,
126,
106,
69,
58,
49,
36,
32,
8,
9,
13,
8,
4,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 679,
"seconds": 12.009,
"avg_fps": 56.542,
"low_1pct_fps": 12.823,
"low_01pct_fps": 8.937,
"p50_ms": 14.344,
"p95_ms": 42.09,
"p99_ms": 65.756,
"p999_ms": 111.897,
"max_ms": 111.897,
"histogram": [
0,
0,
0,
0,
2,
33,
50,
62,
68,
60,
48,
67,
84,
76,
64,
27,
18,
14,
5,
1,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 1478,
"seconds": 12.003,
"avg_fps": 123.133,
"low_1pct_fps": 25.723,
"low_01pct_fps": 17.282,
"p50_ms": 6.388,
"p95_ms": 17.972,
"p99_ms": 29.029,
"p999_ms": 57.659,
"max_ms": 58.069,
"histogram": [
0,
0,
0,
3,
336,
298,
222,
186,
144,
77,
70,
57,
39,
23,
13,
5,
3,
2,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 1944,
"seconds": 12.005,
"avg_fps": 161.938,
"low_1pct_fps": 47.698,
"low_01pct_fps": 35.433,
"p50_ms": 5.168,
"p95_ms": 12.12,
"p99_ms": 17.337,
"p999_ms": 26.48,
"max_ms": 29.965,
"histogram": [
0,
0,
0,
18,
793,
527,
243,
125,
91,
47,
41,
34,
15,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 548,
"seconds": 12.044,
"avg_fps": 45.498,
"low_1pct_fps": 8.585,
"low_01pct_fps": 5.876,
"p50_ms": 15.416,
"p95_ms": 62.523,
"p99_ms": 98.757,
"p999_ms": 170.17,
"max_ms": 170.17,
"histogram": [
0,
0,
0,
0,
2,
15,
26,
52,
53,
58,
37,
56,
39,
58,
60,
27,
24,
18,
21,
1,
1,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 1496,
"seconds": 12.005,
"avg_fps": 124.619,
"low_1pct_fps": 25.756,
"low_01pct_fps": 10.702,
"p50_ms": 6.649,
"p95_ms": 15.751,
"p99_ms": 26.208,
"p999_ms": 76.546,
"max_ms": 110.33,
"histogram": [
0,
0,
0,
15,
250,
299,
266,
216,
163,
115,
68,
40,
32,
13,
14,
3,
0,
0,
1,
1,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 2247,
"seconds": 12.001,
"avg_fps": 187.231,
"low_1pct_fps": 60.822,
"low_01pct_fps": 33.821,
"p50_ms": 4.73,
"p95_ms": 8.497,
"p99_ms": 12.315,
"p999_ms": 21.724,
"max_ms": 42.26,
"histogram": [
0,
0,
0,
347,
987,
453,
211,
122,
67,
32,
15,
6,
4,
2,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 670,
"seconds": 12.007,
"avg_fps": 55.799,
"low_1pct_fps": 7.784,
"low_01pct_fps": 2.526,
"p50_ms": 12.887,
"p95_ms": 45.129,
"p99_ms": 76.315,
"p999_ms": 395.821,
"max_ms": 395.821,
"histogram": [
0,
0,
0,
0,
0,
16,
54,
79,
75,
81,
68,
60,
53,
70,
54,
17,
16,
9,
17,
0,
0,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 455,
"seconds": 12.035,
"avg_fps": 37.807,
"low_1pct_fps": 6.67,
"low_01pct_fps": 4.968,
"p50_ms": 15.986,
"p95_ms": 82.012,
"p99_ms": 117.987,
"p999_ms": 201.279,
"max_ms": 201.279,
"histogram": [
0,
0,
0,
0,
0,
3,
27,
37,
47,
49,
37,
39,
31,
39,
34,
20,
24,
30,
26,
10,
2,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 933,
"seconds": 12.1,
"avg_fps": 77.109,
"low_1pct_fps": 7.697,
"low_01pct_fps": 2.946,
"p50_ms": 8.593,
"p95_ms": 34.696,
"p99_ms": 79.266,
"p999_ms": 339.486,
"max_ms": 339.486,
"histogram": [
0,
0,
0,
0,
0,
45,
176,
209,
196,
95,
45,
43,
26,
19,
30,
9,
9,
14,
13,
2,
1,
1,
0,
0
]
}
],
"label": "vanilla-mods-m4pro-1080p"
}
@@ -1,723 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-11-01T22:55:52Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "macos",
"arch": "aarch64",
"cpu_threads": 14,
"os_version": "27.0.1",
"cpu": "Apple M4 Pro",
"ram_mb": 65536,
"gpu": {
"name": "Apple M4 Pro",
"vendor": "Apple",
"driver": "4.1 Metal - 91.7",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": true,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 74.321,
"window": [
1920,
930
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 56311.461,
"stdev": 26982.658,
"min": 36031.835,
"max": 86935.815,
"ci95": 67034.048
},
"startup.world_ms": {
"n": 3,
"mean": 97731.794,
"stdev": 21691.925,
"min": 72951.835,
"max": 113284.815,
"ci95": 53890.078
},
"chunks.village.load_ms": {
"n": 3,
"mean": 13954.335,
"stdev": 10146.301,
"min": 7679.006,
"max": 25660.115,
"ci95": 25206.843
},
"fps.village.avg_fps": {
"n": 3,
"mean": 33.239,
"stdev": 18.526,
"min": 12.03,
"max": 46.264,
"ci95": 46.026
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 3.772,
"stdev": 2.399,
"min": 1.734,
"max": 6.416,
"ci95": 5.96
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 2.866,
"stdev": 1.762,
"min": 1.433,
"max": 4.834,
"ci95": 4.379
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 19803.961,
"stdev": 14910.114,
"min": 8220.752,
"max": 36626.588,
"ci95": 37041.766
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 47.303,
"stdev": 17.044,
"min": 32.698,
"max": 66.03,
"ci95": 42.343
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 4.365,
"stdev": 0.709,
"min": 3.658,
"max": 5.076,
"ci95": 1.761
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 3.128,
"stdev": 0.564,
"min": 2.651,
"max": 3.751,
"ci95": 1.402
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 18639.188,
"stdev": 8912.103,
"min": 11960.198,
"max": 28758.72,
"ci95": 22140.678
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 34.908,
"stdev": 6.372,
"min": 29.271,
"max": 41.822,
"ci95": 15.831
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 4.614,
"stdev": 1.558,
"min": 2.986,
"max": 6.09,
"ci95": 3.869
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 4.06,
"stdev": 1.662,
"min": 2.224,
"max": 5.463,
"ci95": 4.13
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 19191.558,
"stdev": 2902.338,
"min": 15845.729,
"max": 21030.7,
"ci95": 7210.388
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 67.004,
"stdev": 13.923,
"min": 51.98,
"max": 79.471,
"ci95": 34.589
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 7.862,
"stdev": 1.987,
"min": 6.339,
"max": 10.109,
"ci95": 4.936
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 3.993,
"stdev": 1.679,
"min": 2.06,
"max": 5.088,
"ci95": 4.172
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2019.089,
"stdev": 53.398,
"min": 1960.188,
"max": 2064.328,
"ci95": 132.657
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1927.195,
"stdev": 108.077,
"min": 1813.609,
"max": 2028.758,
"ci95": 268.5
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 2440.727,
"stdev": 72.508,
"min": 2375.394,
"max": 2518.738,
"ci95": 180.134
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 2395.53,
"stdev": 71.597,
"min": 2318.504,
"max": 2460.05,
"ci95": 177.872
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 146,
"seconds": 12.136,
"avg_fps": 12.03,
"low_1pct_fps": 1.734,
"low_01pct_fps": 1.433,
"p50_ms": 28.465,
"p95_ms": 339.625,
"p99_ms": 455.663,
"p999_ms": 697.859,
"max_ms": 697.859,
"histogram": [
0,
0,
1,
4,
9,
9,
5,
5,
8,
6,
5,
5,
1,
10,
9,
6,
11,
10,
5,
8,
15,
13,
1,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 556,
"seconds": 12.018,
"avg_fps": 46.264,
"low_1pct_fps": 6.416,
"low_01pct_fps": 4.834,
"p50_ms": 13.72,
"p95_ms": 71.774,
"p99_ms": 120.078,
"p999_ms": 206.87,
"max_ms": 206.87,
"histogram": [
0,
0,
0,
6,
5,
16,
22,
57,
55,
64,
62,
62,
50,
38,
29,
19,
23,
16,
21,
9,
2,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 498,
"seconds": 12.022,
"avg_fps": 41.423,
"low_1pct_fps": 3.166,
"low_01pct_fps": 2.331,
"p50_ms": 7.7,
"p95_ms": 114.025,
"p99_ms": 246.232,
"p999_ms": 428.98,
"max_ms": 428.98,
"histogram": [
0,
0,
15,
63,
55,
58,
33,
38,
31,
22,
23,
17,
24,
24,
20,
11,
9,
10,
14,
15,
12,
4,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 521,
"seconds": 12.065,
"avg_fps": 43.182,
"low_1pct_fps": 4.362,
"low_01pct_fps": 3.751,
"p50_ms": 8.121,
"p95_ms": 110.621,
"p99_ms": 191.925,
"p999_ms": 266.575,
"max_ms": 266.575,
"histogram": [
0,
0,
1,
63,
27,
49,
61,
67,
39,
45,
27,
17,
13,
17,
7,
11,
13,
11,
18,
22,
11,
2,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 793,
"seconds": 12.01,
"avg_fps": 66.03,
"low_1pct_fps": 5.076,
"low_01pct_fps": 2.651,
"p50_ms": 8.218,
"p95_ms": 44.767,
"p99_ms": 143.139,
"p999_ms": 377.207,
"max_ms": 377.207,
"histogram": [
0,
0,
0,
60,
106,
55,
58,
127,
97,
64,
41,
28,
32,
31,
39,
12,
7,
8,
14,
9,
4,
1,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 395,
"seconds": 12.08,
"avg_fps": 32.698,
"low_1pct_fps": 3.658,
"low_01pct_fps": 2.983,
"p50_ms": 10.283,
"p95_ms": 171.818,
"p99_ms": 231.998,
"p999_ms": 335.242,
"max_ms": 335.242,
"histogram": [
0,
0,
6,
19,
13,
27,
42,
31,
53,
36,
22,
25,
20,
19,
10,
8,
7,
6,
11,
15,
23,
2,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 405,
"seconds": 12.043,
"avg_fps": 33.63,
"low_1pct_fps": 4.765,
"low_01pct_fps": 4.492,
"p50_ms": 14.686,
"p95_ms": 115.893,
"p99_ms": 196.822,
"p999_ms": 222.615,
"max_ms": 222.615,
"histogram": [
0,
0,
0,
5,
6,
16,
18,
49,
42,
36,
22,
30,
35,
32,
23,
10,
10,
22,
22,
16,
11,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 502,
"seconds": 12.003,
"avg_fps": 41.822,
"low_1pct_fps": 6.09,
"low_01pct_fps": 5.463,
"p50_ms": 15.264,
"p95_ms": 76.822,
"p99_ms": 144.7,
"p999_ms": 183.045,
"max_ms": 183.045,
"histogram": [
0,
0,
0,
9,
14,
12,
13,
47,
43,
52,
41,
40,
52,
40,
50,
15,
26,
17,
17,
9,
5,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 353,
"seconds": 12.06,
"avg_fps": 29.271,
"low_1pct_fps": 2.986,
"low_01pct_fps": 2.224,
"p50_ms": 10.806,
"p95_ms": 150.281,
"p99_ms": 268.607,
"p999_ms": 449.56,
"max_ms": 449.56,
"histogram": [
0,
0,
0,
29,
16,
24,
36,
35,
23,
31,
13,
16,
12,
18,
14,
9,
8,
16,
18,
17,
12,
6,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 624,
"seconds": 12.005,
"avg_fps": 51.98,
"low_1pct_fps": 7.137,
"low_01pct_fps": 5.088,
"p50_ms": 12.033,
"p95_ms": 75.22,
"p99_ms": 115.345,
"p999_ms": 196.553,
"max_ms": 196.553,
"histogram": [
0,
0,
4,
29,
48,
34,
38,
40,
61,
56,
50,
57,
54,
41,
33,
13,
15,
15,
28,
6,
2,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 835,
"seconds": 12.004,
"avg_fps": 69.562,
"low_1pct_fps": 6.339,
"low_01pct_fps": 2.06,
"p50_ms": 9.818,
"p95_ms": 37.917,
"p99_ms": 81.491,
"p999_ms": 485.481,
"max_ms": 485.481,
"histogram": [
0,
0,
6,
62,
63,
59,
75,
84,
75,
94,
59,
77,
55,
31,
36,
27,
10,
10,
5,
5,
1,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 954,
"seconds": 12.004,
"avg_fps": 79.471,
"low_1pct_fps": 10.109,
"low_01pct_fps": 4.832,
"p50_ms": 7.91,
"p95_ms": 38.672,
"p99_ms": 79.104,
"p999_ms": 206.935,
"max_ms": 206.935,
"histogram": [
0,
0,
13,
55,
70,
136,
119,
109,
93,
98,
52,
49,
48,
21,
30,
17,
14,
11,
17,
1,
1,
0,
0,
0
]
}
],
"label": "vanilla-mods-m4pro-1080p"
}
@@ -1,706 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:25:53Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 10777.756,
"stdev": 129.67,
"min": 10636.92,
"max": 10892.2,
"ci95": 322.144
},
"startup.world_ms": {
"n": 3,
"mean": 23477.423,
"stdev": 346.614,
"min": 23130.92,
"max": 23824.148,
"ci95": 861.106
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6007.033,
"stdev": 104.227,
"min": 5891.431,
"max": 6093.825,
"ci95": 258.936
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1282.11,
"stdev": 65.207,
"min": 1228.13,
"max": 1354.56,
"ci95": 161.997
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 343.465,
"stdev": 9.96,
"min": 335.223,
"max": 354.532,
"ci95": 24.744
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 131.669,
"stdev": 4.692,
"min": 126.427,
"max": 135.476,
"ci95": 11.656
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8669.311,
"stdev": 434.39,
"min": 8280.854,
"max": 9138.35,
"ci95": 1079.173
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1745.757,
"stdev": 13.014,
"min": 1734.41,
"max": 1759.963,
"ci95": 32.332
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 460.958,
"stdev": 10.859,
"min": 449.051,
"max": 470.316,
"ci95": 26.978
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 178.955,
"stdev": 3.777,
"min": 175.213,
"max": 182.766,
"ci95": 9.383
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 7056.368,
"stdev": 117.329,
"min": 6986.912,
"max": 7191.833,
"ci95": 291.485
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1815.056,
"stdev": 47.875,
"min": 1759.788,
"max": 1843.745,
"ci95": 118.937
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 514.215,
"stdev": 57.205,
"min": 452.445,
"max": 565.368,
"ci95": 142.117
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 191.842,
"stdev": 23.217,
"min": 175.546,
"max": 218.426,
"ci95": 57.68
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 14094.582,
"stdev": 399.624,
"min": 13641.534,
"max": 14397.0,
"ci95": 992.801
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1499.768,
"stdev": 22.197,
"min": 1477.269,
"max": 1521.651,
"ci95": 55.146
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 408.466,
"stdev": 16.114,
"min": 395.097,
"max": 426.358,
"ci95": 40.032
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 139.743,
"stdev": 3.969,
"min": 136.465,
"max": 144.156,
"ci95": 9.86
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2495.607,
"stdev": 75.339,
"min": 2412.738,
"max": 2559.965,
"ci95": 187.166
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2326.611,
"stdev": 15.702,
"min": 2314.98,
"max": 2344.473,
"ci95": 39.01
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 14738,
"seconds": 12.0,
"avg_fps": 1228.13,
"low_1pct_fps": 335.223,
"low_01pct_fps": 133.103,
"p50_ms": 0.722,
"p95_ms": 1.384,
"p99_ms": 2.043,
"p999_ms": 5.224,
"max_ms": 11.459,
"histogram": [
12159,
2421,
128,
14,
1,
5,
2,
3,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 16255,
"seconds": 12.0,
"avg_fps": 1354.56,
"low_1pct_fps": 354.532,
"low_01pct_fps": 135.476,
"p50_ms": 0.672,
"p95_ms": 1.136,
"p99_ms": 1.841,
"p999_ms": 4.913,
"max_ms": 16.94,
"histogram": [
14841,
1298,
86,
13,
2,
5,
4,
1,
3,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 15164,
"seconds": 12.0,
"avg_fps": 1263.64,
"low_1pct_fps": 340.639,
"low_01pct_fps": 126.427,
"p50_ms": 0.708,
"p95_ms": 1.299,
"p99_ms": 1.895,
"p999_ms": 5.173,
"max_ms": 16.748,
"histogram": [
13026,
2014,
94,
13,
1,
3,
3,
6,
2,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 20915,
"seconds": 12.0,
"avg_fps": 1742.897,
"low_1pct_fps": 470.316,
"low_01pct_fps": 182.766,
"p50_ms": 0.5,
"p95_ms": 0.936,
"p99_ms": 1.437,
"p999_ms": 2.77,
"max_ms": 13.414,
"histogram": [
20067,
788,
44,
4,
0,
4,
3,
3,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 21120,
"seconds": 12.0,
"avg_fps": 1759.963,
"low_1pct_fps": 449.051,
"low_01pct_fps": 178.887,
"p50_ms": 0.49,
"p95_ms": 0.901,
"p99_ms": 1.531,
"p999_ms": 3.005,
"max_ms": 12.907,
"histogram": [
20414,
644,
40,
10,
1,
3,
2,
2,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 20813,
"seconds": 12.0,
"avg_fps": 1734.41,
"low_1pct_fps": 463.506,
"low_01pct_fps": 175.213,
"p50_ms": 0.496,
"p95_ms": 0.924,
"p99_ms": 1.414,
"p999_ms": 2.733,
"max_ms": 16.832,
"histogram": [
20049,
707,
41,
5,
0,
1,
5,
2,
2,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 22100,
"seconds": 12.0,
"avg_fps": 1841.634,
"low_1pct_fps": 565.368,
"low_01pct_fps": 218.426,
"p50_ms": 0.483,
"p95_ms": 0.835,
"p99_ms": 1.209,
"p999_ms": 2.082,
"max_ms": 14.627,
"histogram": [
21540,
532,
18,
1,
1,
1,
2,
2,
1,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 21118,
"seconds": 12.0,
"avg_fps": 1759.788,
"low_1pct_fps": 452.445,
"low_01pct_fps": 175.546,
"p50_ms": 0.489,
"p95_ms": 0.915,
"p99_ms": 1.532,
"p999_ms": 2.811,
"max_ms": 11.846,
"histogram": [
20308,
755,
36,
9,
0,
2,
0,
3,
2,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 22125,
"seconds": 12.0,
"avg_fps": 1843.745,
"low_1pct_fps": 524.832,
"low_01pct_fps": 181.555,
"p50_ms": 0.477,
"p95_ms": 0.855,
"p99_ms": 1.241,
"p999_ms": 2.236,
"max_ms": 12.478,
"histogram": [
21487,
608,
18,
1,
1,
1,
2,
2,
1,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 18005,
"seconds": 12.0,
"avg_fps": 1500.383,
"low_1pct_fps": 403.944,
"low_01pct_fps": 136.465,
"p50_ms": 0.591,
"p95_ms": 1.058,
"p99_ms": 1.592,
"p999_ms": 3.186,
"max_ms": 12.608,
"histogram": [
16864,
1084,
32,
8,
0,
5,
3,
5,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 17728,
"seconds": 12.001,
"avg_fps": 1477.269,
"low_1pct_fps": 395.097,
"low_01pct_fps": 138.609,
"p50_ms": 0.598,
"p95_ms": 1.042,
"p99_ms": 1.561,
"p999_ms": 5.033,
"max_ms": 11.462,
"histogram": [
16612,
1051,
39,
7,
1,
6,
5,
3,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 18260,
"seconds": 12.0,
"avg_fps": 1521.651,
"low_1pct_fps": 426.358,
"low_01pct_fps": 144.156,
"p50_ms": 0.585,
"p95_ms": 1.015,
"p99_ms": 1.458,
"p999_ms": 3.609,
"max_ms": 11.945,
"histogram": [
17267,
943,
26,
6,
5,
2,
3,
3,
2,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -1,706 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T19:27:56Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 11038.922,
"stdev": 118.058,
"min": 10935.424,
"max": 11167.506,
"ci95": 293.295
},
"startup.world_ms": {
"n": 3,
"mean": 39248.588,
"stdev": 479.755,
"min": 38964.834,
"max": 39802.506,
"ci95": 1191.875
},
"chunks.village.load_ms": {
"n": 3,
"mean": 11176.953,
"stdev": 356.144,
"min": 10836.65,
"max": 11547.064,
"ci95": 884.781
},
"fps.village.avg_fps": {
"n": 3,
"mean": 925.977,
"stdev": 6.727,
"min": 918.832,
"max": 932.189,
"ci95": 16.713
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 250.092,
"stdev": 1.868,
"min": 248.431,
"max": 252.114,
"ci95": 4.64
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 92.692,
"stdev": 4.466,
"min": 88.423,
"max": 97.332,
"ci95": 11.095
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 17335.638,
"stdev": 687.588,
"min": 16544.168,
"max": 17785.772,
"ci95": 1708.201
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 929.034,
"stdev": 3.11,
"min": 926.32,
"max": 932.428,
"ci95": 7.727
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 312.69,
"stdev": 16.386,
"min": 297.667,
"max": 330.163,
"ci95": 40.708
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 129.331,
"stdev": 12.786,
"min": 115.486,
"max": 140.694,
"ci95": 31.765
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 12601.063,
"stdev": 148.042,
"min": 12430.182,
"max": 12690.506,
"ci95": 367.785
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1016.598,
"stdev": 5.481,
"min": 1010.482,
"max": 1021.068,
"ci95": 13.618
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 325.361,
"stdev": 15.112,
"min": 315.682,
"max": 342.774,
"ci95": 37.543
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 117.042,
"stdev": 11.095,
"min": 107.807,
"max": 129.349,
"ci95": 27.563
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 38513.008,
"stdev": 958.119,
"min": 37447.333,
"max": 39303.226,
"ci95": 2380.292
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 644.782,
"stdev": 3.94,
"min": 640.866,
"max": 648.745,
"ci95": 9.788
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 166.029,
"stdev": 4.758,
"min": 162.326,
"max": 171.395,
"ci95": 11.819
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 86.231,
"stdev": 8.587,
"min": 76.632,
"max": 93.184,
"ci95": 21.334
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2964.911,
"stdev": 8.27,
"min": 2959.813,
"max": 2974.453,
"ci95": 20.545
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2894.541,
"stdev": 12.847,
"min": 2879.928,
"max": 2904.059,
"ci95": 31.917
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 11123,
"seconds": 12.0,
"avg_fps": 926.911,
"low_1pct_fps": 249.732,
"low_01pct_fps": 92.322,
"p50_ms": 0.989,
"p95_ms": 1.492,
"p99_ms": 2.33,
"p999_ms": 7.409,
"max_ms": 15.258,
"histogram": [
6224,
4701,
147,
28,
3,
2,
5,
3,
3,
3,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 11187,
"seconds": 12.001,
"avg_fps": 932.189,
"low_1pct_fps": 252.114,
"low_01pct_fps": 88.423,
"p50_ms": 0.984,
"p95_ms": 1.496,
"p99_ms": 2.328,
"p999_ms": 7.858,
"max_ms": 16.843,
"histogram": [
6518,
4486,
138,
20,
9,
1,
1,
6,
1,
2,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 11026,
"seconds": 12.0,
"avg_fps": 918.832,
"low_1pct_fps": 248.431,
"low_01pct_fps": 97.332,
"p50_ms": 0.989,
"p95_ms": 1.524,
"p99_ms": 2.37,
"p999_ms": 7.446,
"max_ms": 16.573,
"histogram": [
6075,
4740,
162,
23,
5,
2,
4,
7,
4,
1,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 11117,
"seconds": 12.001,
"avg_fps": 926.32,
"low_1pct_fps": 310.239,
"low_01pct_fps": 131.814,
"p50_ms": 1.011,
"p95_ms": 1.34,
"p99_ms": 2.045,
"p999_ms": 4.711,
"max_ms": 14.077,
"histogram": [
3849,
7150,
78,
26,
5,
1,
3,
2,
1,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 11190,
"seconds": 12.001,
"avg_fps": 932.428,
"low_1pct_fps": 330.163,
"low_01pct_fps": 140.694,
"p50_ms": 1.008,
"p95_ms": 1.307,
"p99_ms": 1.869,
"p999_ms": 4.341,
"max_ms": 13.489,
"histogram": [
4273,
6822,
60,
20,
7,
0,
3,
3,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 11141,
"seconds": 12.001,
"avg_fps": 928.354,
"low_1pct_fps": 297.667,
"low_01pct_fps": 115.486,
"p50_ms": 1.005,
"p95_ms": 1.352,
"p99_ms": 2.054,
"p999_ms": 5.292,
"max_ms": 14.339,
"histogram": [
4901,
6118,
83,
23,
3,
2,
4,
3,
0,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 12126,
"seconds": 12.0,
"avg_fps": 1010.482,
"low_1pct_fps": 317.626,
"low_01pct_fps": 113.971,
"p50_ms": 0.933,
"p95_ms": 1.23,
"p99_ms": 1.935,
"p999_ms": 4.393,
"max_ms": 14.824,
"histogram": [
9183,
2830,
82,
17,
3,
1,
2,
3,
0,
1,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 12219,
"seconds": 12.0,
"avg_fps": 1018.243,
"low_1pct_fps": 342.774,
"low_01pct_fps": 129.349,
"p50_ms": 0.929,
"p95_ms": 1.222,
"p99_ms": 1.867,
"p999_ms": 4.329,
"max_ms": 10.33,
"histogram": [
9421,
2710,
60,
15,
3,
0,
0,
5,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 12253,
"seconds": 12.0,
"avg_fps": 1021.068,
"low_1pct_fps": 315.682,
"low_01pct_fps": 107.807,
"p50_ms": 0.923,
"p95_ms": 1.225,
"p99_ms": 1.969,
"p999_ms": 5.235,
"max_ms": 19.524,
"histogram": [
9231,
2904,
92,
10,
2,
5,
0,
4,
1,
2,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 7691,
"seconds": 12.001,
"avg_fps": 640.866,
"low_1pct_fps": 164.367,
"low_01pct_fps": 93.184,
"p50_ms": 1.454,
"p95_ms": 1.95,
"p99_ms": 3.4,
"p999_ms": 9.754,
"max_ms": 14.995,
"histogram": [
36,
7331,
233,
26,
17,
13,
20,
3,
6,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 7737,
"seconds": 12.0,
"avg_fps": 644.736,
"low_1pct_fps": 162.326,
"low_01pct_fps": 76.632,
"p50_ms": 1.454,
"p95_ms": 1.905,
"p99_ms": 3.676,
"p999_ms": 9.926,
"max_ms": 18.459,
"histogram": [
46,
7399,
199,
24,
21,
24,
10,
6,
1,
1,
4,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 7785,
"seconds": 12.0,
"avg_fps": 648.745,
"low_1pct_fps": 171.395,
"low_01pct_fps": 88.876,
"p50_ms": 1.439,
"p95_ms": 1.886,
"p99_ms": 3.562,
"p999_ms": 10.342,
"max_ms": 12.927,
"histogram": [
57,
7432,
186,
44,
23,
20,
9,
3,
3,
7,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -1,706 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:49:40Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
},
{
"name": "sodium",
"version": "mc26.3-0.9.2-fabric"
},
{
"name": "lithium",
"version": "mc26.3-0.26.2-fabric"
},
{
"name": "ferrite-core",
"version": "9.0.0-fabric"
},
{
"name": "immediatelyfast",
"version": "1.17.1+26.3-fabric"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 11089.831,
"stdev": 46.336,
"min": 11036.36,
"max": 11118.192,
"ci95": 115.114
},
"startup.world_ms": {
"n": 3,
"mean": 17643.831,
"stdev": 47.672,
"min": 17589.192,
"max": 17676.942,
"ci95": 118.433
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3603.773,
"stdev": 161.342,
"min": 3488.106,
"max": 3788.086,
"ci95": 400.828
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2103.54,
"stdev": 40.465,
"min": 2078.464,
"max": 2150.222,
"ci95": 100.529
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 577.326,
"stdev": 33.693,
"min": 557.613,
"max": 616.23,
"ci95": 83.704
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 195.53,
"stdev": 18.899,
"min": 181.495,
"max": 217.019,
"ci95": 46.951
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 3282.612,
"stdev": 48.569,
"min": 3233.16,
"max": 3330.246,
"ci95": 120.661
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2424.739,
"stdev": 26.074,
"min": 2394.789,
"max": 2442.383,
"ci95": 64.777
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 732.987,
"stdev": 11.926,
"min": 724.144,
"max": 746.55,
"ci95": 29.627
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 249.926,
"stdev": 8.826,
"min": 241.293,
"max": 258.934,
"ci95": 21.928
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2705.954,
"stdev": 173.487,
"min": 2538.486,
"max": 2884.891,
"ci95": 431.0
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2431.99,
"stdev": 33.071,
"min": 2394.02,
"max": 2454.493,
"ci95": 82.159
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 764.065,
"stdev": 14.782,
"min": 747.287,
"max": 775.168,
"ci95": 36.723
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 261.954,
"stdev": 14.724,
"min": 252.457,
"max": 278.916,
"ci95": 36.58
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 4982.865,
"stdev": 115.523,
"min": 4849.837,
"max": 5057.932,
"ci95": 286.997
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 2538.05,
"stdev": 67.405,
"min": 2462.815,
"max": 2592.938,
"ci95": 167.458
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 760.16,
"stdev": 13.503,
"min": 746.001,
"max": 772.895,
"ci95": 33.547
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 240.897,
"stdev": 17.872,
"min": 224.391,
"max": 259.878,
"ci95": 44.401
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2052.982,
"stdev": 57.756,
"min": 1992.574,
"max": 2107.66,
"ci95": 143.487
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1903.921,
"stdev": 62.254,
"min": 1838.307,
"max": 1962.156,
"ci95": 154.659
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 25803,
"seconds": 12.0,
"avg_fps": 2150.222,
"low_1pct_fps": 616.23,
"low_01pct_fps": 217.019,
"p50_ms": 0.433,
"p95_ms": 0.642,
"p99_ms": 1.04,
"p999_ms": 2.307,
"max_ms": 7.466,
"histogram": [
25503,
269,
8,
6,
7,
6,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 24942,
"seconds": 12.0,
"avg_fps": 2078.464,
"low_1pct_fps": 557.613,
"low_01pct_fps": 181.495,
"p50_ms": 0.45,
"p95_ms": 0.665,
"p99_ms": 1.1,
"p999_ms": 2.305,
"max_ms": 9.819,
"histogram": [
24615,
288,
17,
2,
4,
9,
2,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 24984,
"seconds": 12.0,
"avg_fps": 2081.934,
"low_1pct_fps": 558.135,
"low_01pct_fps": 188.076,
"p50_ms": 0.444,
"p95_ms": 0.682,
"p99_ms": 1.101,
"p999_ms": 2.412,
"max_ms": 11.567,
"histogram": [
24645,
308,
9,
3,
5,
5,
7,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 29245,
"seconds": 12.0,
"avg_fps": 2437.044,
"low_1pct_fps": 728.266,
"low_01pct_fps": 249.55,
"p50_ms": 0.38,
"p95_ms": 0.555,
"p99_ms": 0.892,
"p999_ms": 1.851,
"max_ms": 6.973,
"histogram": [
29066,
154,
4,
6,
7,
6,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 28738,
"seconds": 12.0,
"avg_fps": 2394.789,
"low_1pct_fps": 724.144,
"low_01pct_fps": 241.293,
"p50_ms": 0.384,
"p95_ms": 0.571,
"p99_ms": 0.907,
"p999_ms": 1.833,
"max_ms": 8.35,
"histogram": [
28573,
140,
6,
3,
6,
5,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 29309,
"seconds": 12.0,
"avg_fps": 2442.383,
"low_1pct_fps": 746.55,
"low_01pct_fps": 258.934,
"p50_ms": 0.378,
"p95_ms": 0.566,
"p99_ms": 0.876,
"p999_ms": 1.744,
"max_ms": 10.291,
"histogram": [
29138,
148,
5,
6,
5,
2,
4,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 29454,
"seconds": 12.0,
"avg_fps": 2454.493,
"low_1pct_fps": 769.741,
"low_01pct_fps": 252.457,
"p50_ms": 0.378,
"p95_ms": 0.55,
"p99_ms": 0.853,
"p999_ms": 1.471,
"max_ms": 6.629,
"histogram": [
29323,
110,
2,
2,
7,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 28729,
"seconds": 12.0,
"avg_fps": 2394.02,
"low_1pct_fps": 747.287,
"low_01pct_fps": 254.49,
"p50_ms": 0.383,
"p95_ms": 0.579,
"p99_ms": 0.896,
"p999_ms": 1.56,
"max_ms": 7.367,
"histogram": [
28567,
141,
3,
5,
4,
4,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 29370,
"seconds": 12.0,
"avg_fps": 2447.458,
"low_1pct_fps": 775.168,
"low_01pct_fps": 278.916,
"p50_ms": 0.376,
"p95_ms": 0.559,
"p99_ms": 0.887,
"p999_ms": 1.567,
"max_ms": 9.65,
"histogram": [
29209,
141,
3,
9,
0,
5,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 31116,
"seconds": 12.0,
"avg_fps": 2592.938,
"low_1pct_fps": 761.584,
"low_01pct_fps": 224.391,
"p50_ms": 0.356,
"p95_ms": 0.511,
"p99_ms": 0.767,
"p999_ms": 1.843,
"max_ms": 8.478,
"histogram": [
31011,
76,
4,
3,
13,
4,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 29554,
"seconds": 12.0,
"avg_fps": 2462.815,
"low_1pct_fps": 746.001,
"low_01pct_fps": 238.423,
"p50_ms": 0.366,
"p95_ms": 0.579,
"p99_ms": 0.849,
"p999_ms": 1.75,
"max_ms": 7.607,
"histogram": [
29430,
101,
2,
3,
9,
4,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 30701,
"seconds": 12.0,
"avg_fps": 2558.396,
"low_1pct_fps": 772.895,
"low_01pct_fps": 259.878,
"p50_ms": 0.357,
"p95_ms": 0.537,
"p99_ms": 0.81,
"p999_ms": 1.702,
"max_ms": 6.682,
"histogram": [
30568,
112,
1,
6,
5,
3,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-mods-win-9800x3d-5080"
}
@@ -1,690 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T01:19:23Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 16,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7681.834,
"stdev": 189.48,
"min": 7538.777,
"max": 7896.728,
"ci95": 470.732
},
"startup.world_ms": {
"n": 3,
"mean": 19898.834,
"stdev": 192.981,
"min": 19695.728,
"max": 20079.777,
"ci95": 479.431
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6127.947,
"stdev": 26.756,
"min": 6097.19,
"max": 6145.854,
"ci95": 66.471
},
"fps.village.avg_fps": {
"n": 3,
"mean": 1218.486,
"stdev": 26.337,
"min": 1188.13,
"max": 1235.248,
"ci95": 65.43
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 318.468,
"stdev": 6.292,
"min": 313.2,
"max": 325.435,
"ci95": 15.632
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 126.59,
"stdev": 1.844,
"min": 124.744,
"max": 128.431,
"ci95": 4.58
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 8548.814,
"stdev": 152.302,
"min": 8380.912,
"max": 8678.071,
"ci95": 378.37
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1912.359,
"stdev": 130.238,
"min": 1791.386,
"max": 2050.215,
"ci95": 323.555
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 401.907,
"stdev": 35.911,
"min": 379.849,
"max": 443.345,
"ci95": 89.216
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 149.26,
"stdev": 9.983,
"min": 139.809,
"max": 159.701,
"ci95": 24.801
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 6953.478,
"stdev": 276.636,
"min": 6635.649,
"max": 7140.072,
"ci95": 687.258
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1860.512,
"stdev": 116.512,
"min": 1726.149,
"max": 1933.597,
"ci95": 289.454
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 446.336,
"stdev": 56.993,
"min": 382.26,
"max": 491.369,
"ci95": 141.589
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 164.934,
"stdev": 11.839,
"min": 152.493,
"max": 176.062,
"ci95": 29.413
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 15211.386,
"stdev": 256.348,
"min": 14938.008,
"max": 15446.375,
"ci95": 636.856
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 1653.223,
"stdev": 61.705,
"min": 1584.652,
"max": 1704.273,
"ci95": 153.297
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 331.929,
"stdev": 10.858,
"min": 319.394,
"max": 338.396,
"ci95": 26.974
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 113.436,
"stdev": 3.255,
"min": 110.869,
"max": 117.097,
"ci95": 8.086
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2540.129,
"stdev": 152.057,
"min": 2373.152,
"max": 2670.633,
"ci95": 377.761
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2299.514,
"stdev": 81.451,
"min": 2207.129,
"max": 2360.973,
"ci95": 202.352
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 14785,
"seconds": 12.0,
"avg_fps": 1232.081,
"low_1pct_fps": 325.435,
"low_01pct_fps": 128.431,
"p50_ms": 0.742,
"p95_ms": 1.265,
"p99_ms": 2.032,
"p999_ms": 5.653,
"max_ms": 12.791,
"histogram": [
12845,
1784,
127,
7,
1,
8,
4,
3,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 14258,
"seconds": 12.0,
"avg_fps": 1188.13,
"low_1pct_fps": 313.2,
"low_01pct_fps": 126.595,
"p50_ms": 0.757,
"p95_ms": 1.349,
"p99_ms": 2.12,
"p999_ms": 5.541,
"max_ms": 13.998,
"histogram": [
11994,
2076,
157,
9,
2,
8,
2,
4,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 14823,
"seconds": 12.0,
"avg_fps": 1235.248,
"low_1pct_fps": 316.768,
"low_01pct_fps": 124.744,
"p50_ms": 0.744,
"p95_ms": 1.199,
"p99_ms": 2.047,
"p999_ms": 5.628,
"max_ms": 15.519,
"histogram": [
13214,
1453,
118,
16,
2,
8,
6,
3,
0,
1,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 21497,
"seconds": 12.0,
"avg_fps": 1791.386,
"low_1pct_fps": 382.527,
"low_01pct_fps": 139.809,
"p50_ms": 0.474,
"p95_ms": 0.931,
"p99_ms": 1.646,
"p999_ms": 5.274,
"max_ms": 13.459,
"histogram": [
20598,
799,
66,
9,
3,
8,
8,
2,
2,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 24603,
"seconds": 12.0,
"avg_fps": 2050.215,
"low_1pct_fps": 443.345,
"low_01pct_fps": 159.701,
"p50_ms": 0.426,
"p95_ms": 0.752,
"p99_ms": 1.442,
"p999_ms": 3.134,
"max_ms": 13.282,
"histogram": [
24041,
487,
48,
6,
5,
6,
2,
3,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 22746,
"seconds": 12.0,
"avg_fps": 1895.476,
"low_1pct_fps": 379.849,
"low_01pct_fps": 148.269,
"p50_ms": 0.456,
"p95_ms": 0.829,
"p99_ms": 1.561,
"p999_ms": 4.176,
"max_ms": 10.671,
"histogram": [
22092,
523,
91,
17,
4,
7,
3,
4,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 23062,
"seconds": 12.0,
"avg_fps": 1921.791,
"low_1pct_fps": 465.379,
"low_01pct_fps": 166.247,
"p50_ms": 0.463,
"p95_ms": 0.809,
"p99_ms": 1.45,
"p999_ms": 2.398,
"max_ms": 11.808,
"histogram": [
22454,
561,
27,
3,
2,
6,
0,
4,
3,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 23204,
"seconds": 12.0,
"avg_fps": 1933.597,
"low_1pct_fps": 491.369,
"low_01pct_fps": 176.062,
"p50_ms": 0.466,
"p95_ms": 0.766,
"p99_ms": 1.406,
"p999_ms": 2.229,
"max_ms": 15.102,
"histogram": [
22699,
469,
19,
4,
1,
1,
4,
2,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 20714,
"seconds": 12.0,
"avg_fps": 1726.149,
"low_1pct_fps": 382.26,
"low_01pct_fps": 152.493,
"p50_ms": 0.5,
"p95_ms": 0.956,
"p99_ms": 1.742,
"p999_ms": 3.852,
"max_ms": 12.888,
"histogram": [
19814,
787,
83,
11,
2,
7,
3,
5,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 20049,
"seconds": 12.0,
"avg_fps": 1670.745,
"low_1pct_fps": 338.396,
"low_01pct_fps": 117.097,
"p50_ms": 0.502,
"p95_ms": 0.949,
"p99_ms": 1.673,
"p999_ms": 6.456,
"max_ms": 16.791,
"histogram": [
19187,
753,
70,
8,
1,
6,
12,
4,
3,
4,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 19016,
"seconds": 12.0,
"avg_fps": 1584.652,
"low_1pct_fps": 337.998,
"low_01pct_fps": 112.343,
"p50_ms": 0.531,
"p95_ms": 1.082,
"p99_ms": 1.717,
"p999_ms": 5.939,
"max_ms": 12.958,
"histogram": [
17849,
1069,
63,
7,
3,
6,
2,
7,
4,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 20452,
"seconds": 12.0,
"avg_fps": 1704.273,
"low_1pct_fps": 319.394,
"low_01pct_fps": 110.869,
"p50_ms": 0.496,
"p95_ms": 0.911,
"p99_ms": 1.706,
"p999_ms": 5.938,
"max_ms": 24.863,
"histogram": [
19694,
626,
82,
14,
3,
14,
5,
4,
5,
3,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -1,690 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T19:06:49Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 32,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7518.874,
"stdev": 42.968,
"min": 7469.532,
"max": 7548.049,
"ci95": 106.748
},
"startup.world_ms": {
"n": 3,
"mean": 35424.207,
"stdev": 144.711,
"min": 35318.049,
"max": 35589.041,
"ci95": 359.511
},
"chunks.village.load_ms": {
"n": 3,
"mean": 11146.828,
"stdev": 49.683,
"min": 11094.798,
"max": 11193.773,
"ci95": 123.43
},
"fps.village.avg_fps": {
"n": 3,
"mean": 882.053,
"stdev": 19.433,
"min": 866.126,
"max": 903.706,
"ci95": 48.279
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 183.995,
"stdev": 4.924,
"min": 178.512,
"max": 188.041,
"ci95": 12.234
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 83.193,
"stdev": 6.797,
"min": 77.734,
"max": 90.806,
"ci95": 16.886
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 18184.002,
"stdev": 266.879,
"min": 17880.667,
"max": 18382.735,
"ci95": 663.017
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1202.907,
"stdev": 20.426,
"min": 1179.9,
"max": 1218.906,
"ci95": 50.744
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 228.139,
"stdev": 5.166,
"min": 222.394,
"max": 232.401,
"ci95": 12.833
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 107.956,
"stdev": 4.675,
"min": 105.185,
"max": 113.354,
"ci95": 11.614
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 13146.632,
"stdev": 507.943,
"min": 12572.118,
"max": 13536.145,
"ci95": 1261.902
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 1223.169,
"stdev": 13.11,
"min": 1208.837,
"max": 1234.556,
"ci95": 32.569
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 236.27,
"stdev": 6.77,
"min": 228.686,
"max": 241.702,
"ci95": 16.818
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 99.089,
"stdev": 8.354,
"min": 91.897,
"max": 108.253,
"ci95": 20.755
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 41846.641,
"stdev": 661.649,
"min": 41435.666,
"max": 42609.895,
"ci95": 1643.76
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 815.008,
"stdev": 24.644,
"min": 797.565,
"max": 843.201,
"ci95": 61.224
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 55.371,
"stdev": 5.128,
"min": 50.389,
"max": 60.633,
"ci95": 12.739
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 7.966,
"stdev": 1.103,
"min": 6.848,
"max": 9.053,
"ci95": 2.74
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2892.492,
"stdev": 29.54,
"min": 2859.574,
"max": 2916.691,
"ci95": 73.387
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2799.159,
"stdev": 12.256,
"min": 2786.234,
"max": 2810.613,
"ci95": 30.447
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 10845,
"seconds": 12.001,
"avg_fps": 903.706,
"low_1pct_fps": 185.432,
"low_01pct_fps": 81.039,
"p50_ms": 0.98,
"p95_ms": 1.708,
"p99_ms": 3.082,
"p999_ms": 9.304,
"max_ms": 15.487,
"histogram": [
6030,
4403,
297,
49,
25,
16,
8,
2,
6,
2,
4,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 10516,
"seconds": 12.0,
"avg_fps": 876.328,
"low_1pct_fps": 188.041,
"low_01pct_fps": 90.806,
"p50_ms": 1.012,
"p95_ms": 1.806,
"p99_ms": 3.354,
"p999_ms": 8.714,
"max_ms": 14.39,
"histogram": [
4927,
5158,
292,
70,
33,
13,
6,
5,
6,
2,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 10394,
"seconds": 12.001,
"avg_fps": 866.126,
"low_1pct_fps": 178.512,
"low_01pct_fps": 77.734,
"p50_ms": 1.026,
"p95_ms": 1.789,
"p99_ms": 3.399,
"p999_ms": 10.6,
"max_ms": 15.389,
"histogram": [
4437,
5552,
273,
65,
18,
28,
8,
1,
1,
3,
5,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 14519,
"seconds": 12.0,
"avg_fps": 1209.916,
"low_1pct_fps": 232.401,
"low_01pct_fps": 105.185,
"p50_ms": 0.726,
"p95_ms": 1.306,
"p99_ms": 2.25,
"p999_ms": 7.033,
"max_ms": 13.303,
"histogram": [
13121,
1197,
103,
36,
24,
19,
4,
3,
8,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 14159,
"seconds": 12.0,
"avg_fps": 1179.9,
"low_1pct_fps": 222.394,
"low_01pct_fps": 105.33,
"p50_ms": 0.732,
"p95_ms": 1.366,
"p99_ms": 2.499,
"p999_ms": 8.061,
"max_ms": 11.797,
"histogram": [
12501,
1356,
195,
45,
25,
14,
6,
3,
9,
5,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 14627,
"seconds": 12.0,
"avg_fps": 1218.906,
"low_1pct_fps": 229.622,
"low_01pct_fps": 113.354,
"p50_ms": 0.713,
"p95_ms": 1.292,
"p99_ms": 2.317,
"p999_ms": 7.136,
"max_ms": 12.041,
"histogram": [
13194,
1224,
97,
47,
27,
21,
2,
6,
6,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 14714,
"seconds": 12.001,
"avg_fps": 1226.113,
"low_1pct_fps": 238.423,
"low_01pct_fps": 97.117,
"p50_ms": 0.722,
"p95_ms": 1.223,
"p99_ms": 2.243,
"p999_ms": 7.032,
"max_ms": 13.21,
"histogram": [
13490,
1027,
115,
29,
18,
13,
7,
3,
3,
5,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 14507,
"seconds": 12.001,
"avg_fps": 1208.837,
"low_1pct_fps": 228.686,
"low_01pct_fps": 91.897,
"p50_ms": 0.719,
"p95_ms": 1.256,
"p99_ms": 2.354,
"p999_ms": 7.473,
"max_ms": 15.432,
"histogram": [
13124,
1112,
180,
35,
23,
9,
8,
4,
2,
6,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 14815,
"seconds": 12.0,
"avg_fps": 1234.556,
"low_1pct_fps": 241.702,
"low_01pct_fps": 108.253,
"p50_ms": 0.721,
"p95_ms": 1.2,
"p99_ms": 2.197,
"p999_ms": 6.689,
"max_ms": 12.791,
"histogram": [
13565,
1055,
107,
29,
23,
15,
8,
3,
4,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 10119,
"seconds": 12.001,
"avg_fps": 843.201,
"low_1pct_fps": 60.633,
"low_01pct_fps": 9.053,
"p50_ms": 0.891,
"p95_ms": 1.845,
"p99_ms": 3.91,
"p999_ms": 8.209,
"max_ms": 316.409,
"histogram": [
6956,
2747,
240,
83,
45,
22,
8,
8,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
4,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 9693,
"seconds": 12.052,
"avg_fps": 804.258,
"low_1pct_fps": 50.389,
"low_01pct_fps": 6.848,
"p50_ms": 0.906,
"p95_ms": 1.924,
"p99_ms": 4.157,
"p999_ms": 8.578,
"max_ms": 297.695,
"histogram": [
6578,
2678,
248,
94,
45,
18,
16,
5,
4,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
5,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 9571,
"seconds": 12.0,
"avg_fps": 797.565,
"low_1pct_fps": 55.09,
"low_01pct_fps": 7.997,
"p50_ms": 0.929,
"p95_ms": 1.944,
"p99_ms": 4.495,
"p999_ms": 7.764,
"max_ms": 321.994,
"histogram": [
5954,
3172,
230,
103,
39,
34,
19,
11,
4,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
4,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -1,690 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-29T18:43:17Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "windows",
"arch": "x86_64",
"cpu_threads": 16,
"os_version": "Microsoft Windows [Version 10.0.26200.8037]",
"cpu": "AMD Ryzen 7 9800X3D 8-Core Processor",
"ram_mb": 95943,
"gpu": {
"name": "NVIDIA GeForce RTX 5080/PCIe/SSE2",
"vendor": "NVIDIA Corporation",
"driver": "3.3.0 NVIDIA 617.42",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 8,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 3,
"warmup_s": 2.0,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 7676.526,
"stdev": 305.763,
"min": 7426.024,
"max": 8017.247,
"ci95": 759.617
},
"startup.world_ms": {
"n": 3,
"mean": 13862.526,
"stdev": 284.577,
"min": 13569.024,
"max": 14137.247,
"ci95": 706.985
},
"chunks.village.load_ms": {
"n": 3,
"mean": 3808.014,
"stdev": 61.275,
"min": 3737.989,
"max": 3851.799,
"ci95": 152.227
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2216.576,
"stdev": 45.247,
"min": 2166.285,
"max": 2253.985,
"ci95": 112.409
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 545.307,
"stdev": 14.876,
"min": 528.15,
"max": 554.607,
"ci95": 36.957
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 215.412,
"stdev": 5.692,
"min": 209.522,
"max": 220.882,
"ci95": 14.14
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 3212.958,
"stdev": 151.722,
"min": 3076.655,
"max": 3376.428,
"ci95": 376.928
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2456.542,
"stdev": 336.369,
"min": 2068.162,
"max": 2654.549,
"ci95": 835.654
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 624.759,
"stdev": 90.0,
"min": 521.303,
"max": 685.008,
"ci95": 223.59
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 252.469,
"stdev": 21.044,
"min": 229.228,
"max": 270.233,
"ci95": 52.28
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2983.236,
"stdev": 168.416,
"min": 2788.777,
"max": 3082.249,
"ci95": 418.403
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2469.831,
"stdev": 333.258,
"min": 2085.121,
"max": 2669.9,
"ci95": 827.925
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 688.684,
"stdev": 96.943,
"min": 576.803,
"max": 747.759,
"ci95": 240.839
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 286.444,
"stdev": 21.36,
"min": 262.183,
"max": 302.422,
"ci95": 53.066
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 6079.869,
"stdev": 719.864,
"min": 5542.196,
"max": 6897.69,
"ci95": 1788.385
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 2605.12,
"stdev": 326.424,
"min": 2228.824,
"max": 2812.075,
"ci95": 810.948
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 699.63,
"stdev": 87.846,
"min": 599.44,
"max": 763.45,
"ci95": 218.239
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 264.461,
"stdev": 28.459,
"min": 239.602,
"max": 295.503,
"ci95": 70.701
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2120.426,
"stdev": 116.406,
"min": 2046.93,
"max": 2254.637,
"ci95": 289.191
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1912.642,
"stdev": 38.925,
"min": 1889.766,
"max": 1957.586,
"ci95": 96.702
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 27048,
"seconds": 12.0,
"avg_fps": 2253.985,
"low_1pct_fps": 553.164,
"low_01pct_fps": 215.833,
"p50_ms": 0.412,
"p95_ms": 0.617,
"p99_ms": 1.187,
"p999_ms": 2.238,
"max_ms": 9.792,
"histogram": [
26716,
296,
15,
4,
7,
5,
2,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 26754,
"seconds": 12.0,
"avg_fps": 2229.458,
"low_1pct_fps": 554.607,
"low_01pct_fps": 220.882,
"p50_ms": 0.417,
"p95_ms": 0.626,
"p99_ms": 1.212,
"p999_ms": 2.448,
"max_ms": 8.388,
"histogram": [
26423,
293,
16,
9,
4,
5,
1,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 25996,
"seconds": 12.0,
"avg_fps": 2166.285,
"low_1pct_fps": 528.15,
"low_01pct_fps": 209.522,
"p50_ms": 0.43,
"p95_ms": 0.662,
"p99_ms": 1.308,
"p999_ms": 2.477,
"max_ms": 11.033,
"histogram": [
25615,
338,
23,
5,
5,
5,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 31855,
"seconds": 12.0,
"avg_fps": 2654.549,
"low_1pct_fps": 667.966,
"low_01pct_fps": 257.945,
"p50_ms": 0.341,
"p95_ms": 0.519,
"p99_ms": 0.932,
"p999_ms": 1.761,
"max_ms": 10.944,
"histogram": [
31558,
275,
6,
2,
7,
0,
4,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 31763,
"seconds": 12.0,
"avg_fps": 2646.916,
"low_1pct_fps": 685.008,
"low_01pct_fps": 270.233,
"p50_ms": 0.344,
"p95_ms": 0.52,
"p99_ms": 0.94,
"p999_ms": 1.685,
"max_ms": 11.406,
"histogram": [
31474,
272,
3,
4,
2,
2,
1,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 24818,
"seconds": 12.0,
"avg_fps": 2068.162,
"low_1pct_fps": 521.303,
"low_01pct_fps": 229.228,
"p50_ms": 0.439,
"p95_ms": 0.767,
"p99_ms": 1.284,
"p999_ms": 2.351,
"max_ms": 11.468,
"histogram": [
24356,
410,
36,
4,
4,
3,
2,
2,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 32039,
"seconds": 12.0,
"avg_fps": 2669.9,
"low_1pct_fps": 741.491,
"low_01pct_fps": 294.727,
"p50_ms": 0.345,
"p95_ms": 0.501,
"p99_ms": 0.86,
"p999_ms": 1.431,
"max_ms": 9.257,
"histogram": [
31771,
250,
3,
2,
4,
6,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 31854,
"seconds": 12.0,
"avg_fps": 2654.471,
"low_1pct_fps": 747.759,
"low_01pct_fps": 302.422,
"p50_ms": 0.347,
"p95_ms": 0.503,
"p99_ms": 0.853,
"p999_ms": 1.404,
"max_ms": 13.925,
"histogram": [
31580,
261,
3,
2,
0,
1,
3,
3,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 25022,
"seconds": 12.0,
"avg_fps": 2085.121,
"low_1pct_fps": 576.803,
"low_01pct_fps": 262.183,
"p50_ms": 0.428,
"p95_ms": 0.758,
"p99_ms": 1.214,
"p999_ms": 2.109,
"max_ms": 9.206,
"histogram": [
24600,
390,
20,
3,
2,
3,
2,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 33745,
"seconds": 12.0,
"avg_fps": 2812.075,
"low_1pct_fps": 736.0,
"low_01pct_fps": 258.279,
"p50_ms": 0.327,
"p95_ms": 0.471,
"p99_ms": 0.808,
"p999_ms": 1.689,
"max_ms": 10.07,
"histogram": [
33516,
205,
7,
0,
7,
4,
3,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 33294,
"seconds": 12.0,
"avg_fps": 2774.461,
"low_1pct_fps": 763.45,
"low_01pct_fps": 295.503,
"p50_ms": 0.334,
"p95_ms": 0.471,
"p99_ms": 0.813,
"p999_ms": 1.63,
"max_ms": 8.512,
"histogram": [
33056,
221,
3,
5,
1,
2,
3,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 26747,
"seconds": 12.0,
"avg_fps": 2228.824,
"low_1pct_fps": 599.44,
"low_01pct_fps": 239.602,
"p50_ms": 0.403,
"p95_ms": 0.678,
"p99_ms": 1.107,
"p999_ms": 2.054,
"max_ms": 9.178,
"histogram": [
26418,
298,
13,
6,
2,
8,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-win-9800x3d-5080"
}
@@ -1,106 +0,0 @@
# Horizon v0.2.97 vs vanilla 26.3 on Windows 11: Ryzen 7 9800X3D + RTX 5080 (2026-10-29, RD 8 and 32 rerun 2026-10-30)
The first `tools/perf_bench.py` run on Windows ([docs/benchmark.md](../../docs/benchmark.md)): Horizon, vanilla 26.3 and
vanilla with performance mods, render distances 8, 16 and 32, 1280x720, three games each, every game from a fresh
world. The reports are the `*-win-9800x3d-5080-rd*.json` files next to this page.
```sh
python tools/perf_bench.py --table horizon-win-9800x3d-5080 vanilla-win-9800x3d-5080 vanilla-mods-win-9800x3d-5080
```
## Setup
- **Machine:** AMD Ryzen 7 9800X3D (8 cores, 16 threads), 94 GB RAM (DDR5, EXPO off), NVIDIA GeForce RTX 5080
(driver 617.42), Windows 11 Pro 26200, Balanced power plan, desktop. One 1920x1080 120 Hz display. The game windows
were on the desktop of an unlocked session.
- **Horizon:** release v0.2.97 as the launcher installs it (`--build %APPDATA%\horizon-launcher\versions\v0.2.97`,
`--resources` its `.local\horizon-client\26.3`), Vulkan.
- **Vanilla:** the 26.3 client jar with Fabric Loader 0.19.5 and the driver mod, Java 25.0.5 (Temurin), `-Xmx2G`
with the launcher's G1 flags and the manifest's `-XX:StackShadowPages=32`, OpenGL 3.3.
- **Vanilla + mods:** the same with Sodium mc26.3-0.9.2, Lithium mc26.3-0.26.2, FerriteCore 9.0.0 and
ImmediatelyFast 1.17.1 (`tools/benchmark-deps/mods.lock.json`).
- **Commands:** `python tools/perf_bench.py --game <game> --label <game>-win-9800x3d-5080 --distances <d>
--sizes 1280x720 --repeats 3` (plus `--build` and `--resources` for Horizon), with the games taking turns at
each distance.
- **Memory** is the working set on Windows (what Task Manager shows), sampled from outside the process. Horizon's
GPU figure is its own Vulkan allocator total. `nvidia-smi` gives no per-process figure under WDDM, so vanilla has
none.
## Load
Windows has no load average, so the reports have no `load_avg_1m`. Every game ran on an otherwise idle machine:
- **RD 8 and RD 32** (rerun on 2026-10-30, after the first pass had builds running alongside): a script started each
game only once no `rustc`, `cargo`, `link` (or other compiler) process and no other game (`horizon-client`, `java`,
`javaw`, Minecraft) was running, and the machine's CPU load had stayed under 15 % in every 5-second sample for
30 seconds. During each game it sampled every 2 seconds the machine's CPU load and the CPU of everything outside the
benchmark's own processes. A game during which a compiler or another game ran, or other processes used over 15 % of
the machine for 10 seconds, was discarded and run again: two of the 20 games were (vanilla + mods at RD 32, a
build started during one, background processes during the other).
- **RD 16** (2026-10-29): no `rustc`, `cargo` or `link` process at any point, the load logged every 5 seconds.
| Game | RD | Mean / max CPU load (machine, the game included) | Other processes' CPU, mean / max | Compiler processes |
|---|---|---|---|---|
| Horizon | 8 | 18 % / 49 % | 6 % / 12 % | 0 |
| Vanilla | 8 | 28 % / 75 % | 7 % / 20 % | 0 |
| Vanilla + mods | 8 | 30 % / 81 % | 7 % / 20 % | 0 |
| Horizon | 16 | 29 % / 80 % | – | 0 |
| Vanilla | 16 | 41 % / 91 % | – | 0 |
| Vanilla + mods | 16 | 51 % / 90 % | – | 0 |
| Horizon | 32 | 36 % / 100 % | 5 % / 15 % | 0 |
| Vanilla | 32 | 39 % / 73 % | 6 % / 18 % | 0 |
| Vanilla + mods | 32 | 42 % / 88 % | 6 % / 21 % | 0 |
"Other processes" is the machine's load minus the benchmark's own processes (the game and `perf_bench.py`), in
2-second samples. Most of it is the system's share of the game's own drawing (the graphics driver and the
compositor), which no process of the benchmark is charged for; the desktop apps left open (a browser, chat apps)
briefly reached 5 % at most. RD 16's load was logged from outside every 5 seconds, so it has no such split, and its mean includes the time between
games. Compared with the first pass, the idle RD 8 runs give Horizon about 10 % higher averages and about 50 % higher
1% lows, and vanilla up to 10 % higher averages.
## Results
Mean of 3 games ± 95% confidence interval. FPS over the 12-second flight at 16 blocks/s (average / 1% low /
0.1% low):
| RD | scene | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- | --- |
| 8 | forest | 3495 ±276 / 1295 ±397 / 859 ±298 | 2457 ±836 / 625 ±224 / 252 ±52 | 2425 ±65 / 733 ±30 / 250 ±22 |
| 8 | mountain | 3376 ±67 / 1384 ±168 / 850 ±256 | 2605 ±811 / 700 ±218 / 264 ±71 | 2538 ±167 / 760 ±34 / 241 ±44 |
| 8 | ocean | 3346 ±147 / 1358 ±237 / 912 ±235 | 2470 ±828 / 689 ±241 / 286 ±53 | 2432 ±82 / 764 ±37 / 262 ±37 |
| 8 | village | 2942 ±197 / 1271 ±146 / 839 ±94 | 2217 ±112 / 545 ±37 / 215 ±14 | 2104 ±101 / 577 ±84 / 196 ±47 |
| 16 | forest | 2424 ±138 / 709 ±165 / 401 ±100 | 1912 ±324 / 402 ±89 / 149 ±25 | 1746 ±32 / 461 ±27 / 179 ±9 |
| 16 | mountain | 1961 ±99 / 537 ±96 / 297 ±75 | 1653 ±153 / 332 ±27 / 113 ±8 | 1500 ±55 / 408 ±40 / 140 ±10 |
| 16 | ocean | 2201 ±183 / 554 ±251 / 325 ±326 | 1861 ±289 / 446 ±142 / 165 ±29 | 1815 ±119 / 514 ±142 / 192 ±58 |
| 16 | village | 1712 ±192 / 593 ±80 / 358 ±69 | 1218 ±65 / 318 ±16 / 127 ±5 | 1282 ±162 / 343 ±25 / 132 ±12 |
| 32 | forest | 1389 ±70 / 504 ±33 / 330 ±118 | 1203 ±51 / 228 ±13 / 108 ±12 | 929 ±8 / 313 ±41 / 129 ±32 |
| 32 | mountain | 1086 ±34 / 258 ±78 / 108 ±37 | 815 ±61 / 55 ±13 / 8 ±3 | 645 ±10 / 166 ±12 / 86 ±21 |
| 32 | ocean | 1189 ±27 / 155 ±9 / 68 ±8 | 1223 ±33 / 236 ±17 / 99 ±21 | 1017 ±14 / 325 ±38 / 117 ±28 |
| 32 | village | 1164 ±9 / 416 ±39 / 216 ±86 | 882 ±48 / 184 ±12 / 83 ±17 | 926 ±17 / 250 ±5 / 93 ±11 |
Startup, loading and memory (means of 3 games):
| RD | | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- | --- |
| 8 | start to title / in-world | 3.0 s / 6.1 s | 7.7 s / 13.9 s | 11.1 s / 17.6 s |
| 8 | mean scene load | 1.3 s | 4.0 s | 3.6 s |
| 8 | working set, steady / peak | 1082 / 1172 MB | 1913 / 2120 MB | 1904 / 2053 MB |
| 16 | start to title / in-world | 3.2 s / 7.2 s | 7.7 s / 19.9 s | 10.8 s / 23.5 s |
| 16 | mean scene load | 2.4 s | 9.2 s | 9.0 s |
| 16 | working set, steady / peak | 1236 / 1459 MB | 2300 / 2540 MB | 2327 / 2496 MB |
| 32 | start to title / in-world | 2.9 s / 10.5 s | 7.5 s / 35.4 s | 11.0 s / 39.2 s |
| 32 | mean scene load | 5.7 s | 21.1 s | 19.9 s |
| 32 | working set, steady / peak | 1730 / 2045 MB | 2799 / 2892 MB | 2895 / 2965 MB |
## What it shows
- At RD 8 and 16, Horizon's average FPS is 18–42 % above vanilla's in every scene. Its 1% lows are 1.2–2.3 times
vanilla's and its 0.1% lows 2.0–3.9 times. On this CPU and GPU the performance mods leave vanilla's averages
level or slightly lower, and raise its 1% lows by 6–23 %.
- At RD 32, Horizon's average is 15–33 % above vanilla's over the forest, the mountain and the village, with 1% lows
2.2–4.7 times vanilla's. Over the ocean its average is level with vanilla's, but its lows are lower than vanilla's
and the modded client's (155 vs 236 and 325 FPS 1% low, 68 vs 99 and 117 FPS 0.1% low), on an idle machine too.
That scene is worth a closer look.
- Horizon reaches the title screen 2.4–2.6 times faster than vanilla (3.4–3.8 times faster than the modded client)
and loads a scene's chunks 3–4 times faster. Its steady working set is 38–46 % smaller.
- Each game uses its own graphics API: Horizon Vulkan, vanilla OpenGL. That difference is part of what's compared.
@@ -119,7 +119,7 @@ machines during the runs.
## Reproduce
curl -fsSL https://raw.githubusercontent.com/horizon-rust/horizon/main/packaging/server/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
@@ -148,7 +148,7 @@ early, wrote no report. Its lines come from its output.
## Reproduce
curl -fsSL https://raw.githubusercontent.com/horizon-rust/horizon/main/packaging/server/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
+1 -1
View File
@@ -103,7 +103,7 @@ the Nix store, so the run set `LD_LIBRARY_PATH` to them (see #898).
## Reproduce
curl -fsSL https://raw.githubusercontent.com/horizon-rust/horizon/main/packaging/server/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
Loaded 100 of 3952 files, more files were not shown because too many files have changed in this diff. Show more