Move the website to its own repository (ludovic111/horizon-site)

playhorizon.dev now deploys from the private ludovic111/horizon-site, which holds website/'s history. The files
its build still reads from here move out of website/:

- the design tokens to crates/horizon-theme/tokens.css (xtask tokens writes them, tokens_css_is_up_to_date checks them);
- the roadmap's JSON to roadmap/roadmap.json (tools/roadmap.py build);
- the benchmark fixtures to crates/horizon-telemetry-server/tests/fixtures/benchmarks/ (the site keeps a copy);
- the web icons to docs/media/brand/web/ (xtask icons).

pr_ready loses its website area and the tokens special case; rust_changes, the roadmap's links and every doc
that pointed into website/ follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Ludovic MarieandClaude Opus 5.5 committed 2026-10-26 16:43:32 +01:00
1 parent 9cbf93a193
commit 4fe648b674
154 files changed
+97 -16630

No files matched your search

-1
View File
@@ -2,7 +2,6 @@
target/
.local/
**/node_modules/
website/
benchmarks/
packs/
mods/
+1 -1
View File
@@ -2,5 +2,5 @@
# where Git for Windows' core.autocrlf would check them out with CRLF.
docs/launcher-commands.md text eol=lf
docs/horizon-ctl-commands.md text eol=lf
website/src/tokens.css text eol=lf
crates/horizon-theme/tokens.css text eol=lf
docs/api/*-reference.md text eol=lf
+2 -1
View File
@@ -54,7 +54,8 @@ 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),
`website/` (playhorizon.dev; its paths are relative to `website/`), `docs/`, `.github/workflows/`.
`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.
## Build and test fast
+2 -2
View File
@@ -4,8 +4,8 @@ Thanks for helping. Horizon is Minecraft: Java Edition 26.3 rewritten in Rust, a
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/HcTeMZxe5E).
This repository holds the game, the launcher and the website, plus the tools, tests and docs that build, check and
release them. Anything else (videos, Discord and other community material) lives elsewhere.
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.
**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
+8 -4
View File
@@ -134,7 +134,7 @@ Better tools for mod makers: a test runner that needs no game window, templates
`cargo horizon-mod test` runs a mod's test scripts against a fake game snapshot ([#1816](https://github.com/ludovic111/horizon/issues/1816)) and, for server mods, scripted server events ([#1837](https://github.com/ludovic111/horizon/issues/1837)); the content template and the guides are merged ([#1859](https://github.com/ludovic111/horizon/issues/1859), [#1810](https://github.com/ludovic111/horizon/issues/1810), [docs/api/stability.md](docs/api/stability.md)). Left: templates for hud, server and machine mods, typed wrappers in the SDK, a frozen 0.1 compatibility mod that every runtime must load, a CI check that fails when a stable reference entry is removed without a new major version, and the SDK on crates.io once the API is stable.
More: [website/docs/test-a-mod.md](website/docs/test-a-mod.md)
More: [https://playhorizon.dev/docs/test-a-mod](https://playhorizon.dev/docs/test-a-mod)
</details>
@@ -310,7 +310,7 @@ Horizon should run well on small machines: integrated graphics, the Steam Deck,
Partly done: an opt-in 60 FPS cap with fewer workers on battery, Far Detail Reduced for integrated GPUs and animated textures on the GPU (both shipped). Left: measured targets and fixes per class of machine (memory use, worker counts, startup) on a Steam Deck, old Intel and Apple Silicon Macs and 8 GB computers, reported on the Benchmarks page.
More: [docs/benchmark.md](docs/benchmark.md) · [website/docs/performance.md](website/docs/performance.md)
More: [docs/benchmark.md](docs/benchmark.md) · [https://playhorizon.dev/docs/performance](https://playhorizon.dev/docs/performance)
</details>
@@ -600,7 +600,9 @@ You can port a mod to Horizon with your own AI agent on your own computer. Horiz
<details><summary>Technical</summary>
A player runs their own agent locally with `horizon-ctl` and the MCP servers ([docs/agent-control.md](docs/agent-control.md)), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide ([website/docs/agents.md](website/docs/agents.md), [docs/modpacks.md](docs/modpacks.md)) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.
A player runs their own agent locally with `horizon-ctl` and the MCP servers ([docs/agent-control.md](docs/agent-control.md)), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide (playhorizon.dev/docs/agents, [docs/modpacks.md](docs/modpacks.md)) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.
More: [https://playhorizon.dev/docs/agents](https://playhorizon.dev/docs/agents)
</details>
@@ -936,7 +938,9 @@ Run Horizon's built-in benchmark and share the report: every new computer makes
<details><summary>Technical</summary>
`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it ([website/docs/performance.md](website/docs/performance.md)). Results from three installs with the same hardware appear on the Benchmarks page. Example: [#1711](https://github.com/ludovic111/horizon/issues/1711).
`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it (playhorizon.dev/docs/performance). Results from three installs with the same hardware appear on the Benchmarks page. Example: [#1711](https://github.com/ludovic111/horizon/issues/1711).
More: [https://playhorizon.dev/docs/performance](https://playhorizon.dev/docs/performance)
</details>
+1 -1
View File
@@ -1,4 +1,4 @@
/* The Horizon server panel. Colours and spacing are horizon-theme's tokens (website/src/tokens.css). */
/* The Horizon server panel. Colours and spacing are horizon-theme's tokens (crates/horizon-theme/tokens.css). */
:root {
--page: #fafafc;
--plate-soft: #f3f3f8;
+1 -1
View File
@@ -30,7 +30,7 @@ The totals are grouped per version, OS, GPU vendor and render distance. Each gro
## Benchmarks summary
`src/summary.rs` makes it with the same rules as `website/scripts/benchmarks.mjs`, which bakes the page's fallback copy; both are tested against the same fixtures (`website/scripts/fixtures/benchmarks/`).
`src/summary.rs` makes it with the same rules as the website's `scripts/benchmarks.mjs` (ludovic111/horizon-site), which bakes the page's fallback copy; both are tested against the same fixtures (`tests/fixtures/benchmarks/` here, copied in the site's repository).
- Only reports that compare: a version 1 `horizon-benchmark-report` of the standard flight over the four scenes, VSync off, no frame cap, on screen, from Horizon or plain vanilla, naming its GPU and CPU.
- A group (GPU, CPU, render distance, game) is published only with **at least 3 different installs**. Each install counts once: the median of its runs, then the median across installs.
@@ -1,6 +1,6 @@
//! The hardware classes the public output breaks results down by. Coarse on purpose: a class
//! holds many machines, and a published cell still needs `MIN_INSTALLS` different installs.
//! `website/scripts/benchmarks.mjs` has the same functions, tested on the same fixtures.
//! The website's `scripts/benchmarks.mjs` (ludovic111/horizon-site) has the same functions, tested on the same fixtures.
//!
//! The raw values (the exact memory, the driver, the screen) stay in the database for the
//! retention period and are never published.
@@ -1,7 +1,7 @@
//! `GET /v1/benchmarks/summary`: players' benchmark uploads, grouped for the website's Benchmarks page.
//!
//! The same rules as `website/scripts/benchmarks.mjs` (which builds the page's baked copy), checked against the
//! same fixtures (`website/scripts/fixtures/benchmarks/`):
//! The same rules as the website's `scripts/benchmarks.mjs` (ludovic111/horizon-site; it builds the page's baked
//! copy), checked against the same fixtures (`tests/fixtures/benchmarks/`, which the site's test keeps a copy of):
//!
//! - Only reports that compare: `horizon-benchmark-report` version 1, kind `game`, Horizon or plain vanilla, the
//! standard flight over the four scenes, VSync off, no frame cap, drawn on a screen, with a GPU and CPU name.
@@ -198,7 +198,7 @@ struct Group {
versions: BTreeSet<String>,
}
/// Players' groups of at least `min_installs` installs, as `website/scripts/benchmarks.mjs` makes them.
/// Players' groups of at least `min_installs` installs, as the website's `scripts/benchmarks.mjs` makes them.
pub fn groups(uploads: &[Upload], min_installs: usize) -> Vec<Value> {
let mut by: BTreeMap<(String, String, i64, &str), Group> = BTreeMap::new();
for u in uploads {
@@ -1,7 +1,8 @@
# Benchmark summary fixtures
Shared by the website's aggregator test (`website/scripts/benchmarks.test.mjs`) and the telemetry server's
`GET /v1/benchmarks/summary` test: the same `benchmark` items in, the same groups out.
Used by the telemetry server's `GET /v1/benchmarks/summary` test (`tests/summary.rs`) and, as a copy, by the
website's aggregator test (`scripts/benchmarks.test.mjs` in ludovic111/horizon-site, which checks its copy against
this one on main): the same `benchmark` items in, the same groups out. Change both together.
- `items.jsonl`: one stored item per line (`install_id`, `day`, `version`, `data`: a benchmark report). Group
RTX 4080 + i7-13700KF at render distance 12 has three installs (one with two runs); the same install at 32,
@@ -1,5 +1,5 @@
//! `GET /v1/benchmarks/summary` against the website's fixtures (website/scripts/fixtures/benchmarks/): the same
//! uploads must give the groups `website/scripts/benchmarks.mjs` gives.
//! `GET /v1/benchmarks/summary` against the shared fixtures (tests/fixtures/benchmarks/): the same uploads must
//! give the groups the website's `scripts/benchmarks.mjs` (ludovic111/horizon-site) gives on its copy of them.
use std::sync::Arc;
use std::sync::atomic::AtomicU32;
@@ -15,10 +15,7 @@ use http_body_util::BodyExt;
use serde_json::Value;
use tower::ServiceExt;
const FIXTURES: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../website/scripts/fixtures/benchmarks"
);
const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/benchmarks");
fn items() -> Vec<Value> {
std::fs::read_to_string(format!("{FIXTURES}/items.jsonl"))
+8 -8
View File
@@ -1,6 +1,6 @@
//! Horizon's design tokens, the one source of truth for the launcher (`launcher/src/theme.rs`),
//! the game's on-screen keyboard (`horizon-client`'s `ui_theme`) and the website (`website/src/tokens.css`,
//! generated from [`css`] by `cargo run -p xtask -- tokens`). Change a value here and all three
//! the game's on-screen keyboard (`horizon-client`'s `ui_theme`) and the website (`tokens.css` in this crate,
//! generated from [`css`] by `cargo run -p xtask -- tokens`, which the site's build reads from main). Change a value here and all three
//! follow. The system is described in `docs/design-system.md`.
//!
//! Cool neutrals in two appearances and one violet accent; Geist and Geist Mono only, bundled
@@ -528,7 +528,7 @@ pub fn contrast(a: u32, b: u32) -> f32 {
(x.max(y) + 0.05) / (x.min(y) + 0.05)
}
/// `website/src/tokens.css`: every token as a CSS custom property, the light palette by
/// `tokens.css`: every token as a CSS custom property, the light palette by
/// default, the dark one when the system asks for it or the page sets `data-theme="dark"`.
pub fn css() -> String {
let mut s = String::new();
@@ -771,15 +771,15 @@ mod tests {
assert!((bezier([0., 0., 1., 1.], 0.3) - 0.3).abs() < 1e-3);
}
/// `website/src/tokens.css` is what [`css`] writes; regenerate it with
/// `cargo run -p xtask -- tokens`.
/// `tokens.css` (beside Cargo.toml, read by the website's build) is what [`css`] writes;
/// regenerate it with `cargo run -p xtask -- tokens`.
#[test]
fn website_tokens_are_up_to_date() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../website/src/tokens.css");
fn tokens_css_is_up_to_date() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tokens.css");
let written = std::fs::read_to_string(path).unwrap_or_default();
assert!(
written == css(),
"website/src/tokens.css is out of date: run `cargo run -p xtask -- tokens`"
"crates/horizon-theme/tokens.css is out of date: run `cargo run -p xtask -- tokens`"
);
}
}
File renamed without changes.
+1 -1
View File
@@ -3,7 +3,7 @@
A player's own AI agent (Claude Code, Codex, Cursor, any MCP client) or a script can do
everything a player does with the mouse and keyboard, in the launcher and in the game. This page
covers developers: the surfaces, the full inventory, and the rules every new command follows.
Players read [Control Horizon with your AI agent](../website/docs/agents.md)
Players read [Control Horizon with your AI agent](https://playhorizon.dev/docs/agents)
(playhorizon.dev/docs/agents).
The command lists are generated, so they can't drift:
+1 -1
View File
@@ -53,4 +53,4 @@ Every page has the same parts: **Overview** (what you can build), **Guide** (whe
The website shows these pages at [playhorizon.dev/docs/api](https://playhorizon.dev/docs/api), built from this
folder, and a summary for players and authors at [playhorizon.dev/docs/apis](https://playhorizon.dev/docs/apis)
(`website/docs/apis.md`).
(in the website's repository, ludovic111/horizon-site).
+4 -4
View File
@@ -8,8 +8,8 @@ Horizon's own settings are pages built from vanilla's widgets, the way mods add
The tokens live in one place, `crates/horizon-theme`. The launcher (`launcher/src/theme.rs`)
and the game (`crates/horizon-client/src/ui_theme.rs`) read them as Rust constants, and the
website reads them as CSS custom properties generated from the same crate
(`cargo run -p xtask -- tokens` writes `website/src/tokens.css`; a test fails when the file is
out of date). Change a value in the crate and all three follow.
(`cargo run -p xtask -- tokens` writes `crates/horizon-theme/tokens.css`, which the website's build
reads from main; a test fails when the file is out of date). Change a value in the crate and all three follow.
## Where it comes from
@@ -140,7 +140,7 @@ Geist (400, 500, 600) and Geist Mono (400), bundled under the SIL Open Font Lice
landscape banner with the hero, the version picker and Play; Mods holds the mod, resource pack
and modpack tabs; Versions and Patch notes are lists of panels; Settings is
grouped panels of rows. Dark by default, Light or System in Settings (Appearance).
- **Website** (`website/`): the Blocks look below. The same page structure as before (what it
- **Website** (ludovic111/horizon-site): the Blocks look below. The same page structure as before (what it
is, screenshots, parity, launcher, patch notes, community), headings in Pixelify Sans, lists as
stacks of panels, the stone texture behind the page and Horizon's pixel landscape (a dawn or a
sunset) behind the hero. Light and dark follow the system, with a toggle. A giant wordmark in
@@ -177,7 +177,7 @@ of the main action. The game's menus keep `LIGHT` and `DARK`.
(`TILE_SHADES`), never Minecraft's stone; the landscape is a banded sky, a sun cut in slices like
the mark, hills and square trees (a sunset in dark, a dawn in light). The launcher bakes them in
`launcher/src/art.rs` at the window's scale so each texel covers whole device pixels; the website
draws them in `website/scripts/art.mjs`.
draws them in its `scripts/art.mjs`.
- **Icons**: Pixelarticons (MIT), 24 unit pixel SVGs in `crates/horizon-theme/icons/pixel/`, tinted
with the text colour.
- **Contrast**: every text colour meets WCAG AA (4.5:1) on the panels, the page and the lightest
+4 -4
View File
@@ -1,10 +1,10 @@
<!doctype html>
<!-- README banner (docs/media/banner.webp), 1600x480, in the design of docs/design-system.md.
Rendered with Playwright from website/ (see docs/media/brand/README.md). -->
Rendered with Playwright (see docs/media/brand/README.md). -->
<meta charset="utf-8"><style>
@font-face{font-family:Geist;src:url(../../website/public/fonts/Geist-Regular.woff2);font-weight:400}
@font-face{font-family:Geist;src:url(../../website/public/fonts/Geist-Medium.woff2);font-weight:500}
@font-face{font-family:"Geist Mono";src:url(../../website/public/fonts/GeistMono-Regular.woff2)}
@font-face{font-family:Geist;src:url(../../crates/horizon-theme/fonts/Geist-Regular.ttf);font-weight:400}
@font-face{font-family:Geist;src:url(../../crates/horizon-theme/fonts/Geist-Medium.ttf);font-weight:500}
@font-face{font-family:"Geist Mono";src:url(../../crates/horizon-theme/fonts/GeistMono-Regular.ttf)}
html,body{margin:0;width:1600px;height:480px;overflow:hidden;background:#0d0e13;color:#ecedf3;font-family:Geist}
main{position:relative;height:100%;padding:56px 72px;box-sizing:border-box}
.top{display:flex;align-items:center;gap:12px;font-size:20px;font-weight:500}
+4 -5
View File
@@ -26,16 +26,15 @@ places it in the design system; `candidates/` keeps the two alternatives that we
| `packaging/icons/horizon.ico` | Windows: embedded in `horizon-client.exe`, its tools and `horizon-launcher.exe` as icon resource 1 (`build.rs` of each, Windows builds only) |
| `packaging/icons/horizon.icns` | macOS: `Horizon.app/Contents/Resources/horizon.icns`, packaged with the .dmg by `packaging/macos/package.sh` |
| `crates/horizon-theme/icons/horizon-64.png` | the game window's icon at run time (X11 and Windows title bars, `window::icon`) |
| `website/public/favicon.svg`, `favicon-32.png`, `apple-touch-icon.png` | the website |
| `web/favicon.svg`, `web/favicon-32.png`, `web/apple-touch-icon.png` | the website (its `npm run assets` copies them into its `public/`) |
| `icon-1024.png`, `icon-macos-1024.png` | previews, store listings |
| `social/github-avatar-500.png` | GitHub: the organisation or account avatar |
The page in `social/` is a screenshot, at their exact size, with Playwright from the
website's dev dependencies:
The page in `social/` is a screenshot, at their exact size, with Playwright, from the repository's root:
```sh
cd website && npm install
node -e 'const {chromium}=require("playwright");(async()=>{const b=await chromium.launch();for(const[f,w,h,o]of[["card",1280,640,"github-social-1280x640"]]){const p=await b.newPage({viewport:{width:w,height:h}});await p.goto("file://"+require("path").resolve("../docs/media/brand/social/"+f+".html"));await p.waitForTimeout(300);await p.screenshot({path:"../docs/media/brand/social/"+o+".png"})}await b.close()})()'
d=$(mktemp -d) && npm install --prefix "$d" playwright && npx --prefix "$d" playwright install chromium
NODE_PATH="$d/node_modules" node -e 'const {chromium}=require("playwright");(async()=>{const b=await chromium.launch();for(const[f,w,h,o]of[["card",1280,640,"github-social-1280x640"]]){const p=await b.newPage({viewport:{width:w,height:h}});await p.goto("file://"+require("path").resolve("docs/media/brand/social/"+f+".html"));await p.waitForTimeout(300);await p.screenshot({path:"docs/media/brand/social/"+o+".png"})}await b.close()})()'
```
## Drawn in code
+3 -3
View File
@@ -1,7 +1,7 @@
<!doctype html><meta charset=utf-8><style>
@font-face{font-family:Geist;src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/Geist-Regular.woff2);font-weight:400}
@font-face{font-family:Geist;src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/Geist-Medium.woff2);font-weight:500}
@font-face{font-family:"Geist Mono";src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/GeistMono-Regular.woff2)}
@font-face{font-family:Geist;src:url(../../../../crates/horizon-theme/fonts/Geist-Regular.ttf);font-weight:400}
@font-face{font-family:Geist;src:url(../../../../crates/horizon-theme/fonts/Geist-Medium.ttf);font-weight:500}
@font-face{font-family:"Geist Mono";src:url(../../../../crates/horizon-theme/fonts/GeistMono-Regular.ttf)}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#0d0e13;color:#ecedf3;font-family:Geist}
.wrap{position:relative;height:100%;padding:var(--pad);box-sizing:border-box}
.top{display:flex;align-items:center;gap:12px;font-size:var(--small);font-weight:500}
File renamed without changes.

Before

Width:  |  Height:  |  Size: 733 B

After

Width:  |  Height:  |  Size: 733 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 424 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 537 B

After

Width:  |  Height:  |  Size: 537 B

+1 -1
View File
@@ -96,7 +96,7 @@ The launcher reads it when it opens and every minute after, and shows "N playing
- `GET /v1/benchmarks/summary`: players' benchmark uploads grouped by GPU, CPU, render distance and game for playhorizon.dev/benchmarks. A group shows only with at least 3 different installs, and only its medians and counts: no install ID, day, OS version, driver, exact memory or screen. Uploads are never published one by one. Each group's `breakdowns` count its installs per hardware class.
- `POST /v1/live` and `GET /v1/live`: the "I'm playing" ping and the live count.
Public output never holds a raw hardware value, only these classes (`src/classes.rs`, the same in `website/scripts/benchmarks.mjs`), and a class is published only with at least 3 different installs:
Public output never holds a raw hardware value, only these classes (`src/classes.rs`, the same in the website's `scripts/benchmarks.mjs`, ludovic111/horizon-site), and a class is published only with at least 3 different installs:
| Breakdown | Classes |
|---|---|
+1 -1
View File
@@ -44,7 +44,7 @@ A tag is a 0.2 tag when it is `v` or `launcher@` followed by exactly three numbe
major below 1000; a date's first number is a year, so `v2026.10.21` never is. The rule is written once per
language, with the same tests: `crates/horizon-server/src/update.rs` (`newer`), `launcher/src/backend.rs`
(`release_order`), `tools/release_train.py` (`tag_key`) and
`website/src/versions.js`.
the website's `src/versions.js` (ludovic111/horizon-site).
## The workflows
+1 -1
View File
@@ -11,7 +11,7 @@ use std::sync::{Arc, Mutex};
pub const REPO: &str = "ludovic111/horizon";
pub const README_URL: &str = "https://github.com/ludovic111/horizon/blob/main/launcher/README.md";
/// The players' page on connecting an AI agent (website/docs/agents.md).
/// The players' page on connecting an AI agent (the website's docs/agents.md).
pub const AGENT_DOCS_URL: &str = "https://playhorizon.dev/docs/agents";
/// This launcher's version: the release workflow sets `HORIZON_LAUNCHER_VERSION` from the
/// tag it builds (no version-bump commits); other builds say Cargo.toml's.
+3 -3
View File
@@ -1,9 +1,9 @@
<!doctype html>
<!-- The .dmg window's background (background.png 660x420, [email protected] 1320x840).
Render: from website/, node -e with Playwright at deviceScaleFactor 1 and 2 (see README). -->
Render: from the repository's root, node -e with Playwright at deviceScaleFactor 1 and 2 (see README). -->
<meta charset="utf-8"><style>
@font-face{font-family:Geist;src:url(../../website/public/fonts/Geist-Medium.woff2);font-weight:500}
@font-face{font-family:"Geist Mono";src:url(../../website/public/fonts/GeistMono-Regular.woff2)}
@font-face{font-family:Geist;src:url(../../crates/horizon-theme/fonts/Geist-Medium.ttf);font-weight:500}
@font-face{font-family:"Geist Mono";src:url(../../crates/horizon-theme/fonts/GeistMono-Regular.ttf)}
html,body{margin:0;width:660px;height:420px;overflow:hidden;background:#fafafc;font-family:Geist;color:#16181f}
.arrow{position:absolute;left:262px;top:199px;width:136px;height:1px;background:#5b3fd6}
.arrow::after{content:"";position:absolute;right:-1px;top:-4px;width:8px;height:8px;border-top:1px solid #5b3fd6;border-right:1px solid #5b3fd6;transform:rotate(45deg)}
+2 -2
View File
@@ -52,7 +52,7 @@ Whoever moves the work (the orchestrator on each heartbeat, or anyone in their p
removed items are listed, never deleted.
5. Once a week, write the week's summary in `roadmap/journal/<year>-<ISO week>.md` (for example
`2026-43.md`): `# Title`, a paragraph or two for players, then `## Technical` and the same week for
contributors, with its pull requests. `build` puts it into website/data/roadmap.json; the hub's Journal tab
contributors, with its pull requests. `build` puts it into roadmap/roadmap.json, which the website reads from main; the hub's Journal tab
shows it among the releases, and `/data/journal.json` carries it to Kira's #updates and to X.
6. Suggestions from the website and Discord reach the maintainers' private channel through Kira (below). A good
one becomes an item in a normal pull request; none is published automatically.
@@ -63,7 +63,7 @@ or when the file breaks a rule; `python3 tools/roadmap.py check --offline` runs
## The journal
The hub's Journal tab and `https://playhorizon.dev/data/journal.json` are built with each deploy of the website
(`website/src/hub.js`). The tab shows every game and launcher release with its notes' changes, each placed under
(`src/hub.js` in ludovic111/horizon-site). The tab shows every game and launcher release with its notes' changes, each placed under
the roadmap item it moves (the pull request's `roadmap:<id>` label, else the item that links it), the merged pull
requests no release has yet, and the weekly summaries above.
@@ -320,7 +320,7 @@
"#1816",
"#1837",
"docs/api/stability.md",
"website/docs/test-a-mod.md"
"https://playhorizon.dev/docs/test-a-mod"
]
},
{
@@ -434,7 +434,7 @@
"dev": "Partly done: an opt-in 60 FPS cap with fewer workers on battery, Far Detail Reduced for integrated GPUs and animated textures on the GPU (both shipped). Left: measured targets and fixes per class of machine (memory use, worker counts, startup) on a Steam Deck, old Intel and Apple Silicon Macs and 8 GB computers, reported on the Benchmarks page.",
"links": [
"docs/benchmark.md",
"website/docs/performance.md"
"https://playhorizon.dev/docs/performance"
]
},
{
@@ -677,9 +677,9 @@
"status": "next",
"area": "ai",
"plain": "You can port a mod to Horizon with your own AI agent on your own computer. Horizon documents how and gives you the tools, but provides no hosted agents.",
"dev": "A player runs their own agent locally with `horizon-ctl` and the MCP servers (docs/agent-control.md), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide (website/docs/agents.md, docs/modpacks.md) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.",
"dev": "A player runs their own agent locally with `horizon-ctl` and the MCP servers (docs/agent-control.md), `cargo horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide (playhorizon.dev/docs/agents, docs/modpacks.md) and runs no agents, accounts or keys for players. The No AI switch turns all of it off.",
"links": [
"website/docs/agents.md",
"https://playhorizon.dev/docs/agents",
"docs/modpacks.md",
"docs/agent-control.md"
]
@@ -1001,9 +1001,9 @@
"status": "help-wanted",
"area": "performance",
"plain": "Run Horizon's built-in benchmark and share the report: every new computer makes the performance work more accurate. No coding needed.",
"dev": "`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it (website/docs/performance.md). Results from three installs with the same hardware appear on the Benchmarks page. Example: #1711.",
"dev": "`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data benchmark <report>` shows what would be sent and uploads it (playhorizon.dev/docs/performance). Results from three installs with the same hardware appear on the Benchmarks page. Example: #1711.",
"links": [
"website/docs/performance.md",
"https://playhorizon.dev/docs/performance",
"#1711"
]
},
+6 -6
View File
@@ -273,7 +273,7 @@ mods, scripted server events (#1837); the content template and the guides are me
docs/api/stability.md). Left: templates for hud, server and machine mods, typed wrappers in the SDK, a \
frozen 0.1 compatibility mod that every runtime must load, a CI check that fails when a stable reference \
entry is removed without a new major version, and the SDK on crates.io once the API is stable."""
links = ["#1816", "#1837", "docs/api/stability.md", "website/docs/test-a-mod.md"]
links = ["#1816", "#1837", "docs/api/stability.md", "https://playhorizon.dev/docs/test-a-mod"]
[[item]]
id = "pregen"
@@ -391,7 +391,7 @@ dev = """Partly done: an opt-in 60 FPS cap with fewer workers on battery, Far De
animated textures on the GPU (both shipped). Left: measured targets and fixes per class of machine (memory \
use, worker counts, startup) on a Steam Deck, old Intel and Apple Silicon Macs and 8 GB computers, reported \
on the Benchmarks page."""
links = ["docs/benchmark.md", "website/docs/performance.md"]
links = ["docs/benchmark.md", "https://playhorizon.dev/docs/performance"]
[[item]]
id = "upscaling"
@@ -661,9 +661,9 @@ plain = """You can port a mod to Horizon with your own AI agent on your own comp
you the tools, but provides no hosted agents."""
dev = """A player runs their own agent locally with `horizon-ctl` and the MCP servers (docs/agent-control.md), `cargo \
horizon-mod`, `horizon-port-pack` and the parity harnesses as the check. Horizon writes the guide \
(website/docs/agents.md, docs/modpacks.md) and runs no agents, accounts or keys for players. The No AI \
(playhorizon.dev/docs/agents, docs/modpacks.md) and runs no agents, accounts or keys for players. The No AI \
switch turns all of it off."""
links = ["website/docs/agents.md", "docs/modpacks.md", "docs/agent-control.md"]
links = ["https://playhorizon.dev/docs/agents", "docs/modpacks.md", "docs/agent-control.md"]
[[item]]
id = "x-account"
@@ -1011,9 +1011,9 @@ area = "performance"
plain = """Run Horizon's built-in benchmark and share the report: every new computer makes the performance work \
more accurate. No coding needed."""
dev = """`horizon-launcher play --arg=--benchmark --arg=standard` writes a JSON report; `horizon-launcher data \
benchmark <report>` shows what would be sent and uploads it (website/docs/performance.md). Results from three \
benchmark <report>` shows what would be sent and uploads it (playhorizon.dev/docs/performance). Results from three \
installs with the same hardware appear on the Benchmarks page. Example: #1711."""
links = ["website/docs/performance.md", "#1711"]
links = ["https://playhorizon.dev/docs/performance", "#1711"]
[[item]]
id = "end-city-banners"
+2 -2
View File
@@ -11,12 +11,12 @@ class RustChangesTests(unittest.TestCase):
def test_only_content_changes_skip_rust(self):
self.assertFalse(needs_rust([]))
self.assertFalse(needs_rust([
"website/src/index.html", "website/src/main.ts", "docs/setup.md", "README.md",
"docs/setup.md", "docs/media/banner.html", "README.md",
]))
for path in [
"crates/horizon-client/src/game.rs", "xtask/src/main.rs", "Cargo.lock",
".cargo/config.toml", ".github/workflows/ci.yml", "tools/ci/rust_changes.py",
"research-lock.json", "website/src/tokens.css", "unknown-file",
"research-lock.json", "crates/horizon-theme/tokens.css", "unknown-file",
]:
with self.subTest(path=path):
self.assertTrue(needs_rust(["README.md", path]))
+1 -1
View File
@@ -23,7 +23,7 @@ class PrReadyTests(unittest.TestCase):
["horizon-client", "horizon-core", "xtask"])
self.assertEqual(pr_ready.touched_crates({"crates/horizon-core/README.md"}, PACKAGES), [])
# The generated stylesheet is checked by horizon-theme's tests.
self.assertEqual(pr_ready.touched_crates({"website/src/tokens.css"}, PACKAGES), ["horizon-theme"])
self.assertEqual(pr_ready.touched_crates({"crates/horizon-theme/tokens.css"}, PACKAGES), ["horizon-theme"])
# A crate whose name prefixes another's directory doesn't own it.
self.assertEqual(pr_ready.touched_crates({"crates/horizon-core-extra/x.rs"}, PACKAGES), [])
# The workspace manifest and lock touch every crate.
+2 -5
View File
@@ -1,15 +1,12 @@
#!/usr/bin/env python3
"""Keep verify's Rust steps for any change beyond website content or documentation."""
"""Keep verify's Rust steps for any change beyond documentation."""
import argparse
import subprocess
def needs_rust(paths):
# horizon-theme's Rust tests check that the generated website tokens match.
return any(path == "website/src/tokens.css" or not (
path.startswith(("website/", "docs/")) or path.endswith(".md")
) for path in paths)
return any(not (path.startswith("docs/") or path.endswith(".md")) for path in paths)
def changed_paths(base, head):
+6 -12
View File
@@ -8,7 +8,7 @@
From the files the branch changes since its merge base with --base (committed or not) it decides what to run:
rustfmt, clippy with warnings denied and the tests of the touched crates; the launcher (its own workspace) the
same way when it, horizon-prepare or horizon-theme changed; website/src/tokens.css against horizon-theme;
same way when it, horizon-prepare or horizon-theme changed; crates/horizon-theme/tokens.css against horizon-theme;
Cargo.lock changing only with a Cargo.toml; the Python tests and the public-tree check; and the vanilla
harnesses the changes can affect, picked as CI's parity job picks them (tools/parity.py --changed). Harnesses run
where the vanilla data is (a prepared .local/ here, or the Mac mini); elsewhere they are listed for CI.
@@ -68,7 +68,6 @@ mkdir -p "$state/parity-runner" "$state/parity-target"
ln -sfn "$state/parity-runner" .local/parity-runner && ln -sfn "$state/parity-target" .local/parity-target""",
}
ROOT_RUST = {"Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "rustfmt.toml", "clippy.toml"}
TOKENS = "website/src/tokens.css"
LAUNCHER_FEATURES = """export features=""
if [ "$(uname)" = Darwin ] && ! xcrun metal --version > /dev/null 2>&1; then features="--features gpui/runtime_shaders"; fi
in_shell() { if [ "$(uname)" = Linux ] && ! pkg-config --exists xkbcommon 2>/dev/null && command -v nix-shell > /dev/null; then
@@ -96,8 +95,6 @@ def touched_crates(paths, packages):
return None
crates = set()
for path in paths:
if path == TOKENS:
path = packages.get("horizon-theme", "crates/horizon-theme") + "/"
if is_doc(path):
continue
owners = [name for name, directory in packages.items() if path.startswith(directory.rstrip("/") + "/")]
@@ -113,12 +110,10 @@ def area(path):
return None
if path.startswith(("launcher/", "crates/horizon-prepare/")):
return "launcher"
if path.startswith("website/"):
return "website"
if path.startswith((".github/", "tools/ci/")):
return "CI"
if path.startswith("crates/horizon-theme/"):
return None # the design system feeds the game, the launcher and the website alike
return None # the design system feeds the game, the launcher and the website (its tokens.css) alike
return "game"
@@ -292,7 +287,7 @@ def plan(args, base, changed, crates_of, libraries, dependencies):
# The Python tests also hold ROADMAP.md to roadmap/roadmap.toml (tests/test_roadmap.py).
python = any(path.endswith(".py") or path.startswith(("tests/", "tools/", "roadmap/")) or path == "ROADMAP.md"
for path in changed)
crates = touched_crates(changed, crates_of) if rust or TOKENS in changed else []
crates = touched_crates(changed, crates_of) if rust else []
# An unrecognised Rust path must not silently skip validation.
if rust and crates == []:
crates = None
@@ -317,8 +312,8 @@ def plan(args, base, changed, crates_of, libraries, dependencies):
skip=test_skip, summary=tests_passed),
Step("doc tests", f"cargo test --locked {doc_selection} --doc",
skip=test_skip or (None if doc_crates else "no library touched"), summary=tests_passed),
Step("tokens.css up to date", "cargo test --locked -p horizon-theme --lib website_tokens_are_up_to_date",
skip=None if any(p.startswith(("crates/horizon-theme/", "xtask/")) or p == TOKENS for p in changed)
Step("tokens.css up to date", "cargo test --locked -p horizon-theme --lib tokens_css_is_up_to_date",
skip=None if any(p.startswith(("crates/horizon-theme/", "xtask/")) for p in changed)
else "theme untouched"),
]
launcher_skip = None if launcher else "launcher untouched"
@@ -500,8 +495,7 @@ def main():
parser.error(f"no merge base with {args.base} (git fetch?)")
merge_base = git("merge-base", "HEAD", args.base).strip()
counts = line_counts(merge_base, changed)
crates_of, libraries, dependencies = packages() if any(workspace_rust(p) for p in changed) or TOKENS in changed \
else ({}, set(), {})
crates_of, libraries, dependencies = packages() if any(workspace_rust(p) for p in changed) else ({}, set(), {})
steps = plan(args, args.base, changed, crates_of, libraries, dependencies)
print(f"pr_ready: {len(changed)} files changed since {args.base}; checks on {args.host}", file=sys.stderr, flush=True)
+9 -6
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Horizon's roadmap: roadmap/roadmap.toml is the source, ROADMAP.md is generated from it.
python3 tools/roadmap.py build write ROADMAP.md and website/data/roadmap.json
python3 tools/roadmap.py build write ROADMAP.md and roadmap/roadmap.json
python3 tools/roadmap.py build --check fail when either is out of date (CI)
python3 tools/roadmap.py check --offline the file's own rules (CI)
python3 tools/roadmap.py check the same, then GitHub: links resolve, no stale Now item
@@ -28,8 +28,9 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / "roadmap" / "roadmap.toml"
OUTPUT = ROOT / "ROADMAP.md"
# What the website's hub reads (website/build.mjs), and serves at /data/roadmap.json for Kira and others.
SITE_DATA = ROOT / "website" / "data" / "roadmap.json"
# What the website's hub reads from main at build time (ludovic111/horizon-site), and serves at /data/roadmap.json
# for Kira and others.
SITE_DATA = ROOT / "roadmap" / "roadmap.json"
# The journal's weekly summaries, one file per ISO week: roadmap/journal/2026-43.md (roadmap/README.md).
JOURNAL = ROOT / "roadmap" / "journal"
WEEK = re.compile(r"(\d{4})-(\d{2})\.md$")
@@ -259,7 +260,7 @@ def weeks(directory=JOURNAL):
def site_data(data):
"""website/data/roadmap.json: the items as the hub and Kira read them, in the file's order."""
"""roadmap/roadmap.json: the items as the hub and Kira read them, in the file's order."""
keys = ("id", "title", "status", "area", "plain", "dev", "links", "focus", "ongoing", "shipped_in", "post",
"announce")
out = {
@@ -375,7 +376,9 @@ def online_problems(data, states, now):
# --- sync -----------------------------------------------------------------------------------------------------
def family(paths):
"""Which releases carry a change: the launcher's, the website (deployed from main) or the game's."""
"""Which releases carry a change: the launcher's, the website (deployed when merged) or the game's. The site
left this repository on 2026-10-26 (ludovic111/horizon-site), so only older pull requests are website-only;
an item the site alone ships takes shipped_in = "website" by hand."""
if paths and all(path.startswith("launcher/") for path in paths):
return "launcher"
if paths and all(path.startswith("website/") for path in paths):
@@ -563,7 +566,7 @@ def command_sync(args):
SOURCE.write_text(text)
for path, content in outputs(new).items():
path.write_text(content)
print("roadmap: wrote roadmap/roadmap.toml, ROADMAP.md and website/data/roadmap.json")
print("roadmap: wrote roadmap/roadmap.toml, ROADMAP.md and roadmap/roadmap.json")
return 0
-3
View File
@@ -1,3 +0,0 @@
node_modules/
dist/
shots/
-73
View File
@@ -1,73 +0,0 @@
# Serves dist/ on Railway's $PORT. Fingerprinted CSS and JS are cached for a year,
# media for a week, and the page itself is always revalidated. The site lives at
# https://playhorizon.dev: Railway's own *.up.railway.app address redirects there, so the
# page is indexed once. Railway's healthcheck (Host: healthcheck.railway.app) is still served.
{
admin off
auto_https off
persist_config off
}
:{$PORT:8080} {
root * /srv
@railway header_regexp Host ^[^.]+\.up\.railway\.app(:[0-9]+)?$
redir @railway https://playhorizon.dev{uri} permanent
encode zstd gzip
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
X-Frame-Options DENY
Permissions-Policy "interest-cohort=(), browsing-topics=(), camera=(), microphone=(), geolocation=()"
Content-Security-Policy "default-src 'self'; script-src 'self' {$CSP_SCRIPT}; style-src 'self' {$CSP_STYLE}; img-src 'self' data: https://avatars.githubusercontent.com https://ludovic111.github.io; font-src 'self'; connect-src 'self' https://api.github.com https://raw.githubusercontent.com https://telemetry.playhorizon.dev https://ludovic111.github.io; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
-Server
}
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"
@media path /media/* /fonts/* /og.png /favicon* /apple-touch-icon.png
header @media Cache-Control "public, max-age=604800"
# The server guide became a section of the documentation.
redir /server /docs/hosting permanent
redir /server.html /docs/hosting permanent
rewrite /privacy /privacy.html
rewrite /releases /releases.html
rewrite /benchmarks /benchmarks.html
rewrite /hub /hub.html
# The roadmap is the hub's Roadmap tab.
redir /roadmap /hub#roadmap permanent
@doc path_regexp doc ^/docs/([a-z-]+)$
rewrite @doc /docs/{re.doc.1}.html
# The API portal: /docs/api is its index, /docs/api/<page> its pages.
redir /docs/api/ /docs/api permanent
@apidoc path_regexp apidoc ^/docs/api/([a-z-]+)$
rewrite @apidoc /docs/api/{re.apidoc.1}.html
# The registry's submission service (crates/horizon-mod-registry), over Railway's private network. Unset, the
# form says it isn't open.
handle /api/mods/* {
reverse_proxy {$REGISTRY_UPSTREAM:127.0.0.1:9}
}
# The registry: /mods, /mods/submit and /mods/<id> baked at build time, else the list page, whose script shows a project
# published since.
rewrite /mods /mods.html
@mod {
path_regexp ^/mods/[a-z0-9_]+$
file {path}.html /mods.html
}
rewrite @mod {file_match.relative}
@pages path / /index.html /404.html /privacy.html /releases /releases.html /benchmarks /benchmarks.html /benchmarks.json /hub /hub.html /data/* /docs /docs/* /mods /mods.html /mods/* /sitemap.xml
header @pages Cache-Control "no-cache"
# The hub's data for Kira and anyone else: readable from any origin.
@data path /data/*
header @data Access-Control-Allow-Origin "*"
@legal path /LEGAL.md /fonts/*.txt
header @legal Content-Type "text/plain; charset=utf-8"
file_server {
hide .csp-script .csp-style og.html
}
handle_errors {
rewrite * /404.html
file_server
}
}
-20
View File
@@ -1,20 +0,0 @@
# Railway builds this with the service's root directory set to `website`.
# Stage 1 renders dist/ from src/ and the README on main (no npm dependencies needed).
FROM node:22-alpine AS build
WORKDIR /site
COPY build.mjs ./
COPY src ./src
COPY docs ./docs
COPY data ./data
COPY public ./public
COPY LEGAL.md ./
ARG GITHUB_TOKEN
RUN node build.mjs
# Stage 2 serves it with Caddy on $PORT.
FROM caddy:2-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /site/dist /srv
ENV PORT=8080
# The Content-Security-Policy allows the page's inline script and style by the hashes the build wrote.
CMD ["sh", "-c", "CSP_SCRIPT=\"$(cat /srv/.csp-script)\" CSP_STYLE=\"$(cat /srv/.csp-style)\" exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile"]
-88
View File
@@ -1,88 +0,0 @@
# The website's assets and their licences
Every file the site serves, where it comes from and the terms it is used under. The build
copies this file to `/LEGAL.md` on the site.
Horizon is not an official Minecraft product. It is not approved by or associated with
Mojang or Microsoft. Minecraft is a trademark of Mojang Synergies AB.
## Code
| File | Source | Licence |
| --- | --- | --- |
| `src/*.html`, `src/*.css`, `src/*.js`, `build.mjs`, `serve.mjs`, `scripts/*`, `Caddyfile`, `Dockerfile` | Written for this site | MIT, as the rest of Horizon ([LICENSE](https://github.com/ludovic111/horizon/blob/main/LICENSE)) |
The site loads no third-party code at runtime: no frameworks, CDNs, analytics, trackers or
cookies. The only requests to other hosts are the browser's calls to `api.github.com`
(releases and patch notes) and `raw.githubusercontent.com` (`launcher/links.json`), both on Horizon's own repository.
## Fonts
| File | Source | Licence |
| --- | --- | --- |
| `public/fonts/Geist-Regular.woff2`, `Geist-Medium.woff2`, `Geist-SemiBold.woff2`, `GeistMono-Regular.woff2` | Geist and Geist Mono, from `crates/horizon-theme/fonts` (converted from TTF to WOFF2) | SIL Open Font License 1.1, shipped as `public/fonts/OFL-Geist.txt` |
| `public/fonts/PixelifySans-Medium.woff2`, `PixelifySans-SemiBold.woff2`, `PixelifySans-Bold.woff2` | Pixelify Sans by The Pixelify Sans Project Authors (github.com/eifetx/Pixelify-Sans), from `crates/horizon-theme/fonts` (converted from TTF to WOFF2) | SIL Open Font License 1.1, shipped as `public/fonts/OFL-PixelifySans.txt` |
The fonts are self-hosted; nothing is fetched from a font service.
## Icons and the social preview
| File | Source | Licence |
| --- | --- | --- |
| `public/favicon.svg`, `public/favicon-32.png`, `public/apple-touch-icon.png` | Horizon's mark, `docs/media/brand/icon.svg` (an original drawing: an H whose crossbar is the horizon), rendered by `cargo run -p xtask -- icons` | MIT, Horizon's original work |
| `public/og.png` | Rendered by `scripts/shots.mjs` from `public/og.html`: the mark, a headline and the faint wordmark, in Geist | MIT for the layout; the fonts as listed here |
No Minecraft logo or wordmark appears anywhere on the site, in images or as text styling.
GitHub and Discord appear only as plain text links, without their logos.
## The pixel art
| Files in `public/media/` | Source | Licence |
| --- | --- | --- |
| `stone-light.png`, `stone-dark.png` | The page's stone texture, drawn in code by `scripts/art.mjs` from the page colours of `crates/horizon-theme` | MIT, Horizon's original work |
| `scene-light.png`, `scene-dark.png` | The hero's landscape (a dawn and a sunset with Horizon's sliced sun), drawn in code by `scripts/art.mjs`, the same drawing as the launcher's Home banner (`launcher/src/art.rs`) | MIT, Horizon's original work |
No Minecraft texture is used: the stone is not Minecraft's stone, only shades of the page colour.
The launcher's old background (`launcher/assets/background.jpg`, from backgrounds.supply, licence
never confirmed) is gone from the repository, so the launcher's screenshots no longer show it.
## Screenshots
All captured in Horizon's Rust client, with vanilla resources prepared from the maintainer's
own copy of Minecraft: Java Edition 26.3, at the default graphics settings and with no mods or shaders
(the game last captured on 2026-10-17, the menus on 2026-10-22). Converted to WebP at 1600 and 800 pixels wide
by `scripts/assets.mjs`.
| Files in `public/media/` | Source | What it shows |
| --- | --- | --- |
| `launcher*.webp` | `docs/media/launcher-play.png` | The Horizon launcher's Play page (no background image) |
| `settings*.webp` | `docs/media/settings-simple.png` | Video Settings, with Horizon's graphics buttons at the top |
| `sunset*.webp`, `village*.webp`, `survival*.webp`, `nether*.webp`, `night*.webp`, `underwater*.webp` | `docs/media/<name>.jpg` | In-game views of one fixed-seed world; none shows a Mojang logo or splash text |
Horizon's interface in these images is Horizon's original work (MIT). The block, mob and
item textures and the HUD sprites drawn in them belong to Mojang and Microsoft; they are
shown as screenshots of the game, as the
[Minecraft Usage Guidelines](https://www.minecraft.net/en-us/usage-guidelines) allow for
fan websites about the game, with the disclaimer they require. The site offers no game
files: players need their own legally obtained copy of Minecraft: Java Edition.
### The launcher's Servers page
| Files in `public/media/docs/` | Source | What it shows |
| --- | --- | --- |
| `servers-create.webp`, `servers-dashboard.webp`, `servers-invite.webp`, `servers-backups.webp` | Screenshots of the Horizon launcher's Servers page (a debug build on Linux, with made-up addresses, names and `@example.com` emails), converted to WebP | The Create a server dialog, a running server, a Tailscale invitation, the Backups tab |
They show only Horizon's own interface (MIT) and a default player head.
## Text
The parity table is generated from Horizon's README, and the patch notes
come from Horizon's GitHub releases: Horizon's own text, MIT. The documentation
(`docs/*.md` in this folder, served under `/docs`) is Horizon's own text, MIT.
## Contributors
The Contributors block lists the GitHub accounts that have committed to Horizon: login,
commit count and profile link, from GitHub's API. Their avatars are not stored in this repository;
the visitor's browser loads them from `avatars.githubusercontent.com`, each one the picture its
owner chose for their GitHub account. Bots are left out.
-153
View File
@@ -1,153 +0,0 @@
# Horizon's website
Live at **https://playhorizon.dev**.
A landing page, a Releases page, a Benchmarks page and a documentation section in Horizon's design language (`docs/design-system.md`), with the tokens of
`crates/horizon-theme` (generated into `src/tokens.css` by `cargo run -p xtask -- tokens`):
the Blocks look: Pixelify Sans headings over Geist text, Horizon's purple in a light and a dark
appearance (the system's, or the toggle's), square panels with an outline, a bevel and a lip, a
stone texture and a pixel landscape drawn in code (`scripts/art.mjs`), and short, eased motion. A headline and the download, then a scroll through what it is, the screenshots, the parity grid, the launcher, the newest
releases, the community links and the contributors. Plain HTML, CSS and JavaScript; no framework and
no runtime dependencies.
Home, Releases, Benchmarks and every Docs page share the header and phone menu rendered by
`build.mjs`, with the same links, theme switch and Download button.
## Layout
```
build.mjs renders dist/ (no dependencies)
docs/*.md the documentation, one Markdown file per page: "# Title", a lede paragraph, then sections
docs/nav.json the order of the pages and the groups of the left navigation
serve.mjs a tiny static server for local checks
src/index.html the page, with {{placeholders}} build.mjs fills
src/404.html the not-found page
src/privacy.html what the launcher can send when players allow it, retention, deletion (/privacy)
src/releases.html every release, newest first, with what it adds and fixes, its notes and downloads (/releases)
src/benchmarks.html Horizon against vanilla 26.3 on our machines, the hardware picker, players' results (/benchmarks)
src/hub.html the hub (/hub, /roadmap redirects to its Roadmap tab): Overview, Roadmap, Kanban, Journal, Contribute, a Simple / Technical switch
src/mods.html the registry's list (/mods) and a page per project (/mods/<id>), one template
src/docs.html the template of every documentation page (/docs/...), with its left navigation
src/tokens.css the design tokens, generated from crates/horizon-theme (do not edit)
src/style.css every style, on top of the tokens
src/main.js the home page: download buttons, recent releases, links, contributors, nav, tabs, lightbox, motion
src/chrome.js the light/dark switch, the phone menu and GitHub fetching, shared by the pages
src/releases-page.js the Releases page: the channel filter and the refresh from GitHub
src/mods.js the registry's markup (list, project page, submit form), shared by build and page; mods-page.js: the filter, the search, the refresh; mods-submit.js: the form's sign-in and sending
src/benchmarks-page.js the Benchmarks page: players' results from the telemetry server, the picker
src/docs-page.js the Docs pages: the shared theme switch and phone menu
src/bench.js the Benchmarks tables, shared by build and page
src/hardware.js the spec table estimates use (GPU TFLOPS and bandwidth, CPU single-thread score)
src/estimate.js estimates for unmeasured hardware from the nearest measured machine
src/markdown.js a small Markdown renderer (escapes everything), shared by build and page
src/releases.js the home page's "Recently added" and the Releases page's markup, shared by build and page
src/contributors.js the contributors' filtering and markup, shared by build and page
src/hub.js the hub's roadmap, board and journal from data/roadmap.json, GitHub's issues and the releases, shared by build and page; /data/journal.json
src/hub-page.js the hub page: tabs by #hash, the Simple / Technical switch (localStorage), the area filter, live states
data/roadmap.json the roadmap, generated by tools/roadmap.py from roadmap/roadmap.toml (do not edit); served at /data/roadmap.json
public/ fonts, screenshots, icons, og.png, robots.txt, benchmarks.json: copied as is (sitemap.xml is written by the build)
data/benchmarks/ our raw benchmark reports (tools/perf_bench.py) and fleet.json, the list scripts/benchmarks.mjs reads
scripts/benchmarks.mjs benchmark reports -> public/benchmarks.json (ours, and players' groups of 3+ installs)
scripts/assets.mjs rebuilds public/ from the repo (fonts to WOFF2, images to WebP, the pixel art)
scripts/art.mjs draws the stone tile and the hero's landscape (PNG, one pixel per texel)
scripts/shots.mjs Playwright screenshots at every width, and og.png
Dockerfile, Caddyfile, railway.json the Railway service
LEGAL.md every asset, its source and licence
shots/ (gitignored) full-page screenshots at 360–1920 px, a 1250×700 window, the open phone menu
```
## What comes from where
- **Built in** (`build.mjs`): Horizon's registry (`/mods`, one page per project) from the index ludovic111/horizon-mods
publishes (`HORIZON_MOD_INDEX=<file>` builds from a test index; docs/api/mod-index.md); the parity table, its score, harness count and audit date,
all from `README.md`; the documentation pages (`/docs`) from `website/docs/`; the Discord and Sponsor links from
`launcher/links.json`; the latest releases and the contributors (`/repos/ludovic111/horizon/contributors`,
bots left out, the owner first), when GitHub answers; without an answer the build still succeeds and
the Contributors block and its menu entries stay hidden until the page loads them. Locally these come from
the checkout; on Railway, which builds only this folder, from `main` on GitHub. The build
fails if the README has no parity table, so a broken README never deploys.
- **Fetched by the page**: the registry's index (a project merged after the build shows at once), the releases (the download button picks the newest
`launcher-v*` release's asset for the visitor's OS, the home page lists the newest game releases, and
the Releases page reloads its list when GitHub has a newer release), players' benchmark results
(`GET https://telemetry.playhorizon.dev/v1/benchmarks/summary`, 3-second timeout), the contributors (avatars come from `avatars.githubusercontent.com`),
and `links.json` from `main`. Each
falls back to what was built in, and the download button to the releases page.
## The documentation
`docs/<slug>.md` becomes `/docs/<slug>` (`index.md` is `/docs/`). Write for players first, then hosters,
then contributors; plain words, short paragraphs; claim only what players can download today, and
mark merged-but-unreleased things "next release". `{{HARNESSES}}` and `{{PERCENT}}` are filled from
the README. A line holding only `![caption](/media/...)` becomes a figure (the file must be in
`public/media/`); links to other pages are `/docs/<slug>#heading`. The build checks every link
between pages and every image, and fails with the list when one is broken. `/server`
redirects to `/docs/hosting`.
The API portal is the repository's `docs/api/`, not a copy: `nav.json` lists its README as `api` (`/docs/api`) and
its pages as `api/<page>` (`/docs/api/<page>`), read from the checkout or from `main` on GitHub like the README.
Links between its pages become the site's, every other relative link goes to the file on GitHub, and the same link
check applies. Markdown tables render as tables. The other developer docs in `docs/` stay on GitHub and are linked
from the Contributing page.
## Run it locally
```sh
cd website
node build.mjs && node serve.mjs # http://localhost:4173
```
Asset and screenshot scripts need the dev dependencies (sharp, wawoff2, Playwright):
```sh
npm install
GITHUB_TOKEN=$(gh auth token) node build.mjs # optional token: bakes the releases past the API rate limit
npm run assets # after a font, screenshot or logo changes in the repo
npm run shots # shots/*.png (gitignored; SHOTS=dir to write elsewhere) and public/og.png
```
The screenshots are not committed: full-page PNGs at a dozen widths weigh about 17 MB.
Running text stays under 80 characters a line; on 360–430 px phones the narrowest paragraphs
fall to about 26–35, which is what that width allows. `npm run shots` also reports horizontal overflow, tap targets under 44 px on touch widths
and characters per line, for 360 to 1920 px wide and a 1250×700 window.
## Railway
The service deploys `main` (Railway's GitHub integration; there is no `website` branch any more) when `/website/**`, `README.md` or `launcher/links.json` change, and answers on https://playhorizon.dev. Its
`*.up.railway.app` address redirects there with a 301 (the Caddyfile), so search engines index
one URL; the page also names it as canonical, in its Open Graph tags and in `sitemap.xml`.
Railway's healthcheck reaches `$PORT` under its own host name and is served normally.
Create a service from the GitHub repo with **root directory `website`**. `railway.json`
selects the Dockerfile: a Node stage runs `build.mjs` (reading the README from `main`), and
a Caddy stage serves `dist/` on `$PORT` with compression, cache headers, a strict
Content-Security-Policy and the 404 page. Its watch patterns also redeploy when the README
or `launcher/links.json` change on main. No variables are needed; an optional
`GITHUB_TOKEN` (read-only, public repos) lets the build bake the releases even when
GitHub's anonymous rate limit is hit. `REGISTRY_UPSTREAM`
(`horizon-mod-registry.railway.internal:8080`) sends `/api/mods/*` to the registry's submission
service (`crates/horizon-mod-registry`); unset, `/mods/submit` says the form isn't open.
## Rules the site keeps
- The disclaimer, "Not an official Minecraft product. Not approved by or associated with
Mojang or Microsoft.", is under the headline, in the phone menu, the footer and the 404 page.
- No Minecraft logo or wordmark; "Minecraft" appears only descriptively ("for Minecraft:
Java Edition 26.3"). No game files are offered or linked.
- Parity is worded as the README words it: 100% of what the harnesses compare.
- No cookies, analytics, trackers or third-party fonts and scripts.
- Motion stops under `prefers-reduced-motion`; nothing hijacks the scroll.
## Benchmarks
`public/benchmarks.json` is what the Benchmarks page shows: `{ format: "horizon-benchmark-summary", version: 1,
generated, min_installs, groups }`, one group per GPU, CPU, render distance and game (`horizon`, `vanilla`) with
`medians` (`avg_fps`, `low_1pct_fps`, `p95_ms`, `world_ms`, `chunk_load_ms`, `rss_peak_mb`, `rss_steady_mb`).
Groups `"source": "us"` are our runs (with the machine, date, version and the four scenes); groups `"source":
"players"` come from players' `benchmark` uploads (docs/telemetry.md) and need at least `min_installs` (3)
different installs. The telemetry server's `GET /v1/benchmarks/summary` answers in the same shape; the page asks it
for players' groups and keeps the baked ones when it doesn't answer. The rules are in `scripts/benchmarks.mjs`, with
a fixture set (`scripts/fixtures/benchmarks/`) the server's test shares.
To publish new measurements: run `tools/perf_bench.py` for Horizon and vanilla on a machine (docs/benchmark.md), copy
the reports into `data/benchmarks/`, list them in `data/benchmarks/fleet.json`, then
`node scripts/benchmarks.mjs --ours data/benchmarks/fleet.json`.
-554
View File
@@ -1,554 +0,0 @@
// Builds the site into dist/: src/index.html with the README's parity table and
// the community links and contributors baked in, the documentation (docs/*.md, one page each, listed in
// docs/nav.json), plus public/ and fingerprinted CSS and JS. No dependencies.
//
// Railway builds only website/, so the README and links.json come from the repo when they
// are next to it (a local checkout) and from main on GitHub otherwise. The documentation lives in website/, except the
// API portal (docs/api/), read the same way as the README. The latest releases and
// the contributors are baked in when GitHub answers, so the page reads well before (or without) JavaScript.
import { readFile, writeFile, mkdir, rm, cp } from "node:fs/promises";
import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { join, dirname, posix } from "node:path";
import { fileURLToPath } from "node:url";
import { markdown, inline, section, escape } from "./src/markdown.js";
import { recentHtml, releasesPageHtml } from "./src/releases.js";
import { machinesHtml, playersHtml } from "./src/bench.js";
import { CPUS, GPUS, singleThread } from "./src/hardware.js";
import { isGame, isLauncher, launcherVersion, newestFirst } from "./src/versions.js";
import { contributorsFrom, contributorsHtml } from "./src/contributors.js";
import { areaFilterHtml, boardFilterHtml, boardFrom, boardHtml, creditFrom, creditHtml, helpWantedHtml, journalFrom, journalHtml, journalJson, overviewHtml, roadmapHtml, slimIssue, suggestItemsHtml } from "./src/hub.js";
import { INDEX_URL, KINDS, SORTS, SUBMIT, listHtml, projectHtml, readIndex, sorted, submitHtml } from "./src/mods.js";
const site = dirname(fileURLToPath(import.meta.url));
const dist = join(site, "dist");
const REPO = "ludovic111/horizon";
const RAW = `https://raw.githubusercontent.com/${REPO}/main/`;
const DEFAULT_LINKS = { discord: "https://discord.gg/HcTeMZxe5E", sponsor: "https://github.com/sponsors/ludovic111" };
async function repoFile(path) {
const local = join(site, "..", path);
if (existsSync(local)) return [await readFile(local, "utf8"), "the checkout"];
const res = await fetch(RAW + path, { signal: AbortSignal.timeout(15000) });
if (!res.ok) throw new Error(`${path}: ${res.status} from GitHub`);
return [await res.text(), "main on GitHub"];
}
// The README: required, a site with a stale or missing parity table is worse than no deploy.
const [readme, readmeFrom] = await repoFile("README.md");
let links = { ...DEFAULT_LINKS };
try {
const parsed = JSON.parse((await repoFile("launcher/links.json"))[0]);
for (const key of Object.keys(links)) if (typeof parsed[key] === "string") links[key] = parsed[key].trim();
} catch (e) {
console.warn(`links.json: ${e.message}; using the built-in links`);
}
// Horizon's registry (/mods): the index its repository publishes, or HORIZON_MOD_INDEX (a file or an address) to
// try a test registry. Without an answer the pages are built empty and load the index themselves.
let modProjects = [];
let modIndex = null;
try {
const from = process.env.HORIZON_MOD_INDEX || INDEX_URL;
const text = from.startsWith("https://")
? await fetch(from, { signal: AbortSignal.timeout(15000) }).then((r) => (r.ok ? r.text() : Promise.reject(new Error(`${r.status}`))))
: await readFile(from, "utf8");
modIndex = JSON.parse(text);
modProjects = readIndex(modIndex) ?? (console.warn("mod index: a schema this site doesn't read"), []);
} catch (e) {
console.warn(`mod index: ${e.message}; the pages will load it themselves`);
}
// An optional token (a Railway variable, or `gh auth token` locally) lifts the rate limit.
const githubHeaders = { Accept: "application/vnd.github+json", ...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}) };
// Every release (the Releases page lists them all), a page of 100 at a time.
let releases = [];
try {
let res;
for (let page = 1; page <= 5; page++) {
res = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=100&page=${page}`, { headers: githubHeaders, signal: AbortSignal.timeout(15000) });
if (!res.ok) break;
const batch = await res.json();
releases.push(...batch.filter((r) => !r.draft));
if (batch.length < 100) break;
}
if (!res.ok) console.warn(`releases: ${res.status} from GitHub; the pages will load them themselves`);
// Pre-releases of the old Alpha channel could push the newest release out of the newest 100: it is GitHub's
// "latest" (pre-releases never are).
if (res.ok && !releases.some((r) => isGame(r.tag_name) && !r.prerelease)) {
const latest = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, { headers: githubHeaders, signal: AbortSignal.timeout(15000) });
if (latest.ok) releases.push(await latest.json());
}
} catch (e) {
console.warn(`releases: ${e.message}; the page will load them itself`);
}
let contributors = [];
try {
const res = await fetch(`https://api.github.com/repos/${REPO}/contributors?per_page=100`, {
headers: { Accept: "application/vnd.github+json", ...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}) },
signal: AbortSignal.timeout(15000),
});
if (res.ok) contributors = contributorsFrom(await res.json());
else console.warn(`contributors: ${res.status} from GitHub; the page will load them itself`);
} catch (e) {
console.warn(`contributors: ${e.message}; the page will load them itself`);
}
// The hub's roadmap (data/roadmap.json, generated from roadmap/roadmap.toml by tools/roadmap.py) and the live state
// of the issues and pull requests its items link: the open ones and those closed lately, then (with a token) the
// older ones in one GraphQL request. Without GitHub the hub shows the links without their state.
const roadmap = JSON.parse(await readFile(join(site, "data", "roadmap.json"), "utf8"));
const linked = new Set(roadmap.items.flatMap((i) => (i.links || []).map((l) => Number((l.match(/^#(\d+)$/) || [])[1])).filter(Boolean)));
let hubLive = [];
try {
const urls = [1, 2, 3].map((p) => `https://api.github.com/repos/${REPO}/issues?state=open&per_page=100&page=${p}`);
urls.push(`https://api.github.com/repos/${REPO}/issues?state=closed&sort=updated&direction=desc&per_page=100`);
for (const url of urls) {
const res = await fetch(url, { headers: githubHeaders, signal: AbortSignal.timeout(15000) });
if (!res.ok) throw new Error(`${res.status} from GitHub`);
hubLive.push(...(await res.json()).map(slimIssue));
}
const missing = [...linked].filter((n) => !hubLive.some((i) => i.n === n));
if (missing.length && process.env.GITHUB_TOKEN) {
const fields = missing.map((n) => `n${n}: issueOrPullRequest(number: ${n}) { ... on Issue { number title state stateReason url } ... on PullRequest { number title state isDraft mergedAt url } }`).join(" ");
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: githubHeaders,
body: JSON.stringify({ query: `query { repository(owner: "ludovic111", name: "horizon") { ${fields} } }` }),
signal: AbortSignal.timeout(15000),
});
const nodes = Object.values((await res.json()).data?.repository || {}).filter(Boolean);
hubLive.push(
...nodes.map((x) =>
slimIssue({
number: x.number, title: x.title, html_url: x.url, draft: x.isDraft,
state: x.state === "OPEN" ? "open" : "closed", state_reason: x.stateReason === "NOT_PLANNED" ? "not_planned" : null,
pull_request: "isDraft" in x ? { merged_at: x.mergedAt } : undefined,
}),
),
);
}
} catch (e) {
console.warn(`hub: ${e.message}; the page will load the states itself`);
}
const hubLinked = new Map(hubLive.filter((i) => linked.has(i.n)).map((i) => [i.n, i]));
const board = boardFrom(hubLive, roadmap);
const journal = journalFrom(releases, hubLive, roadmap);
// The hub's server (crates/horizon-telemetry-server: votes, bug reports, suggestions), and Kira's list of the
// contributors who linked their Discord account and agreed to be credited (kira's /contributors.json).
const hubApi = process.env.HUB_API_URL || "https://telemetry.playhorizon.dev";
let kiraCredit = {};
try {
const res = await fetch(process.env.KIRA_CONTRIBUTORS_URL || "https://kira-production-500e.up.railway.app/contributors.json", { signal: AbortSignal.timeout(10000) });
if (!res.ok) throw new Error(`${res.status} from Kira`);
kiraCredit = await res.json();
} catch (e) {
console.warn(`hub: contributors' Discord names: ${e.message}`);
}
// --- The parity table ------------------------------------------------------------------
const status = section(readme, "Project status") ?? "";
const parityHeading = (status.match(/^###\s+(Java parity:\s*[\d.]+%)/m) || [])[1];
if (!parityHeading) throw new Error("README: no '### Java parity: N%' heading under 'Project status'");
const percent = parityHeading.match(/[\d.]+%/)[0];
const parityBody = section(readme, parityHeading) ?? "";
const rows = parityBody
.split("\n")
.filter((l) => /^\|/.test(l) && !/^\|\s*-/.test(l))
.map((l) =>
l
.replace(/^\|/, "")
.replace(/\|\s*$/, "")
.split("|")
.map((c) => c.trim()),
);
const [, ...body] = rows; // the first row is the header
if (body.length < 5) throw new Error("README: the parity table has fewer than five rows");
const harnesses = (readme.match(/all (\d[\d,]*) (?:parity )?harnesses/) || [])[1];
if (!harnesses) throw new Error("README: no 'all N harnesses' count");
const audited = (parityBody.match(/Last audited\s+(\d{4}-\d{2}-\d{2})/) || [])[1] ?? "";
const intro = status.split(/^###/m)[0].trim();
const scoreLabel = (s) => ({ 0: "Absent", 0.25: "Started", 0.5: "Playable", 0.75: "Mostly complete", 1: "Complete" })[Number(s)] ?? s;
const check = `<svg class="check" viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>`;
const parityCells = body
.map(([name, score, gaps], i) => {
// "None: what matches" reads as the evidence; anything else is a gap and says so.
const none = gaps.match(/^None:\s*(.*)$/);
const full = Number(score) === 1;
const detail = none ? inline(none[1].charAt(0).toUpperCase() + none[1].slice(1)) : `<span class="gap">Gaps:</span> ${inline(gaps)}`;
const state = full ? `${check}Matches vanilla` : `${escape(score)} · ${escape(scoreLabel(score))}`;
return `<li class="system${full ? " is-full" : ""}">
<p class="system-head"><span class="system-index">${String(i + 1).padStart(2, "0")}</span><span class="system-state">${state}</span></p>
<h3>${inline(name)}</h3>
<p class="system-detail">${detail}</p>
</li>`;
})
.join("\n");
const matching = body.filter(([, score]) => Number(score) === 1).length;
const parityHeadline = matching === body.length ? "Every subsystem matches vanilla." : `${matching} of ${body.length} subsystems match vanilla.`;
// --- The releases ----------------------------------------------------------------------
// Newest first by tag: every 0.2 tag is newer than the dated ones (src/versions.js). One channel since 2026-10-23
// (docs/release-flow.md, "One channel"); the old Alpha channel's pre-releases stay listed.
const byTag = (a, b) => newestFirst(a.tag_name, b.tag_name);
const ours = releases.filter((r) => isGame(r.tag_name) || isLauncher(r.tag_name)).sort(byTag);
const game = ours.filter((r) => isGame(r.tag_name));
const launcher = releases.filter((r) => isLauncher(r.tag_name) && !r.prerelease).sort(byTag)[0];
// --- The benchmarks ----------------------------------------------------------------------
// public/benchmarks.json: our measurements (scripts/benchmarks.mjs from data/benchmarks/fleet.json) and, once the
// telemetry server has some, players' groups. The page shows it baked and asks the server for newer player results.
const benchSummary = JSON.parse(await readFile(join(site, "public", "benchmarks.json"), "utf8"));
// --- The page --------------------------------------------------------------------------
const fingerprint = (text) => createHash("sha256").update(text).digest("hex").slice(0, 10);
await rm(dist, { recursive: true, force: true });
await mkdir(join(dist, "assets"), { recursive: true });
await cp(join(site, "public"), dist, { recursive: true });
await cp(join(site, "LEGAL.md"), join(dist, "LEGAL.md"));
// The icons keep their names (browsers and other sites ask for /favicon.svg and the
// rest), so the pages link them with their content's hash: a new mark reaches everyone
// at once instead of after the week the server lets browsers keep the old one.
const iconVersions = {};
for (const icon of ["favicon.svg", "favicon-32.png", "apple-touch-icon.png"]) {
iconVersions[icon] = fingerprint(await readFile(join(site, "public", icon)));
}
// /favicon.ico, which browsers request on their own: the 32-pixel PNG in an ICO
// container (one image entry; ICO has held PNG data since Windows Vista).
const png32 = await readFile(join(site, "public", "favicon-32.png"));
const ico = Buffer.alloc(22);
ico.writeUInt16LE(0, 0); // reserved
ico.writeUInt16LE(1, 2); // type: icon
ico.writeUInt16LE(1, 4); // one image
ico.writeUInt8(32, 6); // width
ico.writeUInt8(32, 7); // height
ico.writeUInt16LE(1, 10); // colour planes
ico.writeUInt16LE(32, 12); // bits per pixel
ico.writeUInt32LE(png32.length, 14);
ico.writeUInt32LE(22, 18); // the image follows the header and its entry
await writeFile(join(dist, "favicon.ico"), Buffer.concat([ico, png32]));
// The page's modules, fingerprinted; each import of a shared module is rewritten to its name.
const names = {};
const rewrite = (code) => code.replace(/"\.\/(\w+\.js)"/g, (all, file) => (names[file] ? `"./${names[file]}"` : all));
// Dependencies before what imports them.
const modules = ["markdown.js", "versions.js", "chrome.js", "hardware.js", "estimate.js", "releases.js", "contributors.js", "bench.js", "hub.js", "main.js", "releases-page.js", "benchmarks-page.js", "docs-page.js", "hub-page.js", "mods.js", "mods-page.js", "mods-submit.js"];
for (const file of modules) {
const code = rewrite(await readFile(join(site, "src", file), "utf8"));
names[file] = `${file.replace(".js", "")}.${fingerprint(code)}.js`;
await writeFile(join(dist, "assets", names[file]), code);
}
const jsName = names["main.js"];
// The design tokens come first: src/tokens.css is generated from crates/horizon-theme
// (`cargo run -p xtask -- tokens`), so the page shares the launcher's and the game's values.
const css = (await readFile(join(site, "src/tokens.css"), "utf8")) + "\n" + (await readFile(join(site, "src/style.css"), "utf8"));
// The stylesheet is inlined into the page, so the first paint needs no second request.
const cssName = `style.${fingerprint(css)}.css`;
await writeFile(join(dist, "assets", cssName), css);
const inlineCss = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s*\n\s*/g, "\n").trim();
const baked = {
links,
contributors,
releases: releases.slice(0, 12).map((r) => ({
tag_name: r.tag_name,
name: r.name,
published_at: r.published_at,
html_url: r.html_url,
prerelease: r.prerelease,
body: isGame(r.tag_name) ? r.body : "",
assets: r.assets.map((a) => ({ name: a.name, browser_download_url: a.browser_download_url, size: a.size })),
})),
};
const unavailable = `<p class="lede">The release notes could not be loaded here.</p><a class="button secondary" href="https://github.com/${REPO}/releases">Read them on GitHub</a>`;
// One top bar and phone menu for Home, Releases, Benchmarks and every Docs page.
const navSections = [["/#about", "About"], ["/#screenshots", "In game"], ["/#parity", "Parity"], ["/#launcher", "Launcher"], ["/#recent", "New", "Recently added"], ["/#community", "Community"]];
const navPages = [["/hub", "Hub"], ["/mods", "Mods"], ["/releases", "Releases"], ["/benchmarks", "Benchmarks"], ["/docs/", "Docs", "Documentation"]];
const navLink = ([href, label, menuLabel], current, menu = false) =>
`<a href="${href}"${href === current ? ' aria-current="page"' : ""}${href === "/#contributors" ? ` data-contributors${contributors.length ? "" : " hidden"}` : ""}>${menu && menuLabel ? menuLabel : label}</a>`;
const siteNav = (current) => ` <header class="nav" id="top">
<div class="nav-inner">
<a class="nav-mark" href="/" aria-label="Horizon, home"><svg class="mark" viewBox="0 0 16 16" aria-hidden="true"><path class="mark-stem" d="M4 3h2v4H4zM10 3h2v4h-2z"/><path class="mark-low" d="M4 8h2v4H4zM10 8h2v4h-2z"/><path class="mark-line" d="M1 7h14v1H1z"/></svg>Horizon</a>
<nav class="nav-links" id="nav-links" aria-label="Sections and pages">
${[...navSections, ...navPages].map((link) => ` ${navLink(link, current)}`).join("\n")}
</nav>
<div class="nav-end">
<button class="theme-toggle" type="button" aria-label="Switch between light and dark"><span data-theme-label>Dark</span></button>
<a class="nav-github" href="https://github.com/${REPO}">GitHub</a>
<a class="button primary small" href="/docs/install">Download</a>
<button class="menu-button" type="button" aria-expanded="false" aria-controls="menu"><span class="menu-label">Menu</span><span class="menu-icon" aria-hidden="true"></span></button>
</div>
</div>
</header>
<div class="menu" id="menu" hidden>
<nav aria-label="Sections and pages">
${[...navSections, ["/#contributors", "Contributors"], ...navPages].map((link) => ` ${navLink(link, current, true)}`).join("\n")}
</nav>
<div class="menu-foot">
<a class="button secondary large" href="https://github.com/${REPO}">GitHub</a>
<p>Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft.</p>
</div>
</div>`;
const siteFooter = ` <footer class="footer">
<div class="container">
<div class="footer-bar">
<span class="footer-mark">Made in the open, in Rust.</span>
<nav class="footer-links" aria-label="Elsewhere"><a href="/">Home</a><a href="/hub">Hub</a><a href="/mods">Mods</a><a href="/releases">Releases</a><a href="/benchmarks">Benchmarks</a><a href="/docs/">Docs</a><a href="/docs/how-horizon-is-built">How it's built</a><a href="https://github.com/${REPO}">GitHub</a><a href="/privacy">Privacy</a><a href="/LEGAL.md">Assets</a></nav>
</div>
</div>
<p class="footer-word" aria-hidden="true">horizon</p>
</footer>`;
const fixed = (x, n) => Number(x).toFixed(n);
const specTable =
`<table class="bench-table spec"><thead><tr><th scope="col">Graphics card</th><th scope="col">FP32 TFLOPS</th><th scope="col">Bandwidth, GB/s</th><th scope="col">Memory, GB</th></tr></thead><tbody>` +
GPUS.map((g) => `<tr><th scope="row">${escape(g.name)}</th><td class="num">${fixed(g.tflops, 1)}</td><td class="num">${g.gbps}</td><td class="num">${g.vram_gb || "shared"}</td></tr>`).join("") +
`</tbody></table><table class="bench-table spec"><thead><tr><th scope="col">Processor</th><th scope="col">Boost, GHz</th><th scope="col">Generation factor</th><th scope="col">Single-thread score</th></tr></thead><tbody>` +
CPUS.map((c) => `<tr><th scope="row">${escape(c.name)}</th><td class="num">${fixed(c.ghz, 2)}</td><td class="num">${fixed(c.ipc, 2)}</td><td class="num">${fixed(singleThread(c), 2)}</td></tr>`).join("") +
`</tbody></table>`;
const options = (list, selected) => list.map((x) => `<option${x.name === selected ? " selected" : ""}>${escape(x.name)}</option>`).join("");
const playersGroups = benchSummary.groups.filter((g) => g.source === "players");
const values = {
CSS: `/assets/${cssName}`,
STYLE: inlineCss,
JS: `/assets/${jsName}`,
PERCENT: percent,
HARNESSES: harnesses,
AUDITED: audited,
PARITY_INTRO: markdown(intro.replace("The table below is the project's own audit", "Below is the project's own audit")),
PARITY_CELLS: parityCells,
PARITY_HEADLINE: parityHeadline,
SUBSYSTEMS: String(body.length),
RECENT: recentHtml(game) || unavailable,
SITE_NAV: "",
SITE_FOOTER: siteFooter,
DOCS_JS: `/assets/${names["docs-page.js"]}`,
RELEASES_JS: `/assets/${names["releases-page.js"]}`,
RELEASE_LIST: releasesPageHtml(ours) || unavailable,
RELEASE_COUNT: ours.length ? `${ours.length} releases` : "",
NEWEST_TAG: escape(ours[0]?.tag_name || ""),
BENCH_JS: `/assets/${names["benchmarks-page.js"]}`,
MACHINES: machinesHtml(benchSummary),
PLAYERS: playersHtml(benchSummary),
PLAYERS_SOURCE: `${playersGroups.length ? `${playersGroups.length} groups` : "No groups yet"}, as of ${benchSummary.generated.slice(0, 10)}. The page asks the telemetry server for newer ones.`,
GPU_OPTIONS: options(GPUS, "NVIDIA GeForce RTX 4060"),
CPU_OPTIONS: options(CPUS, "AMD Ryzen 5 5600X"),
SPEC_TABLE: specTable,
BENCH_DATA: JSON.stringify(benchSummary).replace(/</g, "\\u003c"),
LAUNCHER_META: `${launcher ? `Launcher ${escape(launcherVersion(launcher.tag_name))} for` : "For"} Linux x86-64, macOS on Apple Silicon and Windows x64.`,
DISCORD: escape(links.discord || DEFAULT_LINKS.discord),
SPONSOR: escape(links.sponsor || DEFAULT_LINKS.sponsor),
CONTRIBUTORS: contributorsHtml(contributors),
CONTRIBUTORS_HIDDEN: contributors.length ? "" : " hidden",
BAKED: JSON.stringify(baked).replace(/</g, "\\u003c"),
HUB_JS: `/assets/${names["hub-page.js"]}`,
HUB_OVERVIEW: overviewHtml(roadmap, hubLinked),
HUB_ROADMAP: roadmapHtml(roadmap, hubLinked),
HUB_AREAS: areaFilterHtml(roadmap),
HUB_HELP_WANTED: helpWantedHtml(roadmap),
HUB_BOARD: hubLive.length ? boardHtml(board, roadmap) : `<p class="lede">The board could not be loaded here.</p>`,
HUB_BOARD_FILTER: boardFilterHtml(board, roadmap),
HUB_JOURNAL: journalHtml(journal, roadmap) || unavailable,
HUB_DATA: JSON.stringify({ roadmap, live: [...hubLinked.values()], api: hubApi }).replace(/</g, "\\u003c"),
HUB_SUGGEST_ITEMS: suggestItemsHtml(roadmap),
HUB_VERSION: escape(game[0]?.tag_name || ""),
HUB_CREDIT: creditHtml(creditFrom(contributors, kiraCredit)),
YEAR: String(new Date().getUTCFullYear()),
};
// --- The documentation ---------------------------------------------------------------------
// website/docs/<slug>.md: "# Title", a one-paragraph lede, then the body. docs/nav.json orders them in groups.
// The API portal is the repository's docs/api/ (the pages and their generated references): nav.json lists it as
// `api` (its README, /docs/api) and `api/<page>` (/docs/api/<page>). Its links are written for GitHub, so links
// between its pages become the site's and every other relative link goes to the file on GitHub.
const docsNav = JSON.parse(await readFile(join(site, "docs", "nav.json"), "utf8"));
const docSlugs = new Set(docsNav.flatMap((g) => g.pages));
const apiSource = (slug) => `docs/api/${slug === "api" ? "README" : slug.slice(4)}.md`;
function apiLinks(text, from) {
return text.replace(/\]\(([^)\s]+)\)/g, (all, url) => {
if (url.startsWith("https://playhorizon.dev/")) return `](${url.slice(23)})`;
if (/^([a-z]+:|#|\/)/i.test(url)) return all;
const [file, id] = url.split("#");
const path = posix.normalize(posix.join(posix.dirname(from), file));
const page = path.match(/^docs\/api\/([\w-]+)\.md$/)?.[1];
const slug = page === "README" ? "api" : page && `api/${page}`;
const to = docSlugs.has(slug) ? `/docs/${slug}` : `https://github.com/${REPO}/blob/main/${path}`;
return `](${to}${id ? `#${id}` : ""})`;
});
}
const docs = [];
for (const { group, pages: slugs } of docsNav) {
for (const slug of slugs) {
const api = slug === "api" || slug.startsWith("api/");
const file = api ? apiSource(slug) : `website/docs/${slug}.md`;
let text = (api ? (await repoFile(file))[0] : await readFile(join(site, "docs", `${slug}.md`), "utf8")).replace(/\r\n?/g, "\n");
if (api) text = apiLinks(text.replace(/^<!--[\s\S]*?-->\n/, ""), file);
const m = text.match(/^# (.+)\n\n((?:[^\n]+\n)+)\n/);
if (!m) throw new Error(`${file}: needs "# Title", a blank line, a lede paragraph and a blank line`);
docs.push({ slug, file, group, title: m[1].replace(/`/g, "").trim(), lede: m[2].trim().replace(/\n/g, " "), body: text.slice(m[0].length), path: slug === "index" ? "/docs/" : `/docs/${slug}` });
}
}
const plain = (md) => md.replace(/`/g, "").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/\*\*/g, "");
const anchors = new Map(); // path -> ids, to check every link between the pages
const problems = [];
const renderedDocs = docs.map((d) => {
const headings = [];
const html = markdown(d.body.replaceAll("{{HARNESSES}}", harnesses).replaceAll("{{PERCENT}}", percent), { site: true, headings });
anchors.set(d.path, new Set(headings.map((h) => h.id)));
return { ...d, html, headings };
});
for (const d of renderedDocs) {
for (const [, link] of d.html.matchAll(/<a href="((?:\/docs|#)[^"]*)"/g)) {
const [path, id] = link.split("#");
const to = path || d.path;
const known = anchors.get(to);
if (!known) problems.push(`${d.file}: ${link} is not a docs page`);
else if (id && !known.has(id)) problems.push(`${d.file}: ${link} has no such heading`);
}
for (const [, src] of d.html.matchAll(/<img src="(\/media\/[^"]+)"/g)) {
if (!existsSync(join(site, "public", src))) problems.push(`${d.file}: image ${src} is not in public/`);
}
}
if (problems.length) throw new Error(`docs: ${problems.length} broken link(s)\n${problems.join("\n")}`);
const docNav = (current) =>
docsNav
.map(({ group, pages: slugs }) => {
const items = slugs
.map((slug) => {
const d = docs.find((x) => x.slug === slug);
return `<li><a href="${d.path}"${slug === current.slug ? ' aria-current="page"' : ""}>${escape(d.title)}</a></li>`;
})
.join("");
return ` <p class="docs-nav-group">${escape(group)}</p>\n <ul>${items}</ul>`;
})
.join("\n");
const pagerLink = (d, rel) => (d ? `<a class="docs-pager-${rel}" href="${d.path}"><span>${rel === "next" ? "Next" : "Previous"}</span>${escape(d.title)}</a>` : "");
await mkdir(join(dist, "docs", "api"), { recursive: true });
const docPageFiles = [];
const docPages = renderedDocs.map((d, i) => {
const toc = d.headings.filter((h) => h.level === 2);
return {
file: join("docs", d.slug + ".html"),
values: {
DOC_TITLE: escape(d.title),
DOC_DESC: escape(plain(d.lede).slice(0, 220)),
DOC_PATH: d.path,
DOC_GROUP: escape(d.group),
DOC_LEDE: inline(d.lede, true),
DOC_NAV: docNav(d),
DOC_TOC: toc.length >= 4 ? ` <nav class="docs-toc" aria-label="On this page"><p>On this page</p><ul>${toc.map((h) => `<li><a href="#${h.id}">${escape(h.text)}</a></li>`).join("")}</ul></nav>` : "",
DOC_BODY: d.html,
DOC_PAGER: pagerLink(renderedDocs[i - 1], "prev") + pagerLink(renderedDocs[i + 1], "next"),
DOC_FILE: d.file,
SITE_NAV: siteNav("/docs/"),
},
};
});
const pages = ["index.html", "404.html", "privacy.html", "releases.html", "benchmarks.html", "hub.html"];
const currentPath = { "releases.html": "/releases", "benchmarks.html": "/benchmarks", "hub.html": "/hub" };
for (const page of pages) {
let html = await readFile(join(site, "src", page), "utf8");
const own = { ...values, SITE_NAV: siteNav(currentPath[page]) };
html = html.replace(/\{\{(\w+)\}\}/g, (all, key) => {
if (!(key in own)) throw new Error(`${page}: unknown placeholder ${all}`);
return own[key];
});
html = html.replace(/href="\/(favicon\.svg|favicon-32\.png|apple-touch-icon\.png)"/g, (all, icon) => `href="/${icon}?v=${iconVersions[icon]}"`);
await writeFile(join(dist, page), html);
}
// The registry: /mods (the list) and /mods/<id> (a page per project), one template.
const modsTemplate = await readFile(join(site, "src", "mods.html"), "utf8");
const modsList = ` <p class="kicker"><span>Mods</span>Made for Horizon, reviewed</p>
<h1 class="display page-title">Horizon mods</h1>
<p class="lede">Mods, modpacks and resource packs made for Horizon. Each one was reviewed before it was listed, and the launcher checks every file before it installs it. Mods run in a sandbox and can only do what their page says.</p>
<p class="mod-actions"><a class="button primary" href="${SUBMIT}">Submit yours</a> <a class="button secondary" href="/docs/make-a-mod">Make a mod</a></p>
<div class="mod-tools">
<input class="mod-search" id="mod-search" type="search" placeholder="Search" aria-label="Search the registry" autocomplete="off">
<div class="release-filter" role="group" aria-label="Show">
<button type="button" class="button secondary small" data-kind-filter="all" aria-pressed="true">All</button>
${KINDS.map(([k, label]) => ` <button type="button" class="button secondary small" data-kind-filter="${k}" aria-pressed="false">${label}</button>`).join("\n")}
</div>
<select class="mod-search mod-sort" id="mod-sort" aria-label="Order">${SORTS.map(([k, label]) => `<option value="${k}">${label}</option>`).join("")}</select>
</div>
<p class="meta release-count" data-mod-count>${modProjects.length} of ${modProjects.length}</p>
<div class="mod-list" id="mod-list" aria-live="polite">
${listHtml(sorted(modProjects, "downloads"))}
</div>`;
const modPages = [
{ file: "mods.html", path: "/mods", title: "Mods", description: "Mods, modpacks and resource packs made for Horizon, reviewed, each with what it may do and how to install it.", main: modsList },
{ file: "mods/submit.html", path: "/mods/submit", title: "Submit to the registry", description: "Submit a mod, a modpack or a resource pack made for Horizon: it is checked, reviewed, then listed here and in the launcher.", main: submitHtml(), js: "mods-submit.js" },
...modProjects.filter((p) => p.id !== "submit").map((p) => ({ file: `mods/${p.id}.html`, path: `/mods/${p.id}`, title: p.name, description: p.summary || p.name, main: projectHtml(p) })),
];
await mkdir(join(dist, "mods"), { recursive: true });
const modsData = JSON.stringify(modIndex && modProjects.length ? { schema: modIndex.schema, projects: modProjects } : null).replace(/</g, "\\u003c");
for (const page of modPages) {
const own = {
...values,
SITE_NAV: siteNav("/mods"),
MODS_TITLE: escape(page.title),
MODS_DESCRIPTION: escape(page.description),
MODS_PATH: page.path,
MODS_JS: `/assets/${names[page.js || "mods-page.js"]}`,
MODS_MAIN: page.main,
MODS_DATA: modsData,
MODS_BAKED_AT: escape(modIndex?.generated || ""),
};
let html = modsTemplate.replace(/\{\{(\w+)\}\}/g, (all, key) => {
if (!(key in own)) throw new Error(`mods.html: unknown placeholder ${all}`);
return own[key];
});
html = html.replace(/href="\/(favicon\.svg|favicon-32\.png|apple-touch-icon\.png)"/g, (all, icon) => `href="/${icon}?v=${iconVersions[icon]}"`);
await writeFile(join(dist, page.file), html);
}
const docTemplate = await readFile(join(site, "src", "docs.html"), "utf8");
for (const { file, values: own } of docPages) {
let html = docTemplate.replace(/\{\{(\w+)\}\}/g, (all, key) => {
const value = key in own ? own[key] : values[key];
if (value === undefined) throw new Error(`docs.html: unknown placeholder ${all}`);
return value;
});
html = html.replace(/href="\/(favicon\.svg|favicon-32\.png|apple-touch-icon\.png)"/g, (all, icon) => `href="/${icon}?v=${iconVersions[icon]}"`);
await writeFile(join(dist, file), html);
docPageFiles.push(file);
}
// The roadmap for other readers (Kira, scripts): /data/roadmap.json, as tools/roadmap.py wrote it.
await mkdir(join(dist, "data"), { recursive: true });
await cp(join(site, "data", "roadmap.json"), join(dist, "data", "roadmap.json"));
// The journal for Kira's #updates and its X posts: /data/journal.json, rebuilt with every deploy.
await writeFile(join(dist, "data", "journal.json"), JSON.stringify(journalJson(releases, hubLive, roadmap), null, 1) + "\n");
// The pages' inline scripts (they mark the page as scripted and apply the saved theme before CSS
// applies) and the inlined stylesheet: the Content-Security-Policy in the Caddyfile allows them by hash.
const allHtml = (await Promise.all([...pages, ...docPageFiles, ...modPages.map((p) => p.file)].map((page) => readFile(join(dist, page), "utf8")))).join("\n");
const hashes = (re) => [...new Set([...allHtml.matchAll(re)].map((m) => `'sha256-${createHash("sha256").update(m[1]).digest("base64")}'`))].join(" ");
await writeFile(join(dist, ".csp-script"), hashes(/<script>([\s\S]*?)<\/script>/g));
await writeFile(join(dist, ".csp-style"), hashes(/<style>([\s\S]*?)<\/style>/g));
const urls = ["/", "/hub", "/mods", ...modPages.slice(1).map((p) => p.path), "/releases", "/benchmarks", ...docs.map((d) => d.path), "/privacy"];
await writeFile(
join(dist, "sitemap.xml"),
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => ` <url><loc>https://playhorizon.dev${u}</loc></url>`).join("\n")}\n</urlset>\n`,
);
console.log(
`built dist/ from ${readmeFrom}: ${body.length} subsystems, ${percent}, ${harnesses} harnesses, ` +
`${ours.length} releases (${game.length} game)${launcher ? `, launcher ${launcher.tag_name}` : ""}, ${benchSummary.groups.length} benchmark rows, ${contributors.length} contributors, ${docs.length} docs pages, ${modProjects.length} registry projects, ${roadmap.items.length} roadmap items (${hubLinked.size}/${linked.size} links live), ${Object.values(board).flat().length} board cards, ${journal.length} journal entries`,
);
@@ -1,774 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-23T00:36:08Z",
"app": {
"name": "horizon-client",
"version": "0.1.0",
"commit": "07e71797"
},
"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": 17.934,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 10795.933,
"stdev": 13944.902,
"min": 2694.75,
"max": 26898.014,
"ci95": 34643.852
},
"startup.world_ms": {
"n": 3,
"mean": 17972.549,
"stdev": 13965.287,
"min": 6319.925,
"max": 33452.436,
"ci95": 34694.496
},
"chunks.village.load_ms": {
"n": 3,
"mean": 1260.016,
"stdev": 503.052,
"min": 679.248,
"max": 1560.056,
"ci95": 1249.753
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 9182.667,
"stdev": 24.705,
"min": 9160.0,
"max": 9209.0,
"ci95": 61.375
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2593.701,
"stdev": 266.92,
"min": 2285.61,
"max": 2755.256,
"ci95": 663.12
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 253.998,
"stdev": 45.489,
"min": 203.519,
"max": 291.815,
"ci95": 113.011
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 129.195,
"stdev": 39.949,
"min": 99.233,
"max": 174.551,
"ci95": 99.247
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 982.0,
"stdev": 0.0,
"min": 982.0,
"max": 982.0,
"ci95": 0.0
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 1591.886,
"stdev": 579.89,
"min": 950.688,
"max": 2079.581,
"ci95": 1440.643
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 10278.667,
"stdev": 9.292,
"min": 10271.0,
"max": 10289.0,
"ci95": 23.083
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 3426.658,
"stdev": 200.328,
"min": 3202.184,
"max": 3587.268,
"ci95": 497.683
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 327.656,
"stdev": 39.232,
"min": 283.548,
"max": 358.656,
"ci95": 97.466
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 141.195,
"stdev": 12.058,
"min": 127.735,
"max": 151.011,
"ci95": 29.956
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 1323.333,
"stdev": 295.603,
"min": 982.0,
"max": 1494.0,
"ci95": 734.379
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 2659.926,
"stdev": 2159.489,
"min": 1288.28,
"max": 5149.172,
"ci95": 5364.902
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 9016.0,
"stdev": 517.249,
"min": 8623.0,
"max": 9602.0,
"ci95": 1285.023
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 3277.475,
"stdev": 71.523,
"min": 3210.907,
"max": 3353.092,
"ci95": 177.688
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 387.862,
"stdev": 43.664,
"min": 341.534,
"max": 428.255,
"ci95": 108.476
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 173.188,
"stdev": 61.528,
"min": 104.34,
"max": 222.8,
"ci95": 152.857
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 1238.0,
"stdev": 0.0,
"min": 1238.0,
"max": 1238.0,
"ci95": 0.0
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 1675.173,
"stdev": 449.734,
"min": 1361.858,
"max": 2190.49,
"ci95": 1117.293
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 10546.0,
"stdev": 34.699,
"min": 10516.0,
"max": 10584.0,
"ci95": 86.203
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 2672.531,
"stdev": 87.428,
"min": 2572.733,
"max": 2735.621,
"ci95": 217.202
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 275.049,
"stdev": 9.403,
"min": 264.82,
"max": 283.318,
"ci95": 23.361
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 123.382,
"stdev": 2.627,
"min": 121.404,
"max": 126.363,
"ci95": 6.527
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 1238.0,
"stdev": 0.0,
"min": 1238.0,
"max": 1238.0,
"ci95": 0.0
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1414.479,
"stdev": 24.614,
"min": 1386.086,
"max": 1429.77,
"ci95": 61.148
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1202.283,
"stdev": 33.096,
"min": 1169.059,
"max": 1235.25,
"ci95": 82.223
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 33063,
"seconds": 12.0,
"avg_fps": 2755.256,
"low_1pct_fps": 266.659,
"low_01pct_fps": 99.233,
"p50_ms": 0.306,
"p95_ms": 0.459,
"p99_ms": 1.268,
"p999_ms": 4.771,
"max_ms": 124.422,
"cpu_p50_ms": 0.271,
"gpu_p50_ms": 0.065,
"histogram": [
32542,
268,
30,
153,
44,
15,
6,
1,
0,
1,
1,
0,
0,
0,
1,
0,
0,
0,
0,
1,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 27428,
"seconds": 12.0,
"avg_fps": 2285.61,
"low_1pct_fps": 203.519,
"low_01pct_fps": 113.802,
"p50_ms": 0.336,
"p95_ms": 0.715,
"p99_ms": 2.315,
"p999_ms": 7.476,
"max_ms": 13.28,
"cpu_p50_ms": 0.299,
"gpu_p50_ms": 0.066,
"histogram": [
26734,
400,
37,
94,
67,
35,
23,
24,
10,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 32883,
"seconds": 12.0,
"avg_fps": 2740.237,
"low_1pct_fps": 291.815,
"low_01pct_fps": 174.551,
"p50_ms": 0.31,
"p95_ms": 0.437,
"p99_ms": 1.399,
"p999_ms": 5.007,
"max_ms": 9.523,
"cpu_p50_ms": 0.275,
"gpu_p50_ms": 0.066,
"histogram": [
32316,
314,
13,
161,
46,
25,
6,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 41887,
"seconds": 12.0,
"avg_fps": 3490.521,
"low_1pct_fps": 340.763,
"low_01pct_fps": 144.838,
"p50_ms": 0.242,
"p95_ms": 0.38,
"p99_ms": 1.172,
"p999_ms": 5.439,
"max_ms": 9.908,
"cpu_p50_ms": 0.21,
"gpu_p50_ms": 0.084,
"histogram": [
41255,
377,
76,
110,
21,
16,
17,
7,
8,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 38443,
"seconds": 12.005,
"avg_fps": 3202.184,
"low_1pct_fps": 283.548,
"low_01pct_fps": 127.735,
"p50_ms": 0.248,
"p95_ms": 0.49,
"p99_ms": 1.533,
"p999_ms": 6.44,
"max_ms": 13.381,
"cpu_p50_ms": 0.216,
"gpu_p50_ms": 0.084,
"histogram": [
37784,
375,
85,
95,
33,
26,
19,
14,
10,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 43048,
"seconds": 12.0,
"avg_fps": 3587.268,
"low_1pct_fps": 358.656,
"low_01pct_fps": 151.011,
"p50_ms": 0.241,
"p95_ms": 0.318,
"p99_ms": 1.122,
"p999_ms": 4.817,
"max_ms": 10.326,
"cpu_p50_ms": 0.209,
"gpu_p50_ms": 0.084,
"histogram": [
42442,
354,
87,
95,
31,
14,
10,
8,
6,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 38531,
"seconds": 12.0,
"avg_fps": 3210.907,
"low_1pct_fps": 341.534,
"low_01pct_fps": 104.34,
"p50_ms": 0.265,
"p95_ms": 0.44,
"p99_ms": 1.171,
"p999_ms": 4.04,
"max_ms": 169.171,
"cpu_p50_ms": 0.232,
"gpu_p50_ms": 0.082,
"histogram": [
37966,
321,
156,
54,
16,
9,
3,
2,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 40238,
"seconds": 12.0,
"avg_fps": 3353.092,
"low_1pct_fps": 428.255,
"low_01pct_fps": 222.8,
"p50_ms": 0.262,
"p95_ms": 0.385,
"p99_ms": 1.199,
"p999_ms": 3.403,
"max_ms": 9.156,
"cpu_p50_ms": 0.23,
"gpu_p50_ms": 0.081,
"histogram": [
39659,
325,
186,
49,
11,
3,
1,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 39222,
"seconds": 12.0,
"avg_fps": 3268.426,
"low_1pct_fps": 393.798,
"low_01pct_fps": 192.424,
"p50_ms": 0.265,
"p95_ms": 0.415,
"p99_ms": 1.317,
"p999_ms": 3.623,
"max_ms": 10.651,
"cpu_p50_ms": 0.232,
"gpu_p50_ms": 0.081,
"histogram": [
38610,
357,
169,
59,
12,
7,
4,
1,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 30873,
"seconds": 12.0,
"avg_fps": 2572.733,
"low_1pct_fps": 264.82,
"low_01pct_fps": 122.378,
"p50_ms": 0.326,
"p95_ms": 0.605,
"p99_ms": 1.424,
"p999_ms": 6.393,
"max_ms": 14.698,
"cpu_p50_ms": 0.291,
"gpu_p50_ms": 0.082,
"histogram": [
30329,
300,
11,
155,
37,
10,
10,
13,
3,
3,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 32828,
"seconds": 12.0,
"avg_fps": 2735.621,
"low_1pct_fps": 283.318,
"low_01pct_fps": 121.404,
"p50_ms": 0.32,
"p95_ms": 0.496,
"p99_ms": 1.264,
"p999_ms": 5.842,
"max_ms": 17.335,
"cpu_p50_ms": 0.286,
"gpu_p50_ms": 0.082,
"histogram": [
32256,
320,
50,
130,
27,
15,
9,
7,
9,
3,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 32511,
"seconds": 12.0,
"avg_fps": 2709.239,
"low_1pct_fps": 277.009,
"low_01pct_fps": 126.363,
"p50_ms": 0.319,
"p95_ms": 0.536,
"p99_ms": 1.362,
"p999_ms": 5.537,
"max_ms": 15.451,
"cpu_p50_ms": 0.285,
"gpu_p50_ms": 0.082,
"histogram": [
31962,
290,
26,
173,
18,
16,
10,
6,
4,
4,
0,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-desktop"
}
@@ -1,774 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-23T00:43:34Z",
"app": {
"name": "horizon-client",
"version": "0.1.0",
"commit": "07e71797"
},
"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": 22.354,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 3223.157,
"stdev": 1895.181,
"min": 1770.597,
"max": 5366.923,
"ci95": 4708.272
},
"startup.world_ms": {
"n": 3,
"mean": 8999.144,
"stdev": 1593.439,
"min": 7467.122,
"max": 10647.611,
"ci95": 3958.641
},
"chunks.village.load_ms": {
"n": 3,
"mean": 2702.493,
"stdev": 332.199,
"min": 2494.684,
"max": 3085.624,
"ci95": 825.294
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 42899.667,
"stdev": 866.726,
"min": 41974.0,
"max": 43692.0,
"ci95": 2153.241
},
"fps.village.avg_fps": {
"n": 3,
"mean": 685.508,
"stdev": 151.759,
"min": 512.479,
"max": 796.037,
"ci95": 377.022
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 93.031,
"stdev": 31.096,
"min": 57.146,
"max": 112.055,
"ci95": 77.253
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 60.993,
"stdev": 17.294,
"min": 41.024,
"max": 71.142,
"ci95": 42.965
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 2390.0,
"stdev": 0.0,
"min": 2390.0,
"max": 2390.0,
"ci95": 0.0
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 4823.42,
"stdev": 395.462,
"min": 4545.561,
"max": 5276.174,
"ci95": 982.461
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 69214.333,
"stdev": 243.052,
"min": 68957.0,
"max": 69440.0,
"ci95": 603.824
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 590.362,
"stdev": 195.262,
"min": 373.61,
"max": 752.506,
"ci95": 485.096
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 75.679,
"stdev": 34.507,
"min": 41.158,
"max": 110.171,
"ci95": 85.726
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 41.943,
"stdev": 22.598,
"min": 22.254,
"max": 66.617,
"ci95": 56.141
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 2646.0,
"stdev": 0.0,
"min": 2646.0,
"max": 2646.0,
"ci95": 0.0
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 3628.996,
"stdev": 953.676,
"min": 3030.638,
"max": 4728.784,
"ci95": 2369.254
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 66732.667,
"stdev": 470.761,
"min": 66379.0,
"max": 67267.0,
"ci95": 1169.531
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 867.7,
"stdev": 24.924,
"min": 840.039,
"max": 888.411,
"ci95": 61.919
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 113.015,
"stdev": 5.586,
"min": 106.569,
"max": 116.454,
"ci95": 13.878
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 64.662,
"stdev": 4.205,
"min": 60.245,
"max": 68.618,
"ci95": 10.448
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 2646.0,
"stdev": 0.0,
"min": 2646.0,
"max": 2646.0,
"ci95": 0.0
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 7552.085,
"stdev": 1200.747,
"min": 6352.523,
"max": 8754.014,
"ci95": 2983.062
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 75906.0,
"stdev": 159.69,
"min": 75725.0,
"max": 76027.0,
"ci95": 396.725
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 544.171,
"stdev": 109.268,
"min": 419.998,
"max": 625.628,
"ci95": 271.458
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 73.334,
"stdev": 15.459,
"min": 56.192,
"max": 86.217,
"ci95": 38.405
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 44.998,
"stdev": 6.524,
"min": 37.602,
"max": 49.935,
"ci95": 16.207
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 3243.333,
"stdev": 147.802,
"min": 3158.0,
"max": 3414.0,
"ci95": 367.189
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1956.853,
"stdev": 83.778,
"min": 1868.5,
"max": 2035.148,
"ci95": 208.133
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1646.435,
"stdev": 30.998,
"min": 1617.857,
"max": 1679.389,
"ci95": 77.01
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 9552,
"seconds": 11.999,
"avg_fps": 796.037,
"low_1pct_fps": 112.055,
"low_01pct_fps": 70.812,
"p50_ms": 0.975,
"p95_ms": 2.218,
"p99_ms": 6.816,
"p999_ms": 12.22,
"max_ms": 17.3,
"cpu_p50_ms": 0.933,
"gpu_p50_ms": 0.247,
"histogram": [
5935,
2999,
296,
57,
16,
93,
68,
44,
23,
11,
6,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 6150,
"seconds": 12.0,
"avg_fps": 512.479,
"low_1pct_fps": 57.146,
"low_01pct_fps": 41.024,
"p50_ms": 1.162,
"p95_ms": 5.201,
"p99_ms": 14.081,
"p999_ms": 21.346,
"max_ms": 34.138,
"cpu_p50_ms": 1.106,
"gpu_p50_ms": 0.264,
"histogram": [
2098,
2525,
744,
369,
87,
86,
41,
41,
46,
29,
21,
33,
20,
8,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 8981,
"seconds": 12.007,
"avg_fps": 748.007,
"low_1pct_fps": 109.891,
"low_01pct_fps": 71.142,
"p50_ms": 1.021,
"p95_ms": 2.555,
"p99_ms": 7.187,
"p999_ms": 12.828,
"max_ms": 16.902,
"cpu_p50_ms": 0.975,
"gpu_p50_ms": 0.263,
"histogram": [
4071,
4237,
311,
93,
10,
61,
94,
55,
30,
7,
9,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 9031,
"seconds": 12.001,
"avg_fps": 752.506,
"low_1pct_fps": 110.171,
"low_01pct_fps": 66.617,
"p50_ms": 1.047,
"p95_ms": 2.413,
"p99_ms": 6.6,
"p999_ms": 13.248,
"max_ms": 16.878,
"cpu_p50_ms": 1.005,
"gpu_p50_ms": 0.398,
"histogram": [
1109,
7289,
301,
62,
69,
80,
44,
36,
20,
6,
8,
6,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 7740,
"seconds": 12.001,
"avg_fps": 644.97,
"low_1pct_fps": 75.707,
"low_01pct_fps": 36.957,
"p50_ms": 1.091,
"p95_ms": 3.344,
"p99_ms": 8.483,
"p999_ms": 23.855,
"max_ms": 29.382,
"cpu_p50_ms": 1.046,
"gpu_p50_ms": 0.4,
"histogram": [
304,
6528,
456,
139,
55,
72,
51,
53,
40,
13,
7,
8,
1,
6,
7,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 4484,
"seconds": 12.002,
"avg_fps": 373.61,
"low_1pct_fps": 41.158,
"low_01pct_fps": 22.254,
"p50_ms": 1.526,
"p95_ms": 7.517,
"p99_ms": 18.1,
"p999_ms": 33.482,
"max_ms": 58.633,
"cpu_p50_ms": 1.445,
"gpu_p50_ms": 0.414,
"histogram": [
4,
2841,
562,
454,
169,
140,
65,
63,
39,
30,
29,
32,
30,
14,
7,
1,
3,
1,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 10661,
"seconds": 12.0,
"avg_fps": 888.411,
"low_1pct_fps": 116.454,
"low_01pct_fps": 68.618,
"p50_ms": 0.878,
"p95_ms": 2.165,
"p99_ms": 5.268,
"p999_ms": 13.015,
"max_ms": 18.852,
"cpu_p50_ms": 0.837,
"gpu_p50_ms": 0.35,
"histogram": [
8240,
1821,
246,
135,
98,
39,
25,
8,
18,
11,
16,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 10496,
"seconds": 12.0,
"avg_fps": 874.651,
"low_1pct_fps": 116.021,
"low_01pct_fps": 60.245,
"p50_ms": 0.864,
"p95_ms": 2.203,
"p99_ms": 5.482,
"p999_ms": 12.557,
"max_ms": 20.239,
"cpu_p50_ms": 0.821,
"gpu_p50_ms": 0.353,
"histogram": [
7610,
2286,
236,
124,
98,
56,
21,
25,
15,
13,
4,
2,
4,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 10080,
"seconds": 11.999,
"avg_fps": 840.039,
"low_1pct_fps": 106.569,
"low_01pct_fps": 65.123,
"p50_ms": 0.889,
"p95_ms": 2.576,
"p99_ms": 5.746,
"p999_ms": 13.975,
"max_ms": 17.531,
"cpu_p50_ms": 0.845,
"gpu_p50_ms": 0.351,
"histogram": [
7336,
1986,
372,
136,
117,
44,
14,
19,
21,
16,
9,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 7043,
"seconds": 12.001,
"avg_fps": 586.888,
"low_1pct_fps": 77.592,
"low_01pct_fps": 47.456,
"p50_ms": 1.203,
"p95_ms": 3.879,
"p99_ms": 8.496,
"p999_ms": 17.959,
"max_ms": 23.68,
"cpu_p50_ms": 1.159,
"gpu_p50_ms": 0.356,
"histogram": [
0,
5932,
573,
213,
48,
93,
64,
45,
27,
18,
6,
10,
8,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 7508,
"seconds": 12.001,
"avg_fps": 625.628,
"low_1pct_fps": 86.217,
"low_01pct_fps": 49.935,
"p50_ms": 1.2,
"p95_ms": 3.217,
"p99_ms": 7.725,
"p999_ms": 16.759,
"max_ms": 22.613,
"cpu_p50_ms": 1.157,
"gpu_p50_ms": 0.361,
"histogram": [
0,
6649,
448,
117,
26,
70,
93,
40,
25,
14,
10,
7,
4,
5,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 5040,
"seconds": 12.0,
"avg_fps": 419.998,
"low_1pct_fps": 56.192,
"low_01pct_fps": 37.602,
"p50_ms": 1.71,
"p95_ms": 6.335,
"p99_ms": 12.398,
"p999_ms": 23.227,
"max_ms": 32.253,
"cpu_p50_ms": 1.634,
"gpu_p50_ms": 0.429,
"histogram": [
0,
3163,
1077,
371,
104,
58,
48,
71,
47,
41,
17,
16,
17,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-desktop"
}
@@ -1,723 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-23T00:52:47Z",
"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": 25.007,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 12177.771,
"stdev": 8960.751,
"min": 6852.719,
"max": 22523.26,
"ci95": 22261.536
},
"startup.world_ms": {
"n": 3,
"mean": 30469.771,
"stdev": 9525.862,
"min": 23924.719,
"max": 41398.26,
"ci95": 23665.464
},
"chunks.village.load_ms": {
"n": 3,
"mean": 8002.915,
"stdev": 2750.518,
"min": 5499.832,
"max": 10947.467,
"ci95": 6833.218
},
"fps.village.avg_fps": {
"n": 3,
"mean": 2481.889,
"stdev": 303.157,
"min": 2203.739,
"max": 2805.025,
"ci95": 753.144
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 385.31,
"stdev": 241.771,
"min": 129.951,
"max": 610.699,
"ci95": 600.641
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 107.075,
"stdev": 81.437,
"min": 16.766,
"max": 174.928,
"ci95": 202.318
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 44.667,
"stdev": 41.789,
"min": 6.0,
"max": 89.0,
"ci95": 103.818
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 9653.503,
"stdev": 3442.183,
"min": 7186.472,
"max": 13585.896,
"ci95": 8551.546
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 2367.898,
"stdev": 266.822,
"min": 2176.324,
"max": 2672.655,
"ci95": 662.876
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 208.784,
"stdev": 102.377,
"min": 91.127,
"max": 277.551,
"ci95": 254.34
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 56.84,
"stdev": 59.634,
"min": 10.785,
"max": 124.2,
"ci95": 148.151
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 44.667,
"stdev": 41.789,
"min": 6.0,
"max": 89.0,
"ci95": 103.818
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 10441.986,
"stdev": 2957.968,
"min": 7966.98,
"max": 13717.953,
"ci95": 7348.594
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 2727.058,
"stdev": 257.941,
"min": 2429.545,
"max": 2887.978,
"ci95": 640.812
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 246.78,
"stdev": 35.091,
"min": 211.805,
"max": 281.986,
"ci95": 87.178
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 55.43,
"stdev": 45.634,
"min": 26.317,
"max": 108.023,
"ci95": 113.369
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 44.667,
"stdev": 41.789,
"min": 6.0,
"max": 89.0,
"ci95": 103.818
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 15248.98,
"stdev": 1927.895,
"min": 13668.264,
"max": 17396.829,
"ci95": 4789.544
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 2824.349,
"stdev": 222.395,
"min": 2568.693,
"max": 2973.143,
"ci95": 552.505
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 407.099,
"stdev": 174.268,
"min": 207.655,
"max": 529.972,
"ci95": 432.941
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 69.832,
"stdev": 35.341,
"min": 29.188,
"max": 93.326,
"ci95": 87.8
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 44.667,
"stdev": 41.789,
"min": 6.0,
"max": 89.0,
"ci95": 103.818
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 3078.311,
"stdev": 77.595,
"min": 3022.469,
"max": 3166.914,
"ci95": 192.773
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2882.664,
"stdev": 183.911,
"min": 2768.117,
"max": 3094.801,
"ci95": 456.898
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 29243,
"seconds": 12.0,
"avg_fps": 2436.904,
"low_1pct_fps": 415.279,
"low_01pct_fps": 129.532,
"p50_ms": 0.354,
"p95_ms": 0.691,
"p99_ms": 1.231,
"p999_ms": 3.925,
"max_ms": 17.066,
"histogram": [
28752,
394,
53,
20,
6,
2,
3,
4,
0,
5,
0,
3,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 26445,
"seconds": 12.0,
"avg_fps": 2203.739,
"low_1pct_fps": 129.951,
"low_01pct_fps": 16.766,
"p50_ms": 0.33,
"p95_ms": 0.712,
"p99_ms": 1.185,
"p999_ms": 4.919,
"max_ms": 460.22,
"histogram": [
26007,
352,
38,
16,
6,
6,
2,
5,
3,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2,
2,
2,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 33661,
"seconds": 12.0,
"avg_fps": 2805.025,
"low_1pct_fps": 610.699,
"low_01pct_fps": 174.928,
"p50_ms": 0.315,
"p95_ms": 0.5,
"p99_ms": 0.95,
"p999_ms": 2.426,
"max_ms": 12.54,
"histogram": [
33371,
251,
9,
6,
7,
0,
10,
4,
0,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 32072,
"seconds": 12.0,
"avg_fps": 2672.655,
"low_1pct_fps": 257.674,
"low_01pct_fps": 35.534,
"p50_ms": 0.336,
"p95_ms": 0.37,
"p99_ms": 0.797,
"p999_ms": 2.624,
"max_ms": 735.482,
"histogram": [
31895,
137,
11,
14,
1,
1,
1,
3,
3,
1,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 27011,
"seconds": 12.411,
"avg_fps": 2176.324,
"low_1pct_fps": 91.127,
"low_01pct_fps": 10.785,
"p50_ms": 0.346,
"p95_ms": 0.401,
"p99_ms": 0.934,
"p999_ms": 3.871,
"max_ms": 818.414,
"histogram": [
26776,
162,
31,
17,
5,
3,
2,
4,
1,
2,
0,
1,
2,
0,
0,
0,
0,
0,
0,
1,
0,
1,
3,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 27058,
"seconds": 12.001,
"avg_fps": 2254.715,
"low_1pct_fps": 277.551,
"low_01pct_fps": 124.2,
"p50_ms": 0.367,
"p95_ms": 0.761,
"p99_ms": 2.117,
"p999_ms": 5.229,
"max_ms": 18.327,
"histogram": [
26114,
645,
153,
91,
23,
15,
4,
4,
4,
1,
1,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 34656,
"seconds": 12.0,
"avg_fps": 2887.978,
"low_1pct_fps": 246.55,
"low_01pct_fps": 31.951,
"p50_ms": 0.301,
"p95_ms": 0.374,
"p99_ms": 0.744,
"p999_ms": 1.84,
"max_ms": 529.481,
"histogram": [
34514,
111,
10,
3,
3,
3,
3,
0,
1,
1,
2,
1,
1,
0,
0,
0,
0,
0,
0,
1,
0,
1,
1,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 34364,
"seconds": 12.0,
"avg_fps": 2863.651,
"low_1pct_fps": 211.805,
"low_01pct_fps": 26.317,
"p50_ms": 0.299,
"p95_ms": 0.354,
"p99_ms": 0.726,
"p999_ms": 2.066,
"max_ms": 406.779,
"histogram": [
34238,
91,
8,
9,
4,
0,
3,
2,
2,
1,
0,
1,
0,
1,
0,
0,
0,
0,
0,
0,
2,
2,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 29155,
"seconds": 12.0,
"avg_fps": 2429.545,
"low_1pct_fps": 281.986,
"low_01pct_fps": 108.023,
"p50_ms": 0.328,
"p95_ms": 0.694,
"p99_ms": 1.869,
"p999_ms": 5.426,
"max_ms": 21.079,
"histogram": [
28371,
532,
131,
52,
30,
19,
3,
6,
1,
3,
3,
1,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 35678,
"seconds": 12.0,
"avg_fps": 2973.143,
"low_1pct_fps": 529.972,
"low_01pct_fps": 93.326,
"p50_ms": 0.316,
"p95_ms": 0.353,
"p99_ms": 0.67,
"p999_ms": 2.332,
"max_ms": 142.274,
"histogram": [
35573,
63,
14,
8,
2,
1,
0,
6,
5,
1,
1,
1,
0,
1,
1,
0,
0,
0,
0,
1,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 30825,
"seconds": 12.0,
"avg_fps": 2568.693,
"low_1pct_fps": 207.655,
"low_01pct_fps": 29.188,
"p50_ms": 0.329,
"p95_ms": 0.457,
"p99_ms": 0.904,
"p999_ms": 4.074,
"max_ms": 296.278,
"histogram": [
30573,
169,
34,
20,
8,
0,
1,
4,
3,
2,
2,
2,
0,
1,
1,
0,
0,
1,
1,
0,
2,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 35175,
"seconds": 12.0,
"avg_fps": 2931.211,
"low_1pct_fps": 483.67,
"low_01pct_fps": 86.982,
"p50_ms": 0.32,
"p95_ms": 0.366,
"p99_ms": 0.701,
"p999_ms": 3.252,
"max_ms": 186.727,
"histogram": [
35054,
67,
17,
17,
2,
2,
2,
4,
3,
3,
1,
2,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0
]
}
],
"label": "vanilla-desktop"
}
@@ -1,723 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-23T01:07:26Z",
"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": 21.652,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 8940.211,
"stdev": 2530.108,
"min": 6019.207,
"max": 10448.156,
"ci95": 6285.645
},
"startup.world_ms": {
"n": 3,
"mean": 58553.544,
"stdev": 16445.393,
"min": 39635.207,
"max": 69435.156,
"ci95": 40855.919
},
"chunks.village.load_ms": {
"n": 3,
"mean": 19818.416,
"stdev": 5189.664,
"min": 14041.61,
"max": 24086.671,
"ci95": 12892.88
},
"fps.village.avg_fps": {
"n": 3,
"mean": 881.018,
"stdev": 156.975,
"min": 710.499,
"max": 1019.508,
"ci95": 389.978
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 110.182,
"stdev": 38.396,
"min": 83.662,
"max": 154.211,
"ci95": 95.388
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 42.667,
"stdev": 23.983,
"min": 19.55,
"max": 67.431,
"ci95": 59.582
},
"memory.village.gpu_mb": {
"n": 3,
"mean": 66.333,
"stdev": 51.859,
"min": 7.0,
"max": 103.0,
"ci95": 128.835
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 28927.436,
"stdev": 5464.979,
"min": 22671.485,
"max": 32771.861,
"ci95": 13576.855
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 1072.17,
"stdev": 7.49,
"min": 1067.189,
"max": 1080.784,
"ci95": 18.609
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 164.583,
"stdev": 1.587,
"min": 162.781,
"max": 165.773,
"ci95": 3.943
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 75.682,
"stdev": 1.798,
"min": 73.791,
"max": 77.37,
"ci95": 4.467
},
"memory.forest.gpu_mb": {
"n": 3,
"mean": 66.333,
"stdev": 51.859,
"min": 7.0,
"max": 103.0,
"ci95": 128.835
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 19245.588,
"stdev": 3584.123,
"min": 16135.104,
"max": 23165.065,
"ci95": 8904.175
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 893.83,
"stdev": 538.989,
"min": 273.707,
"max": 1249.655,
"ci95": 1339.03
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 141.621,
"stdev": 90.353,
"min": 40.103,
"max": 213.218,
"ci95": 224.467
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 56.775,
"stdev": 31.752,
"min": 20.453,
"max": 79.261,
"ci95": 78.883
},
"memory.ocean.gpu_mb": {
"n": 3,
"mean": 66.333,
"stdev": 51.859,
"min": 7.0,
"max": 103.0,
"ci95": 128.835
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 59282.69,
"stdev": 12376.117,
"min": 50806.144,
"max": 73484.868,
"ci95": 30746.461
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 550.795,
"stdev": 248.553,
"min": 264.54,
"max": 711.867,
"ci95": 617.489
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 33.217,
"stdev": 15.316,
"min": 15.734,
"max": 44.271,
"ci95": 38.051
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 5.325,
"stdev": 1.985,
"min": 3.177,
"max": 7.092,
"ci95": 4.932
},
"memory.mountain.gpu_mb": {
"n": 3,
"mean": 66.333,
"stdev": 51.859,
"min": 7.0,
"max": 103.0,
"ci95": 128.835
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 3483.918,
"stdev": 54.851,
"min": 3420.879,
"max": 3520.746,
"ci95": 136.268
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 3418.959,
"stdev": 50.107,
"min": 3363.992,
"max": 3462.084,
"ci95": 124.482
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 10957,
"seconds": 12.0,
"avg_fps": 913.047,
"low_1pct_fps": 92.672,
"low_01pct_fps": 19.55,
"p50_ms": 0.804,
"p95_ms": 2.202,
"p99_ms": 4.333,
"p999_ms": 13.43,
"max_ms": 410.29,
"histogram": [
7884,
2368,
440,
147,
39,
26,
15,
15,
3,
7,
3,
8,
1,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 8526,
"seconds": 12.0,
"avg_fps": 710.499,
"low_1pct_fps": 83.662,
"low_01pct_fps": 41.019,
"p50_ms": 0.892,
"p95_ms": 3.72,
"p99_ms": 7.437,
"p999_ms": 20.294,
"max_ms": 30.234,
"histogram": [
4933,
2204,
727,
332,
133,
67,
38,
29,
20,
19,
4,
5,
6,
5,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 12236,
"seconds": 12.002,
"avg_fps": 1019.508,
"low_1pct_fps": 154.211,
"low_01pct_fps": 67.431,
"p50_ms": 0.803,
"p95_ms": 1.825,
"p99_ms": 3.764,
"p999_ms": 10.283,
"max_ms": 23.801,
"histogram": [
9657,
2078,
286,
118,
27,
26,
13,
10,
5,
5,
5,
4,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 12823,
"seconds": 12.001,
"avg_fps": 1068.537,
"low_1pct_fps": 165.773,
"low_01pct_fps": 77.37,
"p50_ms": 0.842,
"p95_ms": 1.298,
"p99_ms": 3.415,
"p999_ms": 10.457,
"max_ms": 18.103,
"histogram": [
11615,
909,
137,
66,
30,
20,
13,
16,
3,
7,
3,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 12970,
"seconds": 12.001,
"avg_fps": 1080.784,
"low_1pct_fps": 165.196,
"low_01pct_fps": 75.886,
"p50_ms": 0.841,
"p95_ms": 1.246,
"p99_ms": 3.428,
"p999_ms": 9.769,
"max_ms": 30.26,
"histogram": [
11881,
845,
93,
62,
27,
15,
16,
9,
11,
6,
3,
1,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 12807,
"seconds": 12.001,
"avg_fps": 1067.189,
"low_1pct_fps": 162.781,
"low_01pct_fps": 73.791,
"p50_ms": 0.842,
"p95_ms": 1.31,
"p99_ms": 3.393,
"p999_ms": 11.153,
"max_ms": 19.229,
"histogram": [
11611,
933,
108,
62,
24,
28,
13,
6,
7,
6,
5,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 3285,
"seconds": 12.002,
"avg_fps": 273.707,
"low_1pct_fps": 40.103,
"low_01pct_fps": 20.453,
"p50_ms": 3.047,
"p95_ms": 7.993,
"p99_ms": 14.596,
"p999_ms": 36.882,
"max_ms": 71.876,
"histogram": [
210,
775,
629,
685,
337,
266,
123,
116,
51,
37,
18,
11,
7,
8,
6,
4,
1,
0,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 13898,
"seconds": 12.0,
"avg_fps": 1158.128,
"low_1pct_fps": 171.541,
"low_01pct_fps": 70.612,
"p50_ms": 0.716,
"p95_ms": 1.492,
"p99_ms": 3.27,
"p999_ms": 9.156,
"max_ms": 21.227,
"histogram": [
12221,
1268,
244,
83,
23,
20,
8,
9,
10,
3,
4,
1,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 14996,
"seconds": 12.0,
"avg_fps": 1249.655,
"low_1pct_fps": 213.218,
"low_01pct_fps": 79.261,
"p50_ms": 0.713,
"p95_ms": 1.166,
"p99_ms": 2.533,
"p999_ms": 8.971,
"max_ms": 18.592,
"histogram": [
13939,
816,
131,
58,
17,
8,
8,
4,
4,
3,
2,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 3175,
"seconds": 12.002,
"avg_fps": 264.54,
"low_1pct_fps": 15.734,
"low_01pct_fps": 3.177,
"p50_ms": 2.117,
"p95_ms": 9.913,
"p99_ms": 20.257,
"p999_ms": 48.141,
"max_ms": 619.805,
"histogram": [
319,
1186,
482,
399,
211,
166,
102,
83,
71,
48,
30,
30,
16,
16,
6,
3,
4,
1,
0,
0,
0,
0,
2,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 8543,
"seconds": 12.001,
"avg_fps": 711.867,
"low_1pct_fps": 39.646,
"low_01pct_fps": 5.706,
"p50_ms": 1.033,
"p95_ms": 2.025,
"p99_ms": 5.49,
"p999_ms": 17.993,
"max_ms": 365.508,
"histogram": [
2186,
5922,
181,
98,
43,
43,
22,
17,
12,
5,
2,
3,
1,
2,
0,
0,
0,
0,
0,
0,
3,
3,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 8112,
"seconds": 12.0,
"avg_fps": 675.978,
"low_1pct_fps": 44.271,
"low_01pct_fps": 7.092,
"p50_ms": 1.043,
"p95_ms": 2.629,
"p99_ms": 5.842,
"p999_ms": 16.976,
"max_ms": 258.087,
"histogram": [
1494,
5923,
365,
141,
57,
61,
24,
19,
6,
5,
6,
2,
3,
1,
0,
0,
0,
0,
0,
0,
3,
2,
0,
0
]
}
],
"label": "vanilla-desktop"
}
-73
View File
@@ -1,73 +0,0 @@
[
{
"machine": "Mac mini (M4 Pro)",
"reports": [
"macmini/horizon-macmini-rd12.json"
],
"version": "v2026.10.22.04",
"capped_fps": 144,
"note": "On macOS this Horizon build presents its frames in step with the 144 Hz display even with VSync off, so its average frame rate here is capped at 144: compare the 1% lows and frame times instead. The Mac mini was also running other jobs during the runs (CI, simulators; 1-minute load average 20 to 56 on 14 threads), which costs both games, vanilla more at render distance 32."
},
{
"machine": "Mac mini (M4 Pro)",
"reports": [
"macmini/horizon-macmini-rd32.json"
],
"version": "v2026.10.22.04",
"capped_fps": 144
},
{
"machine": "Mac mini (M4 Pro)",
"reports": [
"macmini/vanilla-macmini-rd12.json"
]
},
{
"machine": "Mac mini (M4 Pro)",
"reports": [
"macmini/vanilla-macmini-rd32.json"
]
},
{
"machine": "ThinkPad (i7-7500U, HD Graphics 620)",
"reports": [
"thinkpad/horizon-thinkpad-rd12.json"
],
"version": "v2026.10.22.04",
"note": "A 2016 laptop: 2 cores / 4 threads and Intel integrated graphics, Linux with Mesa 26.1.8 (Horizon on Vulkan, vanilla on OpenGL). It has no screen attached, so both games drew to a GPU compositor (sway on the Intel GPU) in a 1280x720 window. Horizon ran two flights per scene, vanilla two separate games of one. Render distance 32 is not shown: in 5 minutes neither game finished loading the spawn, and given 25, Horizon kept only about 900 to 1,400 chunk columns resident on this machine instead of the 3,200 a full view holds, so the two games would not have drawn the same world."
},
{
"machine": "ThinkPad (i7-7500U, HD Graphics 620)",
"reports": [
"thinkpad/vanilla-thinkpad-r1-rd12.json",
"thinkpad/vanilla-thinkpad-r2-rd12.json"
]
},
{
"machine": "Desktop (RTX 4080, i7-13700KF)",
"reports": [
"desktop/horizon-desktop-rd12.json"
],
"version": "v2026.10.22.13",
"note": "Linux (NixOS) with the NVIDIA 595 driver. The desktop is in daily use, so both games drew to a separate GPU compositor (a headless sway rendering on the RTX 4080) in a 1280x720 window, never on its screen: Horizon through Wayland, vanilla through Xwayland, both on Vulkan. Other jobs were running during the runs (1-minute load average 18 to 25 on 24 threads), which costs both games, chunk loading most."
},
{
"machine": "Desktop (RTX 4080, i7-13700KF)",
"reports": [
"desktop/horizon-desktop-rd32.json"
],
"version": "v2026.10.22.13"
},
{
"machine": "Desktop (RTX 4080, i7-13700KF)",
"reports": [
"desktop/vanilla-desktop-rd12.json"
]
},
{
"machine": "Desktop (RTX 4080, i7-13700KF)",
"reports": [
"desktop/vanilla-desktop-rd32.json"
]
}
]
@@ -1,758 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-22T18:57:41Z",
"app": {
"name": "horizon-client",
"version": "0.1.0",
"commit": "02260048"
},
"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": false,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 55.691,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 2345.372,
"stdev": 439.947,
"min": 2086.467,
"max": 2853.347,
"ci95": 1092.977
},
"startup.world_ms": {
"n": 3,
"mean": 11074.92,
"stdev": 1749.229,
"min": 9853.098,
"max": 13078.729,
"ci95": 4345.676
},
"chunks.village.load_ms": {
"n": 3,
"mean": 6990.177,
"stdev": 715.08,
"min": 6305.128,
"max": 7731.914,
"ci95": 1776.501
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 15984.667,
"stdev": 344.848,
"min": 15711.0,
"max": 16372.0,
"ci95": 856.72
},
"fps.village.avg_fps": {
"n": 3,
"mean": 143.667,
"stdev": 0.332,
"min": 143.283,
"max": 143.863,
"ci95": 0.826
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 97.283,
"stdev": 17.85,
"min": 77.659,
"max": 112.553,
"ci95": 44.345
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 86.892,
"stdev": 10.682,
"min": 74.96,
"max": 95.564,
"ci95": 26.537
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 10500.388,
"stdev": 2322.043,
"min": 8609.286,
"max": 13092.051,
"ci95": 5768.741
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 18418.333,
"stdev": 765.36,
"min": 17536.0,
"max": 18903.0,
"ci95": 1901.414
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 143.813,
"stdev": 0.113,
"min": 143.684,
"max": 143.893,
"ci95": 0.28
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 100.192,
"stdev": 11.893,
"min": 87.358,
"max": 110.84,
"ci95": 29.546
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 83.715,
"stdev": 6.798,
"min": 75.936,
"max": 88.513,
"ci95": 16.888
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 10672.488,
"stdev": 6950.689,
"min": 6083.379,
"max": 18669.422,
"ci95": 17267.862
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 15184.667,
"stdev": 108.039,
"min": 15096.0,
"max": 15305.0,
"ci95": 268.404
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 143.702,
"stdev": 0.224,
"min": 143.449,
"max": 143.877,
"ci95": 0.558
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 97.956,
"stdev": 16.083,
"min": 82.809,
"max": 114.835,
"ci95": 39.956
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 83.818,
"stdev": 14.218,
"min": 74.969,
"max": 100.219,
"ci95": 35.324
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 9587.759,
"stdev": 2756.675,
"min": 7452.155,
"max": 12699.737,
"ci95": 6848.513
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 16126.0,
"stdev": 665.232,
"min": 15575.0,
"max": 16865.0,
"ci95": 1652.66
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 143.829,
"stdev": 0.126,
"min": 143.691,
"max": 143.938,
"ci95": 0.313
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 93.631,
"stdev": 9.702,
"min": 83.285,
"max": 102.526,
"ci95": 24.104
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 76.626,
"stdev": 0.844,
"min": 75.806,
"max": 77.493,
"ci95": 2.098
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 1332.448,
"stdev": 38.71,
"min": 1289.484,
"max": 1364.609,
"ci95": 96.168
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1201.484,
"stdev": 14.839,
"min": 1184.859,
"max": 1213.391,
"ci95": 36.866
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 1967.601,
"stdev": 14.451,
"min": 1958.158,
"max": 1984.236,
"ci95": 35.9
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 1678.093,
"stdev": 11.285,
"min": 1666.799,
"max": 1689.369,
"ci95": 28.036
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 1727,
"seconds": 12.005,
"avg_fps": 143.854,
"low_1pct_fps": 101.638,
"low_01pct_fps": 90.151,
"p50_ms": 6.95,
"p95_ms": 7.788,
"p99_ms": 9.014,
"p999_ms": 10.416,
"max_ms": 11.769,
"cpu_p50_ms": 0.685,
"gpu_p50_ms": 0.0,
"histogram": [
0,
1,
0,
6,
12,
38,
1003,
635,
26,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 1727,
"seconds": 12.004,
"avg_fps": 143.863,
"low_1pct_fps": 112.553,
"low_01pct_fps": 95.564,
"p50_ms": 6.952,
"p95_ms": 7.703,
"p99_ms": 8.321,
"p999_ms": 9.559,
"max_ms": 11.369,
"cpu_p50_ms": 0.658,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
1,
0,
7,
43,
990,
669,
16,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 1720,
"seconds": 12.004,
"avg_fps": 143.283,
"low_1pct_fps": 77.659,
"low_01pct_fps": 74.96,
"p50_ms": 6.95,
"p95_ms": 7.725,
"p99_ms": 12.282,
"p999_ms": 13.317,
"max_ms": 13.364,
"cpu_p50_ms": 0.675,
"gpu_p50_ms": 0.0,
"histogram": [
4,
8,
0,
13,
3,
20,
1011,
624,
10,
9,
18,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 1727,
"seconds": 12.002,
"avg_fps": 143.893,
"low_1pct_fps": 102.379,
"low_01pct_fps": 88.513,
"p50_ms": 6.951,
"p95_ms": 7.722,
"p99_ms": 8.837,
"p999_ms": 11.08,
"max_ms": 11.516,
"cpu_p50_ms": 0.636,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
2,
7,
10,
27,
1025,
632,
20,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 1727,
"seconds": 12.005,
"avg_fps": 143.861,
"low_1pct_fps": 110.84,
"low_01pct_fps": 86.696,
"p50_ms": 6.95,
"p95_ms": 7.699,
"p99_ms": 8.258,
"p999_ms": 10.764,
"max_ms": 12.305,
"cpu_p50_ms": 0.628,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
1,
2,
3,
33,
1036,
637,
12,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 1725,
"seconds": 12.006,
"avg_fps": 143.684,
"low_1pct_fps": 87.358,
"low_01pct_fps": 75.936,
"p50_ms": 6.947,
"p95_ms": 7.69,
"p99_ms": 9.094,
"p999_ms": 13.064,
"max_ms": 13.274,
"cpu_p50_ms": 0.617,
"gpu_p50_ms": 0.0,
"histogram": [
0,
4,
1,
10,
6,
9,
1044,
628,
10,
4,
9,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 1727,
"seconds": 12.003,
"avg_fps": 143.877,
"low_1pct_fps": 114.835,
"low_01pct_fps": 100.219,
"p50_ms": 6.948,
"p95_ms": 7.663,
"p99_ms": 8.021,
"p999_ms": 9.584,
"max_ms": 10.372,
"cpu_p50_ms": 0.631,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
0,
1,
4,
19,
1058,
632,
12,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 1726,
"seconds": 12.004,
"avg_fps": 143.78,
"low_1pct_fps": 96.223,
"low_01pct_fps": 76.266,
"p50_ms": 6.95,
"p95_ms": 7.675,
"p99_ms": 8.561,
"p999_ms": 12.865,
"max_ms": 13.359,
"cpu_p50_ms": 0.65,
"gpu_p50_ms": 0.0,
"histogram": [
2,
0,
0,
9,
5,
15,
1062,
615,
9,
5,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 1723,
"seconds": 12.011,
"avg_fps": 143.449,
"low_1pct_fps": 82.809,
"low_01pct_fps": 74.969,
"p50_ms": 6.949,
"p95_ms": 7.7,
"p99_ms": 9.785,
"p999_ms": 12.978,
"max_ms": 13.699,
"cpu_p50_ms": 0.636,
"gpu_p50_ms": 0.0,
"histogram": [
2,
6,
1,
5,
5,
17,
1071,
585,
14,
5,
12,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 1727,
"seconds": 12.005,
"avg_fps": 143.858,
"low_1pct_fps": 102.526,
"low_01pct_fps": 76.579,
"p50_ms": 6.947,
"p95_ms": 7.68,
"p99_ms": 8.234,
"p999_ms": 12.879,
"max_ms": 13.238,
"cpu_p50_ms": 0.688,
"gpu_p50_ms": 0.0,
"histogram": [
0,
1,
1,
4,
7,
16,
1066,
616,
12,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 1728,
"seconds": 12.005,
"avg_fps": 143.938,
"low_1pct_fps": 95.083,
"low_01pct_fps": 75.806,
"p50_ms": 6.944,
"p95_ms": 7.709,
"p99_ms": 8.959,
"p999_ms": 13.175,
"max_ms": 13.208,
"cpu_p50_ms": 0.706,
"gpu_p50_ms": 0.0,
"histogram": [
0,
1,
5,
10,
4,
22,
1019,
640,
17,
7,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 1725,
"seconds": 12.005,
"avg_fps": 143.691,
"low_1pct_fps": 83.285,
"low_01pct_fps": 77.493,
"p50_ms": 6.943,
"p95_ms": 7.714,
"p99_ms": 10.345,
"p999_ms": 12.837,
"max_ms": 12.972,
"cpu_p50_ms": 0.69,
"gpu_p50_ms": 0.0,
"histogram": [
0,
11,
0,
7,
5,
24,
1035,
612,
13,
7,
11,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-macmini"
}
@@ -1,758 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-22T19:04:50Z",
"app": {
"name": "horizon-client",
"version": "0.1.0",
"commit": "02260048"
},
"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": false,
"repeats": 3,
"warmup_s": 2.0,
"load_avg_1m": 34.828,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 1790.674,
"stdev": 56.914,
"min": 1752.696,
"max": 1856.111,
"ci95": 141.393
},
"startup.world_ms": {
"n": 3,
"mean": 16377.0,
"stdev": 336.835,
"min": 16104.909,
"max": 16753.738,
"ci95": 836.813
},
"chunks.village.load_ms": {
"n": 3,
"mean": 12453.023,
"stdev": 604.693,
"min": 11803.423,
"max": 12999.563,
"ci95": 1502.262
},
"chunks.village.meshed_count": {
"n": 3,
"mean": 51719.0,
"stdev": 122.491,
"min": 51587.0,
"max": 51829.0,
"ci95": 304.309
},
"fps.village.avg_fps": {
"n": 3,
"mean": 143.847,
"stdev": 0.057,
"min": 143.783,
"max": 143.892,
"ci95": 0.141
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 101.488,
"stdev": 2.262,
"min": 99.902,
"max": 104.078,
"ci95": 5.619
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 84.797,
"stdev": 3.7,
"min": 80.571,
"max": 87.457,
"ci95": 9.193
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 16309.457,
"stdev": 928.209,
"min": 15257.604,
"max": 17013.641,
"ci95": 2305.986
},
"chunks.forest.meshed_count": {
"n": 3,
"mean": 87279.0,
"stdev": 407.052,
"min": 86853.0,
"max": 87664.0,
"ci95": 1011.254
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 143.86,
"stdev": 0.001,
"min": 143.859,
"max": 143.86,
"ci95": 0.001
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 113.796,
"stdev": 13.168,
"min": 98.591,
"max": 121.412,
"ci95": 32.713
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 94.786,
"stdev": 14.752,
"min": 80.86,
"max": 110.244,
"ci95": 36.649
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 14321.991,
"stdev": 573.379,
"min": 13755.732,
"max": 14902.235,
"ci95": 1424.468
},
"chunks.ocean.meshed_count": {
"n": 3,
"mean": 77599.667,
"stdev": 139.429,
"min": 77500.0,
"max": 77759.0,
"ci95": 346.388
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 143.832,
"stdev": 0.049,
"min": 143.777,
"max": 143.872,
"ci95": 0.122
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 118.94,
"stdev": 3.668,
"min": 114.708,
"max": 121.215,
"ci95": 9.113
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 93.392,
"stdev": 2.385,
"min": 90.66,
"max": 95.059,
"ci95": 5.925
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 22432.475,
"stdev": 553.334,
"min": 22065.669,
"max": 23068.944,
"ci95": 1374.669
},
"chunks.mountain.meshed_count": {
"n": 3,
"mean": 92974.333,
"stdev": 374.559,
"min": 92542.0,
"max": 93201.0,
"ci95": 930.531
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 143.783,
"stdev": 0.013,
"min": 143.772,
"max": 143.797,
"ci95": 0.032
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 100.357,
"stdev": 3.901,
"min": 97.468,
"max": 104.795,
"ci95": 9.692
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 74.988,
"stdev": 1.321,
"min": 74.05,
"max": 76.499,
"ci95": 3.282
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2267.208,
"stdev": 19.513,
"min": 2245.516,
"max": 2283.328,
"ci95": 48.476
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 1892.026,
"stdev": 34.165,
"min": 1854.414,
"max": 1921.141,
"ci95": 84.878
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 4916.789,
"stdev": 86.517,
"min": 4817.112,
"max": 4972.425,
"ci95": 214.938
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 3822.76,
"stdev": 13.305,
"min": 3807.417,
"max": 3831.096,
"ci95": 33.053
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 1726,
"seconds": 12.004,
"avg_fps": 143.783,
"low_1pct_fps": 104.078,
"low_01pct_fps": 87.457,
"p50_ms": 6.947,
"p95_ms": 7.68,
"p99_ms": 8.338,
"p999_ms": 11.28,
"max_ms": 11.588,
"cpu_p50_ms": 1.378,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
2,
4,
4,
15,
1036,
647,
12,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 1727,
"seconds": 12.002,
"avg_fps": 143.892,
"low_1pct_fps": 99.902,
"low_01pct_fps": 86.362,
"p50_ms": 6.954,
"p95_ms": 7.654,
"p99_ms": 8.863,
"p999_ms": 11.513,
"max_ms": 11.645,
"cpu_p50_ms": 1.4,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
4,
7,
7,
20,
994,
669,
17,
9,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 1727,
"seconds": 12.004,
"avg_fps": 143.865,
"low_1pct_fps": 100.485,
"low_01pct_fps": 80.571,
"p50_ms": 6.952,
"p95_ms": 7.729,
"p99_ms": 8.604,
"p999_ms": 12.182,
"max_ms": 12.64,
"cpu_p50_ms": 1.407,
"gpu_p50_ms": 0.0,
"histogram": [
0,
2,
1,
3,
7,
29,
977,
687,
14,
5,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 1727,
"seconds": 12.005,
"avg_fps": 143.86,
"low_1pct_fps": 98.591,
"low_01pct_fps": 80.86,
"p50_ms": 6.953,
"p95_ms": 7.732,
"p99_ms": 8.83,
"p999_ms": 12.092,
"max_ms": 12.642,
"cpu_p50_ms": 1.432,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
4,
5,
7,
27,
978,
681,
17,
6,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 1727,
"seconds": 12.005,
"avg_fps": 143.86,
"low_1pct_fps": 121.384,
"low_01pct_fps": 93.253,
"p50_ms": 6.954,
"p95_ms": 7.634,
"p99_ms": 7.827,
"p999_ms": 8.154,
"max_ms": 13.293,
"cpu_p50_ms": 1.35,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
0,
1,
1,
14,
1027,
683,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 1727,
"seconds": 12.005,
"avg_fps": 143.859,
"low_1pct_fps": 121.412,
"low_01pct_fps": 110.244,
"p50_ms": 6.952,
"p95_ms": 7.707,
"p99_ms": 7.941,
"p999_ms": 9.001,
"max_ms": 9.141,
"cpu_p50_ms": 1.415,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
0,
0,
3,
24,
1006,
689,
5,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 1727,
"seconds": 12.004,
"avg_fps": 143.872,
"low_1pct_fps": 114.708,
"low_01pct_fps": 90.66,
"p50_ms": 6.95,
"p95_ms": 7.685,
"p99_ms": 7.957,
"p999_ms": 10.313,
"max_ms": 11.747,
"cpu_p50_ms": 1.316,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
1,
0,
5,
15,
1046,
650,
8,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 1727,
"seconds": 12.006,
"avg_fps": 143.847,
"low_1pct_fps": 120.896,
"low_01pct_fps": 94.456,
"p50_ms": 6.948,
"p95_ms": 7.635,
"p99_ms": 7.829,
"p999_ms": 9.702,
"max_ms": 11.472,
"cpu_p50_ms": 1.16,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
1,
1,
0,
10,
1024,
688,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 1726,
"seconds": 12.005,
"avg_fps": 143.777,
"low_1pct_fps": 121.215,
"low_01pct_fps": 95.059,
"p50_ms": 6.953,
"p95_ms": 7.66,
"p99_ms": 7.856,
"p999_ms": 8.69,
"max_ms": 12.35,
"cpu_p50_ms": 1.248,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
0,
0,
0,
14,
1039,
671,
1,
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 1726,
"seconds": 12.003,
"avg_fps": 143.797,
"low_1pct_fps": 98.809,
"low_01pct_fps": 74.415,
"p50_ms": 6.95,
"p95_ms": 7.847,
"p99_ms": 9.041,
"p999_ms": 12.965,
"max_ms": 13.911,
"cpu_p50_ms": 1.988,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
2,
3,
13,
54,
946,
669,
33,
4,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 1726,
"seconds": 12.005,
"avg_fps": 143.772,
"low_1pct_fps": 104.795,
"low_01pct_fps": 76.499,
"p50_ms": 6.952,
"p95_ms": 7.647,
"p99_ms": 8.489,
"p999_ms": 12.811,
"max_ms": 13.333,
"cpu_p50_ms": 1.758,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
0,
2,
5,
21,
975,
703,
16,
2,
2,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 1726,
"seconds": 12.005,
"avg_fps": 143.779,
"low_1pct_fps": 97.468,
"low_01pct_fps": 74.05,
"p50_ms": 6.949,
"p95_ms": 7.808,
"p99_ms": 8.836,
"p999_ms": 13.384,
"max_ms": 13.624,
"cpu_p50_ms": 1.93,
"gpu_p50_ms": 0.0,
"histogram": [
0,
0,
3,
3,
9,
36,
975,
671,
23,
2,
4,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "horizon-macmini"
}
@@ -1,707 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-22T19:11:15Z",
"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": 19.995,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 9255.174,
"stdev": 1377.709,
"min": 8291.517,
"max": 10833.18,
"ci95": 3422.694
},
"startup.world_ms": {
"n": 3,
"mean": 22267.841,
"stdev": 2359.592,
"min": 19676.825,
"max": 24293.18,
"ci95": 5862.025
},
"chunks.village.load_ms": {
"n": 3,
"mean": 8373.541,
"stdev": 1607.431,
"min": 6736.307,
"max": 9949.411,
"ci95": 3993.401
},
"fps.village.avg_fps": {
"n": 3,
"mean": 136.491,
"stdev": 49.607,
"min": 80.587,
"max": 175.255,
"ci95": 123.241
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 40.05,
"stdev": 20.951,
"min": 17.531,
"max": 58.964,
"ci95": 52.048
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 27.562,
"stdev": 16.348,
"min": 9.896,
"max": 42.155,
"ci95": 40.613
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 10975.903,
"stdev": 2773.208,
"min": 7859.955,
"max": 13173.278,
"ci95": 6889.586
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 114.749,
"stdev": 25.895,
"min": 88.614,
"max": 140.397,
"ci95": 64.332
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 22.337,
"stdev": 2.265,
"min": 20.316,
"max": 24.786,
"ci95": 5.628
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 13.925,
"stdev": 1.313,
"min": 12.44,
"max": 14.934,
"ci95": 3.263
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 7219.032,
"stdev": 512.777,
"min": 6863.314,
"max": 7806.817,
"ci95": 1273.913
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 127.344,
"stdev": 39.331,
"min": 87.832,
"max": 166.491,
"ci95": 97.711
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 28.658,
"stdev": 13.052,
"min": 19.943,
"max": 43.664,
"ci95": 32.426
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 17.368,
"stdev": 3.553,
"min": 14.965,
"max": 21.449,
"ci95": 8.826
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 14487.3,
"stdev": 1978.448,
"min": 12307.989,
"max": 16170.426,
"ci95": 4915.133
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 112.429,
"stdev": 10.054,
"min": 101.699,
"max": 121.631,
"ci95": 24.976
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 23.328,
"stdev": 4.326,
"min": 18.537,
"max": 26.948,
"ci95": 10.748
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 15.99,
"stdev": 3.13,
"min": 13.061,
"max": 19.289,
"ci95": 7.777
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2444.542,
"stdev": 59.065,
"min": 2402.813,
"max": 2512.125,
"ci95": 146.737
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2283.372,
"stdev": 54.05,
"min": 2223.469,
"max": 2328.492,
"ci95": 134.278
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 3142.514,
"stdev": 65.681,
"min": 3092.55,
"max": 3216.91,
"ci95": 163.174
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 2963.105,
"stdev": 109.382,
"min": 2845.722,
"max": 3062.175,
"ci95": 271.743
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 2104,
"seconds": 12.005,
"avg_fps": 175.255,
"low_1pct_fps": 58.964,
"low_01pct_fps": 42.155,
"p50_ms": 5.974,
"p95_ms": 7.928,
"p99_ms": 11.472,
"p999_ms": 21.981,
"max_ms": 25.823,
"histogram": [
0,
0,
31,
626,
131,
268,
600,
374,
41,
14,
4,
5,
4,
5,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 968,
"seconds": 12.012,
"avg_fps": 80.587,
"low_1pct_fps": 17.531,
"low_01pct_fps": 9.896,
"p50_ms": 10.008,
"p95_ms": 27.353,
"p99_ms": 40.768,
"p999_ms": 101.056,
"max_ms": 101.056,
"histogram": [
0,
0,
0,
10,
6,
40,
121,
163,
143,
133,
103,
72,
62,
51,
40,
12,
7,
3,
1,
1,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 1844,
"seconds": 12.003,
"avg_fps": 153.632,
"low_1pct_fps": 43.656,
"low_01pct_fps": 30.635,
"p50_ms": 6.497,
"p95_ms": 10.85,
"p99_ms": 18.965,
"p999_ms": 31.548,
"max_ms": 33.737,
"histogram": [
0,
0,
32,
343,
146,
267,
472,
344,
121,
51,
18,
24,
12,
11,
2,
1,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 1064,
"seconds": 12.007,
"avg_fps": 88.614,
"low_1pct_fps": 20.316,
"low_01pct_fps": 14.401,
"p50_ms": 9.718,
"p95_ms": 23.959,
"p99_ms": 38.344,
"p999_ms": 66.283,
"max_ms": 72.6,
"histogram": [
0,
0,
0,
28,
17,
52,
151,
169,
157,
165,
103,
82,
50,
47,
26,
7,
7,
2,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 1383,
"seconds": 12.001,
"avg_fps": 115.237,
"low_1pct_fps": 24.786,
"low_01pct_fps": 14.934,
"p50_ms": 7.377,
"p95_ms": 15.575,
"p99_ms": 23.447,
"p999_ms": 59.361,
"max_ms": 74.564,
"histogram": [
0,
0,
0,
31,
57,
146,
307,
316,
203,
150,
75,
41,
34,
11,
4,
1,
4,
2,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 1687,
"seconds": 12.016,
"avg_fps": 140.397,
"low_1pct_fps": 21.91,
"low_01pct_fps": 12.44,
"p50_ms": 6.331,
"p95_ms": 15.139,
"p99_ms": 31.057,
"p999_ms": 75.339,
"max_ms": 85.428,
"histogram": [
0,
0,
21,
337,
165,
245,
396,
251,
104,
47,
26,
28,
16,
23,
15,
4,
5,
2,
2,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 1999,
"seconds": 12.007,
"avg_fps": 166.491,
"low_1pct_fps": 43.664,
"low_01pct_fps": 21.449,
"p50_ms": 5.909,
"p95_ms": 9.406,
"p99_ms": 16.205,
"p999_ms": 45.922,
"max_ms": 47.324,
"histogram": [
0,
0,
30,
481,
178,
349,
520,
277,
84,
34,
16,
12,
8,
7,
1,
0,
2,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 1054,
"seconds": 12.0,
"avg_fps": 87.832,
"low_1pct_fps": 19.943,
"low_01pct_fps": 15.69,
"p50_ms": 9.863,
"p95_ms": 21.668,
"p99_ms": 39.308,
"p999_ms": 62.346,
"max_ms": 65.125,
"histogram": [
0,
0,
0,
5,
15,
93,
121,
162,
147,
176,
100,
87,
70,
40,
22,
6,
6,
4,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 1533,
"seconds": 12.004,
"avg_fps": 127.709,
"low_1pct_fps": 22.367,
"low_01pct_fps": 14.965,
"p50_ms": 6.703,
"p95_ms": 17.53,
"p99_ms": 35.256,
"p999_ms": 63.191,
"max_ms": 70.458,
"histogram": [
0,
0,
13,
222,
109,
208,
366,
275,
122,
69,
37,
28,
20,
29,
18,
8,
6,
2,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 1368,
"seconds": 12.004,
"avg_fps": 113.958,
"low_1pct_fps": 26.948,
"low_01pct_fps": 19.289,
"p50_ms": 7.238,
"p95_ms": 18.057,
"p99_ms": 29.957,
"p999_ms": 49.049,
"max_ms": 54.639,
"histogram": [
0,
0,
0,
88,
46,
128,
327,
284,
165,
133,
75,
41,
37,
22,
15,
3,
3,
1,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 1221,
"seconds": 12.006,
"avg_fps": 101.699,
"low_1pct_fps": 18.537,
"low_01pct_fps": 13.061,
"p50_ms": 7.5,
"p95_ms": 21.53,
"p99_ms": 39.922,
"p999_ms": 70.942,
"max_ms": 82.181,
"histogram": [
0,
0,
2,
62,
36,
105,
288,
232,
146,
109,
64,
47,
45,
48,
18,
7,
5,
5,
2,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 1460,
"seconds": 12.004,
"avg_fps": 121.631,
"low_1pct_fps": 24.5,
"low_01pct_fps": 15.62,
"p50_ms": 6.975,
"p95_ms": 17.318,
"p99_ms": 31.34,
"p999_ms": 59.755,
"max_ms": 68.287,
"histogram": [
0,
0,
8,
144,
75,
131,
381,
321,
129,
107,
51,
36,
24,
30,
11,
6,
4,
1,
1,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-macmini"
}
@@ -1,707 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-22T19:28:11Z",
"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": 24.818,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 3,
"mean": 11381.196,
"stdev": 3370.766,
"min": 8420.137,
"max": 15049.455,
"ci95": 8374.123
},
"startup.world_ms": {
"n": 3,
"mean": 65830.529,
"stdev": 11011.236,
"min": 56970.455,
"max": 78158.137,
"ci95": 27355.633
},
"chunks.village.load_ms": {
"n": 3,
"mean": 23987.198,
"stdev": 11152.991,
"min": 17293.537,
"max": 36862.165,
"ci95": 27707.801
},
"fps.village.avg_fps": {
"n": 3,
"mean": 47.737,
"stdev": 31.496,
"min": 27.991,
"max": 84.059,
"ci95": 78.247
},
"fps.village.low_1pct_fps": {
"n": 3,
"mean": 6.2,
"stdev": 2.609,
"min": 4.528,
"max": 9.206,
"ci95": 6.481
},
"fps.village.low_01pct_fps": {
"n": 3,
"mean": 4.617,
"stdev": 1.026,
"min": 3.768,
"max": 5.758,
"ci95": 2.55
},
"chunks.forest.load_ms": {
"n": 3,
"mean": 36378.52,
"stdev": 15983.433,
"min": 24065.993,
"max": 54441.562,
"ci95": 39708.252
},
"fps.forest.avg_fps": {
"n": 3,
"mean": 53.928,
"stdev": 33.496,
"min": 28.564,
"max": 91.898,
"ci95": 83.215
},
"fps.forest.low_1pct_fps": {
"n": 3,
"mean": 12.927,
"stdev": 13.712,
"min": 4.023,
"max": 28.717,
"ci95": 34.065
},
"fps.forest.low_01pct_fps": {
"n": 3,
"mean": 9.842,
"stdev": 11.775,
"min": 2.927,
"max": 23.438,
"ci95": 29.254
},
"chunks.ocean.load_ms": {
"n": 3,
"mean": 18553.145,
"stdev": 3789.708,
"min": 14921.886,
"max": 22483.568,
"ci95": 9414.916
},
"fps.ocean.avg_fps": {
"n": 3,
"mean": 48.22,
"stdev": 8.307,
"min": 38.643,
"max": 53.477,
"ci95": 20.638
},
"fps.ocean.low_1pct_fps": {
"n": 3,
"mean": 7.916,
"stdev": 2.369,
"min": 6.195,
"max": 10.617,
"ci95": 5.884
},
"fps.ocean.low_01pct_fps": {
"n": 3,
"mean": 5.755,
"stdev": 2.962,
"min": 3.274,
"max": 9.034,
"ci95": 7.358
},
"chunks.mountain.load_ms": {
"n": 3,
"mean": 123216.543,
"stdev": 45006.944,
"min": 74469.433,
"max": 163191.844,
"ci95": 111812.471
},
"fps.mountain.avg_fps": {
"n": 3,
"mean": 23.396,
"stdev": 13.96,
"min": 12.505,
"max": 39.134,
"ci95": 34.682
},
"fps.mountain.low_1pct_fps": {
"n": 3,
"mean": 3.144,
"stdev": 2.263,
"min": 1.469,
"max": 5.719,
"ci95": 5.623
},
"fps.mountain.low_01pct_fps": {
"n": 3,
"mean": 1.601,
"stdev": 0.663,
"min": 1.033,
"max": 2.329,
"ci95": 1.646
},
"memory.rss_peak_mb": {
"n": 3,
"mean": 2946.771,
"stdev": 9.561,
"min": 2940.781,
"max": 2957.797,
"ci95": 23.752
},
"memory.rss_steady_mb": {
"n": 3,
"mean": 2931.729,
"stdev": 4.529,
"min": 2927.547,
"max": 2936.539,
"ci95": 11.251
},
"memory.footprint_peak_mb": {
"n": 3,
"mean": 4416.488,
"stdev": 139.323,
"min": 4302.254,
"max": 4571.707,
"ci95": 346.127
},
"memory.footprint_steady_mb": {
"n": 3,
"mean": 4208.842,
"stdev": 45.014,
"min": 4162.535,
"max": 4252.441,
"ci95": 111.831
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 1009,
"seconds": 12.003,
"avg_fps": 84.059,
"low_1pct_fps": 9.206,
"low_01pct_fps": 5.758,
"p50_ms": 8.462,
"p95_ms": 25.665,
"p99_ms": 79.305,
"p999_ms": 162.932,
"max_ms": 184.432,
"histogram": [
0,
0,
0,
0,
0,
15,
184,
285,
186,
111,
65,
46,
36,
28,
21,
5,
8,
6,
8,
3,
2,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 375,
"seconds": 12.035,
"avg_fps": 31.16,
"low_1pct_fps": 4.866,
"low_01pct_fps": 4.326,
"p50_ms": 14.505,
"p95_ms": 116.616,
"p99_ms": 195.71,
"p999_ms": 231.163,
"max_ms": 231.163,
"histogram": [
0,
0,
0,
0,
0,
0,
1,
22,
47,
62,
48,
27,
24,
18,
28,
11,
13,
18,
30,
18,
8,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 2,
"frames": 336,
"seconds": 12.004,
"avg_fps": 27.991,
"low_1pct_fps": 4.528,
"low_01pct_fps": 3.768,
"p50_ms": 15.612,
"p95_ms": 140.292,
"p99_ms": 200.831,
"p999_ms": 265.4,
"max_ms": 265.4,
"histogram": [
0,
0,
0,
0,
0,
0,
1,
14,
36,
51,
42,
34,
29,
20,
19,
5,
21,
14,
14,
21,
14,
1,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 1103,
"seconds": 12.002,
"avg_fps": 91.898,
"low_1pct_fps": 28.717,
"low_01pct_fps": 23.438,
"p50_ms": 9.702,
"p95_ms": 19.437,
"p99_ms": 29.906,
"p999_ms": 40.578,
"max_ms": 44.753,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
175,
470,
261,
64,
43,
39,
31,
14,
4,
2,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 496,
"seconds": 12.003,
"avg_fps": 41.322,
"low_1pct_fps": 6.041,
"low_01pct_fps": 3.16,
"p50_ms": 13.948,
"p95_ms": 73.747,
"p99_ms": 106.208,
"p999_ms": 316.463,
"max_ms": 316.463,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
24,
97,
73,
56,
52,
23,
31,
34,
18,
34,
20,
27,
5,
1,
1,
0,
0
]
},
{
"phase": "forest",
"repeat": 2,
"frames": 343,
"seconds": 12.008,
"avg_fps": 28.564,
"low_1pct_fps": 4.023,
"low_01pct_fps": 2.927,
"p50_ms": 22.196,
"p95_ms": 99.833,
"p99_ms": 195.141,
"p999_ms": 341.688,
"max_ms": 341.688,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
17,
39,
30,
28,
32,
45,
46,
24,
22,
18,
25,
9,
6,
2,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 631,
"seconds": 12.01,
"avg_fps": 52.54,
"low_1pct_fps": 10.617,
"low_01pct_fps": 9.034,
"p50_ms": 13.716,
"p95_ms": 49.488,
"p99_ms": 87.282,
"p999_ms": 110.687,
"max_ms": 110.687,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
20,
99,
130,
77,
92,
40,
43,
52,
26,
22,
17,
12,
1,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 646,
"seconds": 12.08,
"avg_fps": 53.477,
"low_1pct_fps": 6.935,
"low_01pct_fps": 3.274,
"p50_ms": 12.88,
"p95_ms": 51.476,
"p99_ms": 95.547,
"p999_ms": 305.469,
"max_ms": 305.469,
"histogram": [
0,
0,
0,
0,
0,
0,
1,
29,
112,
135,
98,
80,
47,
45,
34,
14,
16,
20,
9,
4,
1,
1,
0,
0
]
},
{
"phase": "ocean",
"repeat": 2,
"frames": 464,
"seconds": 12.007,
"avg_fps": 38.643,
"low_1pct_fps": 6.195,
"low_01pct_fps": 4.956,
"p50_ms": 13.978,
"p95_ms": 88.331,
"p99_ms": 146.049,
"p999_ms": 201.767,
"max_ms": 201.767,
"histogram": [
0,
0,
0,
0,
0,
0,
1,
29,
64,
86,
54,
44,
34,
26,
28,
14,
20,
23,
27,
12,
2,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 223,
"seconds": 12.021,
"avg_fps": 18.55,
"low_1pct_fps": 2.245,
"low_01pct_fps": 1.441,
"p50_ms": 33.819,
"p95_ms": 148.781,
"p99_ms": 257.941,
"p999_ms": 693.793,
"max_ms": 693.793,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
2,
6,
14,
20,
23,
26,
19,
9,
21,
26,
30,
16,
8,
2,
1,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 470,
"seconds": 12.01,
"avg_fps": 39.134,
"low_1pct_fps": 5.719,
"low_01pct_fps": 2.329,
"p50_ms": 19.327,
"p95_ms": 55.82,
"p99_ms": 103.388,
"p999_ms": 429.314,
"max_ms": 429.314,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
6,
63,
55,
63,
58,
65,
67,
25,
33,
18,
11,
5,
0,
1,
0,
0
]
},
{
"phase": "mountain",
"repeat": 2,
"frames": 151,
"seconds": 12.075,
"avg_fps": 12.505,
"low_1pct_fps": 1.469,
"low_01pct_fps": 1.033,
"p50_ms": 38.238,
"p95_ms": 192.226,
"p99_ms": 393.788,
"p999_ms": 967.961,
"max_ms": 967.961,
"histogram": [
0,
0,
0,
0,
0,
0,
0,
0,
2,
7,
8,
13,
14,
18,
8,
6,
3,
12,
8,
26,
22,
3,
1,
0
]
}
],
"label": "vanilla-macmini"
}
@@ -1,606 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-22T19:03:59Z",
"app": {
"name": "horizon-client",
"version": "0.1.0",
"commit": "02260048"
},
"hardware": {
"os": "linux",
"arch": "x86_64",
"cpu_threads": 4,
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.55)",
"cpu": "Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz",
"ram_mb": 31984,
"gpu": {
"name": "Intel(R) HD Graphics 620 (KBL GT2)",
"vendor": "intel",
"driver": "Intel open-source Mesa driver Mesa 26.1.8",
"backend": "vulkan",
"kind": "integrated"
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 12,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 2,
"warmup_s": 2.0,
"load_avg_1m": 0.988,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 2,
"mean": 3359.833,
"stdev": 71.241,
"min": 3309.458,
"max": 3410.208,
"ci95": 640.068
},
"startup.world_ms": {
"n": 2,
"mean": 50794.052,
"stdev": 11192.703,
"min": 42879.616,
"max": 58708.488,
"ci95": 100560.827
},
"chunks.village.load_ms": {
"n": 2,
"mean": 44860.072,
"stdev": 4027.103,
"min": 42012.48,
"max": 47707.664,
"ci95": 36181.501
},
"chunks.village.meshed_count": {
"n": 2,
"mean": 18412.5,
"stdev": 723.37,
"min": 17901.0,
"max": 18924.0,
"ci95": 6499.119
},
"fps.village.avg_fps": {
"n": 2,
"mean": 272.998,
"stdev": 15.604,
"min": 261.964,
"max": 284.031,
"ci95": 140.192
},
"fps.village.low_1pct_fps": {
"n": 2,
"mean": 45.939,
"stdev": 2.832,
"min": 43.936,
"max": 47.941,
"ci95": 25.444
},
"fps.village.low_01pct_fps": {
"n": 2,
"mean": 29.722,
"stdev": 2.801,
"min": 27.741,
"max": 31.702,
"ci95": 25.164
},
"memory.village.gpu_mb": {
"n": 2,
"mean": 402.965,
"stdev": 2.093,
"min": 401.486,
"max": 404.445,
"ci95": 18.804
},
"chunks.forest.load_ms": {
"n": 2,
"mean": 67527.123,
"stdev": 1738.341,
"min": 66297.93,
"max": 68756.315,
"ci95": 15618.12
},
"chunks.forest.meshed_count": {
"n": 2,
"mean": 23490.0,
"stdev": 992.778,
"min": 22788.0,
"max": 24192.0,
"ci95": 8919.612
},
"fps.forest.avg_fps": {
"n": 2,
"mean": 246.342,
"stdev": 2.258,
"min": 244.745,
"max": 247.939,
"ci95": 20.291
},
"fps.forest.low_1pct_fps": {
"n": 2,
"mean": 48.098,
"stdev": 2.354,
"min": 46.433,
"max": 49.762,
"ci95": 21.149
},
"fps.forest.low_01pct_fps": {
"n": 2,
"mean": 31.405,
"stdev": 2.788,
"min": 29.433,
"max": 33.376,
"ci95": 25.05
},
"memory.forest.gpu_mb": {
"n": 2,
"mean": 458.26,
"stdev": 2.943,
"min": 456.179,
"max": 460.341,
"ci95": 26.443
},
"chunks.ocean.load_ms": {
"n": 2,
"mean": 51484.922,
"stdev": 3499.487,
"min": 49010.411,
"max": 53959.434,
"ci95": 31441.14
},
"chunks.ocean.meshed_count": {
"n": 2,
"mean": 17323.5,
"stdev": 75.66,
"min": 17270.0,
"max": 17377.0,
"ci95": 679.771
},
"fps.ocean.avg_fps": {
"n": 2,
"mean": 204.022,
"stdev": 2.495,
"min": 202.258,
"max": 205.786,
"ci95": 22.413
},
"fps.ocean.low_1pct_fps": {
"n": 2,
"mean": 36.306,
"stdev": 0.351,
"min": 36.058,
"max": 36.554,
"ci95": 3.151
},
"fps.ocean.low_01pct_fps": {
"n": 2,
"mean": 25.509,
"stdev": 0.501,
"min": 25.155,
"max": 25.863,
"ci95": 4.498
},
"memory.ocean.gpu_mb": {
"n": 2,
"mean": 421.627,
"stdev": 3.887,
"min": 418.878,
"max": 424.376,
"ci95": 34.926
},
"chunks.mountain.load_ms": {
"n": 2,
"mean": 75031.799,
"stdev": 8886.455,
"min": 68748.127,
"max": 81315.471,
"ci95": 79840.341
},
"chunks.mountain.meshed_count": {
"n": 2,
"mean": 17961.5,
"stdev": 225.567,
"min": 17802.0,
"max": 18121.0,
"ci95": 2026.607
},
"fps.mountain.avg_fps": {
"n": 2,
"mean": 111.151,
"stdev": 5.637,
"min": 107.165,
"max": 115.137,
"ci95": 50.646
},
"fps.mountain.low_1pct_fps": {
"n": 2,
"mean": 25.851,
"stdev": 13.871,
"min": 16.043,
"max": 35.659,
"ci95": 124.62
},
"fps.mountain.low_01pct_fps": {
"n": 2,
"mean": 17.065,
"stdev": 8.985,
"min": 10.711,
"max": 23.418,
"ci95": 80.728
},
"memory.mountain.gpu_mb": {
"n": 2,
"mean": 602.19,
"stdev": 3.887,
"min": 599.441,
"max": 604.938,
"ci95": 34.926
},
"memory.rss_peak_mb": {
"n": 2,
"mean": 986.4,
"stdev": 4.593,
"min": 983.152,
"max": 989.648,
"ci95": 41.27
},
"memory.rss_steady_mb": {
"n": 2,
"mean": 797.12,
"stdev": 11.351,
"min": 789.094,
"max": 805.146,
"ci95": 101.983
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 3409,
"seconds": 12.002,
"avg_fps": 284.031,
"low_1pct_fps": 43.936,
"low_01pct_fps": 27.741,
"p50_ms": 2.557,
"p95_ms": 10.879,
"p99_ms": 16.323,
"p999_ms": 34.079,
"max_ms": 39.182,
"cpu_p50_ms": 2.08,
"gpu_p50_ms": 2.486,
"histogram": [
0,
549,
1776,
607,
95,
66,
35,
17,
41,
117,
46,
27,
16,
7,
5,
5,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "village",
"repeat": 1,
"frames": 3144,
"seconds": 12.002,
"avg_fps": 261.964,
"low_1pct_fps": 47.941,
"low_01pct_fps": 31.702,
"p50_ms": 2.82,
"p95_ms": 11.328,
"p99_ms": 15.64,
"p999_ms": 25.788,
"max_ms": 46.032,
"cpu_p50_ms": 2.305,
"gpu_p50_ms": 3.02,
"histogram": [
0,
109,
1797,
717,
128,
67,
34,
20,
53,
102,
59,
32,
10,
10,
5,
0,
1,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 2937,
"seconds": 12.0,
"avg_fps": 244.745,
"low_1pct_fps": 49.762,
"low_01pct_fps": 33.376,
"p50_ms": 2.77,
"p95_ms": 11.641,
"p99_ms": 16.127,
"p999_ms": 28.686,
"max_ms": 31.833,
"cpu_p50_ms": 2.211,
"gpu_p50_ms": 3.023,
"histogram": [
0,
65,
1619,
360,
323,
233,
55,
31,
57,
61,
73,
41,
9,
4,
6,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 1,
"frames": 2976,
"seconds": 12.003,
"avg_fps": 247.939,
"low_1pct_fps": 46.433,
"low_01pct_fps": 29.433,
"p50_ms": 2.712,
"p95_ms": 11.957,
"p99_ms": 16.649,
"p999_ms": 33.203,
"max_ms": 34.667,
"cpu_p50_ms": 2.118,
"gpu_p50_ms": 2.977,
"histogram": [
0,
107,
1608,
453,
236,
226,
58,
45,
48,
49,
74,
43,
16,
7,
4,
2,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 2428,
"seconds": 12.004,
"avg_fps": 202.258,
"low_1pct_fps": 36.058,
"low_01pct_fps": 25.863,
"p50_ms": 3.921,
"p95_ms": 13.515,
"p99_ms": 20.018,
"p999_ms": 37.372,
"max_ms": 39.911,
"cpu_p50_ms": 2.261,
"gpu_p50_ms": 4.35,
"histogram": [
0,
25,
850,
514,
460,
214,
51,
33,
27,
42,
115,
47,
25,
8,
13,
4,
0,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 1,
"frames": 2467,
"seconds": 11.988,
"avg_fps": 205.786,
"low_1pct_fps": 36.554,
"low_01pct_fps": 25.155,
"p50_ms": 3.432,
"p95_ms": 13.9,
"p99_ms": 21.587,
"p999_ms": 36.46,
"max_ms": 41.679,
"cpu_p50_ms": 2.325,
"gpu_p50_ms": 3.908,
"histogram": [
0,
13,
1018,
563,
304,
166,
66,
35,
36,
66,
81,
58,
31,
19,
7,
2,
2,
0,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 1383,
"seconds": 12.012,
"avg_fps": 115.137,
"low_1pct_fps": 35.659,
"low_01pct_fps": 23.418,
"p50_ms": 8.232,
"p95_ms": 16.336,
"p99_ms": 21.375,
"p999_ms": 35.291,
"max_ms": 50.114,
"cpu_p50_ms": 3.875,
"gpu_p50_ms": 8.272,
"histogram": [
0,
0,
30,
150,
102,
95,
114,
220,
297,
135,
86,
92,
40,
15,
4,
2,
0,
1,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 1,
"frames": 1286,
"seconds": 12.0,
"avg_fps": 107.165,
"low_1pct_fps": 16.043,
"low_01pct_fps": 10.711,
"p50_ms": 7.582,
"p95_ms": 20.742,
"p99_ms": 46.27,
"p999_ms": 82.86,
"max_ms": 103.861,
"cpu_p50_ms": 3.753,
"gpu_p50_ms": 7.922,
"histogram": [
0,
0,
62,
156,
96,
86,
110,
259,
188,
73,
61,
88,
42,
23,
21,
4,
6,
7,
3,
1,
0,
0,
0,
0
]
}
],
"label": "horizon-thinkpad"
}
@@ -1,371 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-22T19:14:38Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "linux",
"arch": "x86_64",
"cpu_threads": 4,
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.55)",
"cpu": "Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz",
"ram_mb": 31984,
"gpu": {
"name": "Mesa Intel(R) HD Graphics 620 (KBL GT2)",
"vendor": "Intel",
"driver": "4.6 (Core Profile) Mesa 26.1.8",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 12,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 1,
"warmup_s": 2.0,
"load_avg_1m": 5.749,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 1,
"mean": 16138.646,
"stdev": null,
"min": 16138.646,
"max": 16138.646,
"ci95": null
},
"startup.world_ms": {
"n": 1,
"mean": 68774.646,
"stdev": null,
"min": 68774.646,
"max": 68774.646,
"ci95": null
},
"chunks.village.load_ms": {
"n": 1,
"mean": 53635.946,
"stdev": null,
"min": 53635.946,
"max": 53635.946,
"ci95": null
},
"fps.village.avg_fps": {
"n": 1,
"mean": 89.712,
"stdev": null,
"min": 89.712,
"max": 89.712,
"ci95": null
},
"fps.village.low_1pct_fps": {
"n": 1,
"mean": 23.313,
"stdev": null,
"min": 23.313,
"max": 23.313,
"ci95": null
},
"fps.village.low_01pct_fps": {
"n": 1,
"mean": 17.52,
"stdev": null,
"min": 17.52,
"max": 17.52,
"ci95": null
},
"chunks.forest.load_ms": {
"n": 1,
"mean": 45938.493,
"stdev": null,
"min": 45938.493,
"max": 45938.493,
"ci95": null
},
"fps.forest.avg_fps": {
"n": 1,
"mean": 104.223,
"stdev": null,
"min": 104.223,
"max": 104.223,
"ci95": null
},
"fps.forest.low_1pct_fps": {
"n": 1,
"mean": 29.079,
"stdev": null,
"min": 29.079,
"max": 29.079,
"ci95": null
},
"fps.forest.low_01pct_fps": {
"n": 1,
"mean": 19.181,
"stdev": null,
"min": 19.181,
"max": 19.181,
"ci95": null
},
"chunks.ocean.load_ms": {
"n": 1,
"mean": 32096.642,
"stdev": null,
"min": 32096.642,
"max": 32096.642,
"ci95": null
},
"fps.ocean.avg_fps": {
"n": 1,
"mean": 103.183,
"stdev": null,
"min": 103.183,
"max": 103.183,
"ci95": null
},
"fps.ocean.low_1pct_fps": {
"n": 1,
"mean": 22.0,
"stdev": null,
"min": 22.0,
"max": 22.0,
"ci95": null
},
"fps.ocean.low_01pct_fps": {
"n": 1,
"mean": 12.275,
"stdev": null,
"min": 12.275,
"max": 12.275,
"ci95": null
},
"chunks.mountain.load_ms": {
"n": 1,
"mean": 56418.74,
"stdev": null,
"min": 56418.74,
"max": 56418.74,
"ci95": null
},
"fps.mountain.avg_fps": {
"n": 1,
"mean": 205.996,
"stdev": null,
"min": 205.996,
"max": 205.996,
"ci95": null
},
"fps.mountain.low_1pct_fps": {
"n": 1,
"mean": 41.77,
"stdev": null,
"min": 41.77,
"max": 41.77,
"ci95": null
},
"fps.mountain.low_01pct_fps": {
"n": 1,
"mean": 23.358,
"stdev": null,
"min": 23.358,
"max": 23.358,
"ci95": null
},
"memory.rss_peak_mb": {
"n": 1,
"mean": 2200.957,
"stdev": null,
"min": 2200.957,
"max": 2200.957,
"ci95": null
},
"memory.rss_steady_mb": {
"n": 1,
"mean": 2126.844,
"stdev": null,
"min": 2126.844,
"max": 2126.844,
"ci95": null
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 1077,
"seconds": 12.005,
"avg_fps": 89.712,
"low_1pct_fps": 23.313,
"low_01pct_fps": 17.52,
"p50_ms": 9.15,
"p95_ms": 23.582,
"p99_ms": 34.847,
"p999_ms": 56.312,
"max_ms": 57.842,
"histogram": [
0,
0,
0,
2,
36,
106,
149,
175,
132,
138,
92,
88,
71,
47,
28,
8,
2,
3,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 1251,
"seconds": 12.003,
"avg_fps": 104.223,
"low_1pct_fps": 29.079,
"low_01pct_fps": 19.181,
"p50_ms": 8.002,
"p95_ms": 18.85,
"p99_ms": 26.464,
"p999_ms": 50.754,
"max_ms": 53.518,
"histogram": [
0,
0,
0,
4,
20,
136,
223,
302,
197,
124,
70,
75,
53,
31,
12,
1,
1,
2,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 1239,
"seconds": 12.008,
"avg_fps": 103.183,
"low_1pct_fps": 22.0,
"low_01pct_fps": 12.275,
"p50_ms": 7.959,
"p95_ms": 18.934,
"p99_ms": 24.948,
"p999_ms": 74.736,
"max_ms": 88.202,
"histogram": [
0,
0,
0,
1,
8,
126,
335,
189,
192,
144,
73,
56,
71,
32,
6,
0,
0,
3,
3,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 2473,
"seconds": 12.005,
"avg_fps": 205.996,
"low_1pct_fps": 41.77,
"low_01pct_fps": 23.358,
"p50_ms": 3.996,
"p95_ms": 10.11,
"p99_ms": 17.299,
"p999_ms": 37.643,
"max_ms": 46.668,
"histogram": [
0,
1,
756,
577,
279,
317,
222,
107,
82,
60,
26,
18,
16,
5,
3,
2,
2,
0,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-thinkpad-r1"
}
@@ -1,371 +0,0 @@
{
"format": "horizon-benchmark-report",
"version": 1,
"kind": "game",
"created": "2026-10-22T19:20:01Z",
"app": {
"name": "minecraft",
"version": "26.3",
"mods": [
{
"name": "fabric-loader",
"version": "0.19.5"
}
]
},
"hardware": {
"os": "linux",
"arch": "x86_64",
"cpu_threads": 4,
"os_version": "NixOS 26.05 (Yarara) (kernel 6.18.55)",
"cpu": "Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz",
"ram_mb": 31984,
"gpu": {
"name": "Mesa Intel(R) HD Graphics 620 (KBL GT2)",
"vendor": "Intel",
"driver": "4.6 (Core Profile) Mesa 26.1.8",
"backend": "opengl",
"kind": ""
}
},
"conditions": {
"scenario": "perf_bench:village,forest,ocean,mountain",
"seed": "1234",
"render_distance": 12,
"simulation_distance": 12,
"vsync": false,
"offscreen": false,
"repeats": 1,
"warmup_s": 2.0,
"load_avg_1m": 5.332,
"window": [
1280,
720
]
},
"metrics": {
"startup.title_ms": {
"n": 1,
"mean": 17463.142,
"stdev": null,
"min": 17463.142,
"max": 17463.142,
"ci95": null
},
"startup.world_ms": {
"n": 1,
"mean": 67480.142,
"stdev": null,
"min": 67480.142,
"max": 67480.142,
"ci95": null
},
"chunks.village.load_ms": {
"n": 1,
"mean": 50565.877,
"stdev": null,
"min": 50565.877,
"max": 50565.877,
"ci95": null
},
"fps.village.avg_fps": {
"n": 1,
"mean": 93.221,
"stdev": null,
"min": 93.221,
"max": 93.221,
"ci95": null
},
"fps.village.low_1pct_fps": {
"n": 1,
"mean": 20.719,
"stdev": null,
"min": 20.719,
"max": 20.719,
"ci95": null
},
"fps.village.low_01pct_fps": {
"n": 1,
"mean": 12.956,
"stdev": null,
"min": 12.956,
"max": 12.956,
"ci95": null
},
"chunks.forest.load_ms": {
"n": 1,
"mean": 48496.598,
"stdev": null,
"min": 48496.598,
"max": 48496.598,
"ci95": null
},
"fps.forest.avg_fps": {
"n": 1,
"mean": 103.859,
"stdev": null,
"min": 103.859,
"max": 103.859,
"ci95": null
},
"fps.forest.low_1pct_fps": {
"n": 1,
"mean": 26.111,
"stdev": null,
"min": 26.111,
"max": 26.111,
"ci95": null
},
"fps.forest.low_01pct_fps": {
"n": 1,
"mean": 17.943,
"stdev": null,
"min": 17.943,
"max": 17.943,
"ci95": null
},
"chunks.ocean.load_ms": {
"n": 1,
"mean": 34745.051,
"stdev": null,
"min": 34745.051,
"max": 34745.051,
"ci95": null
},
"fps.ocean.avg_fps": {
"n": 1,
"mean": 103.828,
"stdev": null,
"min": 103.828,
"max": 103.828,
"ci95": null
},
"fps.ocean.low_1pct_fps": {
"n": 1,
"mean": 28.222,
"stdev": null,
"min": 28.222,
"max": 28.222,
"ci95": null
},
"fps.ocean.low_01pct_fps": {
"n": 1,
"mean": 16.856,
"stdev": null,
"min": 16.856,
"max": 16.856,
"ci95": null
},
"chunks.mountain.load_ms": {
"n": 1,
"mean": 53136.991,
"stdev": null,
"min": 53136.991,
"max": 53136.991,
"ci95": null
},
"fps.mountain.avg_fps": {
"n": 1,
"mean": 204.734,
"stdev": null,
"min": 204.734,
"max": 204.734,
"ci95": null
},
"fps.mountain.low_1pct_fps": {
"n": 1,
"mean": 43.014,
"stdev": null,
"min": 43.014,
"max": 43.014,
"ci95": null
},
"fps.mountain.low_01pct_fps": {
"n": 1,
"mean": 20.945,
"stdev": null,
"min": 20.945,
"max": 20.945,
"ci95": null
},
"memory.rss_peak_mb": {
"n": 1,
"mean": 2086.688,
"stdev": null,
"min": 2086.688,
"max": 2086.688,
"ci95": null
},
"memory.rss_steady_mb": {
"n": 1,
"mean": 2020.902,
"stdev": null,
"min": 2020.902,
"max": 2020.902,
"ci95": null
}
},
"frames": [
{
"phase": "village",
"repeat": 0,
"frames": 1119,
"seconds": 12.004,
"avg_fps": 93.221,
"low_1pct_fps": 20.719,
"low_01pct_fps": 12.956,
"p50_ms": 8.87,
"p95_ms": 20.999,
"p99_ms": 32.814,
"p999_ms": 69.794,
"max_ms": 84.579,
"histogram": [
0,
0,
0,
1,
19,
130,
125,
211,
187,
140,
89,
85,
63,
40,
18,
4,
2,
3,
2,
0,
0,
0,
0,
0
]
},
{
"phase": "forest",
"repeat": 0,
"frames": 1247,
"seconds": 12.007,
"avg_fps": 103.859,
"low_1pct_fps": 26.111,
"low_01pct_fps": 17.943,
"p50_ms": 8.071,
"p95_ms": 20.077,
"p99_ms": 27.606,
"p999_ms": 48.322,
"max_ms": 63.142,
"histogram": [
0,
0,
0,
2,
36,
171,
170,
296,
231,
125,
52,
43,
57,
41,
14,
5,
3,
1,
0,
0,
0,
0,
0,
0
]
},
{
"phase": "ocean",
"repeat": 0,
"frames": 1246,
"seconds": 12.001,
"avg_fps": 103.828,
"low_1pct_fps": 28.222,
"low_01pct_fps": 16.856,
"p50_ms": 7.949,
"p95_ms": 17.653,
"p99_ms": 24.326,
"p999_ms": 50.371,
"max_ms": 68.283,
"histogram": [
0,
0,
0,
0,
3,
72,
329,
263,
142,
179,
116,
60,
52,
20,
4,
2,
2,
1,
1,
0,
0,
0,
0,
0
]
},
{
"phase": "mountain",
"repeat": 0,
"frames": 2457,
"seconds": 12.001,
"avg_fps": 204.734,
"low_1pct_fps": 43.014,
"low_01pct_fps": 20.945,
"p50_ms": 4.356,
"p95_ms": 9.472,
"p99_ms": 14.185,
"p999_ms": 34.466,
"max_ms": 57.719,
"histogram": [
0,
2,
633,
520,
318,
417,
268,
128,
68,
60,
17,
11,
4,
4,
3,
2,
0,
2,
0,
0,
0,
0,
0,
0
]
}
],
"label": "vanilla-thinkpad-r2"
}
-126
View File
@@ -1,126 +0,0 @@
# Control Horizon with your AI agent
Your own AI agent can drive the launcher and the game: Claude Code, Codex, Cursor or any client that speaks MCP (the Model Context Protocol). It runs on your computer and uses the same commands you can type in a terminal. It can install a version, start the game, play it, change settings and manage your worlds. Before it deletes anything, it asks you. If you don't want any of this, one **No AI** switch hides and turns off every AI feature.
## What an agent can do
Everything you can do with the mouse and keyboard, through commands instead of clicks:
- **In the launcher:**
- install, remove and prepare versions;
- start the game, alone or with a world, or a server;
- your settings and the extra game arguments;
- the mod browser (mods, resource packs, modpacks), imported modpacks and hosted servers;
- your accounts (names only), logs and crash reports, patch notes, the data collection settings and the benchmark.
- **In the game:**
- read what is on screen (every button, slider, field and slot, by name), click and type;
- move, look, mine, place, attack, use items and pick from the hotbar;
- the inventory, chests and every other container, crafting, trading, books and signs;
- every options page, including Horizon Settings, and any option by its key;
- worlds (create, open, rename, back up, delete), servers, LAN and Play with Friends;
- chat and commands, screenshots, F3 data, waiting for an event and the game's log.
The full lists are in the repository: [launcher commands](https://github.com/ludovic111/horizon/blob/main/docs/launcher-commands.md) and [game commands](https://github.com/ludovic111/horizon/blob/main/docs/horizon-ctl-commands.md).
## Connect it from the launcher
The easiest way. Open the launcher's **Settings** and scroll to **AI**:
1. Under **Connect an AI agent**, pick your client: **Claude Code**, **Codex**, **Cursor** or **Any MCP client**.
2. For Claude Code and Codex, **Add to Claude Code** (or **Add to Codex**) runs that client's own command to add Horizon. The command is shown above the button, and runs only when you click it. For the others, **Copy** the config and paste it into the file named above it.
3. **Test the connection** starts Horizon's MCP server the way your client will and says how many tools it offers.
4. Restart your agent if it was open, then ask it: "Say hello from Horizon".
One server, called `horizon`, covers both the launcher and the game. Ask your agent to start the game and it starts it under its control. The game's tools are the ones whose names begin with `game_`.
## Copy-paste config
The launcher prints these with the exact path of your copy: `horizon-launcher agent-config`. The paths below are the usual ones:
- **macOS:** `/Applications/Horizon.app/Contents/MacOS/horizon-launcher`
- **Windows:** where you extracted the zip, for example `C:\Horizon\horizon-launcher.exe`
- **Linux:** where you put the AppImage or binary
**Claude Code**, in a terminal:
```sh
claude mcp add --scope user horizon -- /Applications/Horizon.app/Contents/MacOS/horizon-launcher mcp
```
**Codex**, in a terminal:
```sh
codex mcp add horizon -- /Applications/Horizon.app/Contents/MacOS/horizon-launcher mcp
```
Or in `~/.codex/config.toml`:
```toml
[mcp_servers.horizon]
command = "/Applications/Horizon.app/Contents/MacOS/horizon-launcher"
args = ["mcp"]
```
**Cursor** (`~/.cursor/mcp.json`), **Claude Desktop**, and **any other MCP client** that takes the usual JSON:
```json
{
"mcpServers": {
"horizon": {
"command": "/Applications/Horizon.app/Contents/MacOS/horizon-launcher",
"args": ["mcp"]
}
}
}
```
The server talks over its standard input and output, so it needs no port and is never on the network.
## A game you started yourself
A game your agent starts is under its control. A game you start with **Play** is not, unless you allow it. In the game, open **Options**, then **Horizon Settings**, then **AI Agent...**:
- **Agent Control: On** lets agents on this computer reach the game you started. It is off by default.
- The page shows whether an agent is connected and when it last sent a command.
- **Copy Connection Details** copies where the game listens, a command to try, and an MCP config for `horizon-ctl`, the game's own command line, which comes with the game.
Turning **Agent Control** off disconnects agents at once.
## The command line
The same commands, for scripts and terminals. `--json` prints JSON.
- See the versions: `horizon-launcher versions`
- Install the newest: `horizon-launcher install latest`
- Play a world: `horizon-launcher play --world "My world"`
- Change a setting: `horizon-launcher settings install-new-versions on`
- Find a resource pack: `horizon-launcher mods search --kind resource faithful`
- Read the game's log: `horizon-launcher logs`
- Turn No AI on: `horizon-launcher no-ai on`
- Start a game agents control: `horizon-ctl launch --world Test`
- See the game's state: `horizon-ctl state`
- See what is on screen: `horizon-ctl ui`
- Click a button by name: `horizon-ctl click singleplayer`
- Walk somewhere: `horizon-ctl goto 10 64 -3`
- Take a screenshot: `horizon-ctl screenshot shot.png`
`horizon-ctl help` lists every game command. `horizon-launcher help` lists the launcher's.
## Safety
- **Your computer only.** The launcher's server talks only to the agent that started it. The game listens on a local socket that only your user account can open; where there is none (Windows), it uses a localhost port and a secret token in a file only you can read. Nothing listens on your network.
- **It asks before deleting.** Removing a version, a modpack, a mod or a server, deleting a world or your collected data, and signing out need `confirm: true` from the agent. A call without it is refused and tells the agent to ask you first.
- **It asks before reaching out.** The tools that send something or reach other people say "only when the player asks". That covers joining a server, chatting on one, opening to LAN, hosting for friends, creating a server and sending data or a bug report. Data collection stays off until you allow it yourself.
- **No secrets.** No command returns your account tokens, session files or invite secrets. Accounts appear as names. Signing in stays a code that you enter in your own browser.
- **Your agent, your rules.** Clients such as Claude Code ask you before each tool they haven't been allowed to use. Allow only what you are happy for them to do.
## No AI: turn it all off
In the launcher's **Settings**, under **AI**, turn on **No AI**. You can also use **No AI** in the game's **Horizon Settings**, or type `horizon-launcher no-ai on`. Then:
- the **Connect an AI agent** section and the game's **AI Agent** page are hidden;
- the game opens no control socket, even when it is started with one;
- `horizon-ctl`, `horizon-ctl mcp` and `horizon-launcher mcp` refuse with "AI features are off: turn off No AI in the launcher's settings";
- the launcher's own commands (install, play, server, update and the rest) keep working, so scripts and packagers don't break.
Turn it off the same way to bring everything back.
-34
View File
@@ -1,34 +0,0 @@
# APIs at a glance
Horizon is built to be built on. Mods, shaders, agents, server panels, launchers' scripts and tools all get a documented API with a version, a stability level and a reference generated from the code, so it never says something the game doesn't do.
## What you can build on
| Surface | For | Today | Stability |
| --- | --- | --- | --- |
| **Mods** | mod authors | Rust compiled to sandboxed WebAssembly: read the player and the clock, chat, a HUD panel, storage, commands, tick and frame hooks; data-driven blocks and items. Lua scripts, run in the same sandbox, are planned | preview |
| **Shaders** | shader authors | a new shader API in WGSL is being designed: stages with typed inputs, options, reload on save, a debug overlay | planned |
| **Agent control** | AI agents, scripts, tests | `horizon-ctl` and its MCP server: 96 commands to watch and play the game | preview |
| **Servers** | hosts, panels, bots | `horizon-server`'s commands, `server.toml` and a local HTTP control API | stable |
| **Launcher** | your agent, pack makers | `horizon-launcher`'s 46 commands and its MCP server | preview |
| **Data formats** | tools that read Horizon's files | benchmark reports, telemetry, settings, release checksums; worlds are vanilla's format | per format |
The full pages, one per surface, with their references: [the API portal](/docs/api), starting with [the Mod API](/docs/api/mods). Mods for players: [Mods](/docs/mods). Agents: [Control Horizon with your AI agent](/docs/agents). Hosting: [Host a server](/docs/hosting).
## What the levels mean
- **Stable**: only additions within a major version. A change that would break you comes as a new major, served beside the old one for at least 180 days.
- **Preview**: documented and tested. It may change at a new line (0.1 to 0.2), after a deprecation of at least 30 days during which old and new both work.
- **Experimental**: opt-in only, and it may change in any release.
Every API has its own version (the mod API is `horizon:[email protected]`, the server's control API is `/api/v1`), separate from the game's release number. The whole policy: [stability and versioning](https://github.com/ludovic111/horizon/blob/main/docs/api/stability.md).
## How they are designed
Each API starts from what its users need to do, and from how Horizon is built: Rust, wgpu, WebAssembly and a server thread that runs every world. Horizon's APIs are its own: they are not modelled on another mod loader's, renderer's or server platform's. Vanilla Minecraft 26.3 is the reference for game concepts such as ids, commands and data packs.
They are safe by default (mods run in a sandbox with the permissions you allow; control sockets only listen on your computer, behind a token) and cost nothing when unused.
## References that can't drift
Each reference is written by code from the source it describes, and a test fails when the two differ: the mod API from its WIT contract, `horizon-ctl` and the launcher from their command tables, the server from its settings template and route table. A change to an API changes its reference in the same pull request.
-124
View File
@@ -1,124 +0,0 @@
# Content mods
A content mod adds blocks and items to the game with data files only: no Rust, no compiler. Each new block or item copies a vanilla one (its template) and changes what differs, and comes with its recipes, loot table, tags, model, textures and name, in the same formats as vanilla's data packs and resource packs.
## What a content mod is
A folder (while you work on it) or a `.hmod` file (to share it) holding a manifest, `assets/` and `data/`. This is the SDK's [`ruby-content`](https://github.com/ludovic111/horizon/tree/main/crates/horizon-mod-sdk/examples/ruby-content) example, a ruby gem and a block of ruby:
```
ruby/
├── horizon-mod.toml
├── data/ruby/horizon/block/ruby_block.json the block: a template and what differs
├── data/ruby/horizon/item/ruby.json the item
├── data/ruby/recipe/ruby_block.json recipes, as in a data pack
├── data/ruby/recipe/ruby_from_ruby_block.json
├── data/ruby/loot_table/blocks/ruby_block.json what the block drops
├── data/minecraft/tags/block/mineable/pickaxe.json added to vanilla's tags
├── data/minecraft/tags/block/needs_iron_tool.json
├── assets/ruby/blockstates/ruby_block.json models and textures, as in a resource pack
├── assets/ruby/models/block/ruby_block.json
├── assets/ruby/models/item/ruby.json
├── assets/ruby/items/ruby.json
├── assets/ruby/items/ruby_block.json
├── assets/ruby/textures/block/ruby_block.png
├── assets/ruby/textures/item/ruby.png
└── assets/ruby/lang/en_us.json the names players see
```
Its manifest asks for API `0.2` and the `registries` capability. It has no code, so it needs no `Cargo.toml` and no `mod.wasm`:
```toml
id = "ruby"
name = "Ruby"
version = "0.1.0"
description = "A ruby gem and a block of ruby"
authors = ["You"]
api = "0.2"
side = "both"
capabilities = ["registries"]
```
## Blocks
A block is a file `data/<id>/horizon/block/<name>.json`. It becomes the block `<id>:<name>`, with the template's behaviour, states and shapes:
```json
{
"template": "minecraft:iron_block",
"properties": { "hardness": 4.0, "light": 3, "sounds": "minecraft:amethyst_block", "map_color": 28 }
}
```
| Key | Meaning |
| --- | --- |
| `template` | The vanilla block it copies, such as `minecraft:iron_block`, `minecraft:oak_door` or `minecraft:diamond_ore`. A door copies a door's states and shape, an ore an ore's. |
| `properties.hardness` | How long it takes to mine (vanilla's destroy time; `-1` cannot be broken). |
| `properties.explosion_resistance` | How well it stands up to explosions. |
| `properties.light` | The light it gives, 0 to 15. |
| `properties.requires_tool` | `true`: it drops nothing unless mined with the right tool (the tags say which). |
| `properties.friction` | How slippery it is (ice is `0.98`). |
| `properties.map_color` | Its colour on maps, a vanilla map colour id from 0 to 61. |
| `properties.sounds` | A vanilla block whose sounds it uses. |
| `item` | `false` for a block with no item. By default the block gets an item of the same id. |
Every property you leave out is the template's.
## Items
An item is a file `data/<id>/horizon/item/<name>.json`, copying a vanilla item:
```json
{ "template": "minecraft:diamond", "rarity": "uncommon" }
```
It may change `max_stack_size` (1 to 99) and `rarity` (`common`, `uncommon`, `rare` or `epic`, the colour of its name).
## Recipes, loot, tags, models and names
Everything else uses vanilla 26.3's own formats, so the [Minecraft Wiki's](https://minecraft.wiki/w/Data_pack) pages on data packs and resource packs apply:
- **Recipes** in `data/<id>/recipe/`, **loot tables** in `data/<id>/loot_table/`.
- **Tags**: your own in `data/<id>/tags/`. To add to vanilla's (which pickaxe mines the block, for example), put a tag file in `data/minecraft/tags/`; to add to the convention tags NeoForge and Fabric mods share, in `data/c/tags/`. They add to the tag as a data pack does; `"replace": true` drops what came before it, and the log says so.
- **Block states, models, item models and textures** in `assets/<id>/`, as in a resource pack. Models can use vanilla's parents, such as `minecraft:block/cube_all`.
- **Names** in `assets/<id>/lang/en_us.json`: `"block.ruby.ruby_block": "Block of Ruby"`, `"item.ruby.ruby": "Ruby"`.
**Structures.** A content mod can also carry structures, template pools and structure sets in its namespace, and add its own pieces to vanilla's villages or to another mod's pools. The [mod design page](https://github.com/ludovic111/horizon/blob/main/docs/mods.md#content-mods-api-02) has the details.
## Namespaces
Your files go under your mod's id (`assets/ruby/`, `data/ruby/`), plus vanilla's and the convention tags (`data/minecraft/tags/`, `data/c/tags/`). Tags are the only files a mod may put under `minecraft`.
A mod made from a Java mod whose files use other names lists them in its manifest: `namespaces = ["kaisyn", "towns_and_towers"]`. A listed namespace is the mod's own, as its id is, but blocks and items are still declared under `data/<id>/horizon/`. `minecraft`, `c` and `horizon` can't be listed, and two mods that are on can't own the same namespace: the game keeps the first and logs the clash.
## Limits
- A mod's files use vanilla's resource-name characters: at most 65,536 files, 16 MiB each and 512 MiB in all.
- A file the game can't read (a block model that doesn't parse, a recipe with a typo) is logged and skipped, as vanilla does: the rest of the mod still loads. Read the log while you work.
- Unknown keys in a block or item file are refused, and the mod stays off with the reason on its row.
- Content mods work in single player today. Playing them together on a server is planned.
## Start one
```sh
cargo horizon-mod new my-blocks --template content
```
makes a content mod with one block, Glowing Amethyst: an amethyst block that gives light, with its block state, model (using vanilla's amethyst texture), item model, name, loot table, a recipe (an amethyst block and glowstone dust) and the pickaxe tag. Rename and copy its files to add your own.
## Try it and share it
While you work, give the game the folder as a dev mod ([how](/docs/make-a-mod#see-it-in-the-game)): `--dev-mod "/path/to/ruby"`. The first start with a new set of mods takes a little longer while the game adds their content to its tables; after a change to the files, start the game again.
To share it, build the `.hmod` in the mod's folder:
```sh
cargo horizon-mod build
cargo horizon-mod check target/horizon-mod/ruby.hmod
```
A folder without a `Cargo.toml` is a content mod: `build` checks its files as the game will and packs the manifest, `assets/` and `data/` into `target/horizon-mod/<id>.hmod`, with no compiler involved. `check` reads it as the registry does and says "no code: content only, nothing to run". Players put that file in their `horizon-mods` folder and turn it on in **Options, Mods...**, and you can [publish it](/docs/publish-a-mod) like any mod.
A mod can have both: a `Cargo.toml` with code, and `assets/` and `data/` with content. `build` packs both.
Next: [test it](/docs/test-a-mod).
-56
View File
@@ -1,56 +0,0 @@
# Contribute
Horizon is open source and takes pull requests. The one rule that shapes everything: **it plays exactly like vanilla.** Wherever vanilla has a feature, vanilla is the specification.
Questions and ideas are welcome on [Discord](https://discord.gg/HcTeMZxe5E). Bugs and feature requests go in [GitHub issues](https://github.com/ludovic111/horizon/issues). If you want to take something on, say so in its issue so nobody else starts the same work.
## The repository
[github.com/ludovic111/horizon](https://github.com/ludovic111/horizon) holds the game, the launcher and this website, plus the tools, tests and docs that build, check and release them.
- `crates/` is the game, as Rust crates: the client, the server, the world and protocol code, the mod system, the design tokens and more.
- `launcher/` is the launcher, a GPUI app with its own Cargo workspace.
- `website/` is this site and its documentation, in `website/docs/`.
- `tools/` holds the parity harness drivers, the parity runner, the release tools and the self-check.
- `tests/` holds the Python tests, `benchmarks/` the published results and `packaging/` the installers.
- `docs/` holds the developer documentation, [indexed here](https://github.com/ludovic111/horizon/blob/main/docs/README.md): the native client, the design system, world generation, the parity workflow, the release flow and more.
## Build it
You need Rust (stable), Python 3.10 or newer, 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.
```sh
git clone https://github.com/ludovic111/horizon.git
cd horizon
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
cargo run --release -p horizon-client
```
Everything prepared lands in the git-ignored `.local/`. It is derived from the game: never commit it or share it. The launcher is its own workspace: `cd launcher && cargo run`. The website builds with `node build.mjs` in `website/`.
## Parity harnesses
A **harness** is a `tools/test_*_parity.py` script. It drives the same inputs through Horizon and through the vanilla client and server bytecode from your own copy of the game, and checks that the outputs are identical. There are {{HARNESSES}} in the audit.
- **Read the vanilla code before writing Horizon's.** `tools/rea_vanilla.py` queries the pinned game bytecode, for example `python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`. Say in your pull request which vanilla classes and methods you followed.
- **Gameplay changes come with a harness**, new or extended, so the next change cannot silently break them. Copy a nearby one.
- `python3 tools/parity.py` runs the harnesses a change can affect. [The parity workflow](https://github.com/ludovic111/horizon/blob/main/docs/parity-workflow.md) explains the rest.
- Horizon's own features (the launcher, the new menus) follow [the design system](https://github.com/ludovic111/horizon/blob/main/docs/design-system.md) and stay 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, never for player-facing code.
## How pull requests are checked
1. **Self-check first.** Run `python3 tools/pr_ready.py` and paste the block it prints into your pull request. It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check and the harnesses your change can affect.
2. **`verify`** runs on every pull request on GitHub's Linux runners: the same checks, without the vanilla harnesses. Docs-only changes finish in seconds.
3. **`parity`** runs the affected harnesses on a maintainer's computer, which holds the game files. It never runs a fork's code on its own: a maintainer reads the change, then adds the `run-parity` label, which runs it once on your current commit.
4. **Small pull requests.** One change each, under about 500 lines of code where you can. They get reviewed and merged the same day.
5. **Labels.** `feature` when players get something new, `skip-release` when they see no change, nothing for a fix. We set them if you cannot.
6. **"Allow edits by maintainers."** Tick it. When something small is left, we push the fix ourselves instead of sending you back.
Every merge to `main` that players can see is released automatically, so `main` always stays playable. See [the release flow](https://github.com/ludovic111/horizon/blob/main/docs/release-flow.md).
## What never goes in
Never commit game files, decompiled source or prepared resources. `tools/check_public_tree.py` rejects them, and `git config core.hooksPath .githooks` runs it before every commit. Tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
[CONTRIBUTING.md](https://github.com/ludovic111/horizon/blob/main/CONTRIBUTING.md) is the short version of this page, and the [contributors](/#contributors) are listed on the home page.
-75
View File
@@ -1,75 +0,0 @@
# FAQ
Short answers, with a link when there is more to say.
## Do I need to buy Minecraft?
Yes. Horizon contains no Minecraft code, assets or data. It builds its game files from your own copy of Minecraft: Java Edition 26.3, on your computer. See [Install](/docs/install).
## Is Horizon official?
No. It is not an official Minecraft product, and it is not approved by or associated with Mojang or Microsoft. It is open source, under the MIT licence.
## Will my Forge, NeoForge or Fabric mods work?
No. Java mod jars do not run in Horizon. Horizon has its own mod system, and the launcher can carry over the data-and-assets part of some Java mods. See [Modpacks](/docs/modpacks) and [Mods and data packs](/docs/mods).
## Can I join public servers?
Not yet. Most public servers check that you own Minecraft, which needs a Microsoft account sign-in, and Horizon cannot do that yet. It can join servers in offline mode, such as your own or a friend's. See [Multiplayer](/docs/multiplayer).
## When will Microsoft sign-in work?
We cannot give a date. It needs an app registration with Microsoft and an approval from Mojang that the project does not have yet. Until then, this site only describes what works today.
## Can vanilla players join my Horizon world or server?
Yes. A vanilla 26.3 client can join a Horizon server and a Horizon LAN world. See [Multiplayer](/docs/multiplayer) and [Host a server](/docs/hosting).
## Can I use my existing worlds?
Yes. Worlds from Minecraft 1.8 to 26.2 are upgraded the way vanilla upgrades them, and saves use vanilla's format. Copy your world into the game folder's `saves` folder, and make a backup first, as you would before opening a world in any new version.
## Where are my worlds and settings?
In the game folder: `~/.local/share/horizon` on Linux, `~/Library/Application Support/horizon` on macOS and `%APPDATA%\horizon` on Windows. Updating the game never touches it.
## Does it change my normal Minecraft install?
No. Horizon only reads your Minecraft files when it prepares, and keeps what it needs in its own folder.
## Why does preparing need Java 25?
Preparation runs vanilla's own data generator on your copy of the game, to read data such as every item's default components, and that is Java. It is needed for the preparation only. The game and the server themselves run without Java.
## What does "100% vanilla parity" mean?
Every subsystem matches vanilla in every harness that compares it. It means 100% of what the harnesses compare, not proof that nothing else differs. See [What Horizon is](/docs/) and [the status table](/#parity).
## Which systems does it run on?
Linux x86-64, macOS on Apple Silicon and Windows x64. See [supported systems](/docs/#supported-systems).
## Are shader packs supported?
Not right now. They have been removed, and will come back later, written again from scratch. See [Settings](/docs/settings#shader-packs).
## Does Horizon collect data?
Only if you say yes, and each kind has its own switch. Everything is off by default. The one thing sent without asking is an anonymous "I'm playing" ping, for the count of how many play right now: it holds a random number made each time the game starts, the version, your OS and whether a world is open to LAN, and nothing about you. See [Privacy](/privacy).
## How often does it update?
Often. Every change players can see is released automatically after it is merged, so new versions can come out several times a day. Each one is tested before it goes out (the game has to start and draw a world), and the launcher installs it as soon as it is out. There is one version for everyone. See [Install](/docs/install#updating).
## What do the version numbers mean?
Since 0.2, every release has a number: 0.2.0, then 0.2.1, 0.2.2 and so on, one for each release. The game and the launcher are both on 0.2 and count on their own, so you can play Horizon 0.2.7 with launcher 0.2.3. A bigger step, such as 0.3, is announced when it comes. Releases before 0.2 were named after the day they came out (2026.10.19.15); every 0.2 release is newer than all of them.
## How do I report a bug?
See [Logs, crashes and bug reports](/docs/troubleshooting).
## How can I help?
See [Contribute](/docs/contributing). You can also [sponsor the project on GitHub](https://github.com/sponsors/ludovic111).
-162
View File
@@ -1,162 +0,0 @@
# Host a server
`horizon-server` runs a Minecraft: Java Edition 26.3 server with Horizon's own game as the engine. It is one native program: no Java, one settings file, a web panel and backups. Vanilla 26.3 clients and Horizon clients can join it.
There are two ways to run one. The launcher's **Servers** page does everything with buttons and no commands. The command line is for a VPS or a headless machine. Both run the same program and keep the same folder.
> The **Servers** page is in the launcher since version 0.15. The launcher updates itself, so open it once to get it.
## Before you start
- **You need your own copy of Minecraft 26.3**, like for playing: the server reads its game files from the ones you prepared. See [Install](/docs/install#what-you-need).
- **Horizon players cannot join servers that check accounts yet.** That needs a Microsoft sign-in, which Horizon does not have. So a server for Horizon players runs in offline mode, which means anyone who can reach it can join under any name. Keep it on your network or a VPN such as Tailscale, and use a whitelist ("Friends only" in the launcher). Vanilla players can join an offline server too.
- **A server that checks accounts is fine for vanilla players.** It works as on a vanilla server, and Horizon players cannot join it yet.
- Nothing of Mojang's is downloaded or shipped with the server.
## From the launcher: the Servers page
**Servers** is in the launcher's top navigation, between **Modpacks** and **Patch notes**. The **Play** page also has a **Host a server** button that leads there.
### Create a server
Choose **Create a server**. To use a server folder you already have, choose **Add a server folder…** instead. The dialog asks for:
- **Name**.
- **World:** **New world** (with an optional seed) or **Copy one of my worlds**.
- **Game mode:** **Survival**, **Creative** or **Adventure**.
- **Difficulty:** **Peaceful**, **Easy**, **Normal** or **Hard**.
- **Most players at once:** use **–** and **+**.
- **Who can join:** **Friends only** or **Anyone with the address**. With Friends only, type your friends' names; the first one becomes an operator.
- **Check Minecraft accounts**, off by default. Leave it off for Horizon players. Turn it on only if every player signs in with Microsoft.
- **I agree to the Minecraft EULA**, with a **Read it** link. You must tick it.
Everything has a default, so ticking the EULA and choosing **Create** makes a friends-only survival world. You can change all of it later.
![The Create a server dialog](/media/docs/servers-create.webp)
### Start it and watch it
Open the server with **Open** on its row. At the top, a line says whether it is Stopped, Starting, Running, Restarting, Stopping or Updating, with how long it has been up. Then:
- **Start** and **Stop**, and **Restart**.
- **Open the web panel** and **Open the folder**.
- Four tiles: **Players online**, **TPS** (20 is full speed), **MSPT** (how long a tick takes) and **RAM**.
- Four tabs: **Overview**, **Players**, **Settings** and **Backups**.
The **Overview** tab has the server's console, with a command line: type a command and press Enter. If the game crashes, the launcher restarts it by itself and says so ("The game crashed N times and was restarted by itself").
![A running server's page in the launcher](/media/docs/servers-dashboard.webp)
### Let your friends join
The Overview tab shows **Address on this network** with a **Copy** button. Friends on the same Wi-Fi choose Multiplayer, **Add server** and paste it.
For friends elsewhere, the page offers **Friends over the internet** through [Tailscale](https://tailscale.com), which is free, needs nothing changed on your router and reaches only the people you invite. Depending on what it finds, it shows:
- **Install Tailscale** if it is not installed, **Open Tailscale** if it is not running, or **Sign in to Tailscale** if you are signed out. Each has **Check again**.
- Once it runs, the server's Tailscale name with **Copy**. Friends type it in Multiplayer, once they have accepted your invitation.
- **Invite a friend:** open Tailscale's admin console, or **Use an access token…** so the launcher can make the invitation itself. Type their email (optional), choose **Make an invitation** and send them the link. The list shows **Withdraw** for an invitation not accepted yet and **Stop sharing** for one that was.
- **Limit them to Minecraft** changes your Tailscale access controls so invited friends reach the server and nothing else on your computer. **Remove the limit** undoes it.
Horizon never turns on Tailscale Funnel: the server is not put on the public internet. The other way is to forward TCP port 25565 on your router to the computer.
![Inviting a friend through Tailscale](/media/docs/servers-invite.webp)
### Players
The **Players** tab lists who is **Online now**, with **Make operator** (or **Remove operator**), **Kick** and **Ban**. Under **Who may join**, the **Friends only** switch turns the whitelist on. Type a player's name and choose **Let in** to add them.
### Settings
The **Settings** tab has the message in the server list, game mode, difficulty, PvP, most players at once, view distance, simulation distance, port, **Check Minecraft accounts** and **Run in the background and at login**. Each says whether it applies at once or after a restart, and the page then offers **Restart now**. A value the server would refuse shows in red under its field.
When something is wrong the launcher says so in plain words. If it can fix it, there is a button: **Use port 25566** when the port is busy, **Prepare the game files** when they are missing.
### Backups and updates
The **Backups** tab has **Back up now**, and **Back up automatically** (every 6 hours, keeping the last 12; backups you make by hand are never deleted). **Restore** next to a backup asks you to confirm with **Replace the world**, and keeps the world it replaces.
Under **This server's Horizon**, **Check for updates** and **Update now** move the server to the newest release. The world is backed up first, and the server goes back to the old release by itself if the new one does not start.
![A server's Backups tab](/media/docs/servers-backups.webp)
## From the command line
This works today, on a VPS or any computer. Everything below is the same program the launcher runs.
### 1. Install
On Linux (x86-64) and macOS (Apple Silicon):
```sh
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
```
On Windows (x64), in PowerShell:
```powershell
irm https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.ps1 | iex
```
The script downloads the latest release from GitHub, checks it against the SHA-256 GitHub publishes, and never asks for sudo or administrator rights. It puts `horizon-server` and `horizon-prepare` in `~/.local/bin` (on Windows, in your PATH). Other systems, such as Linux on ARM, build from source.
### 2. Prepare the game files
You need Minecraft 26.3 played once, Java 25 (a JDK) and the 26.3 `server.jar`, which is the `downloads.server.url` in your launcher's `versions/26.3/26.3.json`. Then:
```sh
horizon-prepare --prepare --server-jar /path/to/server.jar
```
**On a machine with no Minecraft**, prepare on your own computer (with the launcher or `horizon-prepare`) and copy the prepared `26.3` folder to the server, into `~/.local/share/horizon/game-files/horizon-client/26.3`.
### 3. First run
```sh
mkdir my-server && cd my-server
horizon-server init
horizon-server start
horizon-server status
```
`init` asks whether you accept the [Minecraft EULA](https://aka.ms/MinecraftEULA), the world's name and seed, the port, whether to use a whitelist and who to add, and whether to make the first player an operator. It writes `server.toml`. Every question has a flag, so it also runs from a script.
Players join at your computer's address and the port, 25565 by default. `horizon-server run` runs in the foreground with the console on your terminal instead.
### 4. The everyday commands
```text
horizon-server run in the foreground, with the console
horizon-server start | stop | restart in the background
horizon-server status up or not, TPS, MSPT, players, RAM
horizon-server cmd COMMAND... one console command, and its answer
horizon-server logs [-f] [-n N] the log; -f follows it
horizon-server op NAME | deop NAME
horizon-server whitelist add|remove NAME
horizon-server backup | backups | restore BACKUP
horizon-server panel a one-time login link for the web panel
horizon-server service install start it with the computer
horizon-server update [--check] install the newest release and restart on it
horizon-server rollback back to the release before
```
Settings are in `server.toml`: the message of the day, port, most players, online mode, whitelist, view distance, game mode, difficulty, backups and the panel. Changes apply at the next restart. A vanilla server folder works as it is: run `horizon-server init` in it, and the world, `ops.json` and the whitelist keep vanilla's names and formats.
The server restarts the game by itself if it crashes. **Updates** never happen on their own: it tells you a new release is out, and `horizon-server update` backs up the world, switches, and goes back by itself if the new game does not start.
### The web panel
`horizon-server panel` prints a login link that works once, for ten minutes. The panel shows live TPS, MSPT, RAM and CPU for the last ten minutes, the players (with a kick button), the console, the backups, and Restart and Stop. It listens on this computer only. To reach it from elsewhere, put your Tailscale address in `[panel] bind`, or forward the port over SSH. Never put it on the open internet.
### What does not work yet
- Horizon players cannot join a server that checks accounts (see above).
- No Docker image is published. You can build one from the repository.
- Settings in `server.toml` apply at the next restart, not live.
- `console` (attach to a server running in the background) and `ban` and `pardon` as their own commands are coming. In the meantime, `horizon-server cmd ban NAME` works.
The full reference, with every option, is [docs/hosting.md](https://github.com/ludovic111/horizon/blob/main/docs/hosting.md) in the repository, and [docs/server.md](https://github.com/ludovic111/horizon/blob/main/docs/server.md) explains the design.
## How fast is it
On the latest bot benchmark, with up to 50 players, Horizon used 3.8 to 4.8 times less memory than vanilla, Paper and Fabric with Lithium, started in about 8 seconds against 15 to 22, and used the least CPU from 10 players up. Its average tick was longer than Paper's at low player counts. See [Performance](/docs/performance#servers) for the numbers and what they mean.
-39
View File
@@ -1,39 +0,0 @@
# How Horizon is built
Who writes Horizon, how changes are checked, and why it is safe to run. You can check every claim on this page in the repository, and anything not done yet is marked as not done.
## Who builds it
Horizon is Ludovic Marie's project: he decides what it does and what ships. **Most of the code is written by AI agents**: Claude sessions that Ludovic runs, and contributors' own AI tools. Some contributors write their code by hand.
Agents say what they are. A pull request opened by one of Ludovic's agents starts with "Opened by Ludovic's AI agent (Claude), not by Ludovic himself.", its comments start with "🤖 Agent:", and its commits name the agent in a `Co-Authored-By` line.
## How changes are checked
Not every line is read by a person. Instead, every change is checked against the real game, automatically:
- **Harnesses.** A harness is a test that gives the same inputs to Horizon and to the real Minecraft 26.3, then compares the results. There are 326 of them (25 October 2026), from world generation to mobs, redstone and commands. The [status table](/#parity) shows what they cover. Passing them all does not prove that nothing else differs.
- **Checks on every pull request.** `verify` runs on GitHub's machines for every pull request and every change to main: formatting, lints with warnings treated as errors, the tests, and the check that no Minecraft files are committed. Main takes no change without it. `parity` runs the harnesses a change can affect, on a machine that holds a copy of Minecraft. For a pull request from a fork, it runs once a maintainer has read the change.
- **Outside contributions are checked again.** Before a contributor's pull request merges, Ludovic's agents compare what it claims with vanilla's own code, review it line by line, rewrite the weak parts, and test it in single player, on a LAN and on a dedicated server where those apply.
- **Releases are tested first.** A release is published only after its smoke test passes (the game starts and draws a world) and while no issue is labelled `release-blocker`.
## Why it's safe to run
- **Open source.** All of the code is public on [GitHub](https://github.com/ludovic111/horizon), under the MIT licence.
- **Built from the public code.** Releases are built by GitHub Actions, the build service of GitHub, from the public source. Linux and Windows are built on GitHub's machines, and macOS on Ludovic's Mac mini, which runs GitHub Actions jobs. Every build log is [public](https://github.com/ludovic111/horizon/actions/workflows/release.yml).
- **Signed checksums.** Every release has a `SHA256SUMS` file, a fingerprint of each download, and `SHA256SUMS.sig`, a signature of that list made with Horizon's release key, which only release builds from main can use. The launcher and `horizon-server update` check both before they install anything, and refuse a file that does not match. The very first server install with `install.sh` checks the fingerprint but not the signature.
- **Not yet signed by Apple or Microsoft.** The macOS app is not notarized by Apple, because it is not signed with a paid Apple developer account, so macOS asks you to confirm the first time ([Install](/docs/install#download-the-launcher) shows how). The Windows programs are not code-signed either, so Windows may warn you too.
- **Mods run in a sandbox.** Horizon mods are WebAssembly programs with no access to your files, the network or other programs. A mod lists what it needs, you see that list before you turn it on, and it can do nothing else. See [Mods and data packs](/docs/mods).
- **Nothing collected unless you say yes.** Data collection is off until you turn it on, with four separate switches. The one exception is an anonymous "I'm playing" ping for the launcher's player count, which holds nothing about you. See [Privacy](/privacy).
## What it doesn't contain
- **No Minecraft files.** Horizon contains no Minecraft code, assets or data, and never downloads them. The launcher builds the game files on your computer from your own copy of Minecraft: Java Edition 26.3. A check that runs on every pull request (`tools/check_public_tree.py`) keeps game files and decompiled code out of the repository and its history.
- **No code from other mods.** Since 24 October 2026, Horizon's code may only be written from Minecraft 26.3 itself and public documentation, never from another mod, loader or renderer ([the rule](https://github.com/ludovic111/horizon/blob/main/CONTRIBUTING.md#only-original-code)). That day shader packs were removed ([#1561](https://github.com/ludovic111/horizon/pull/1561)), after a developer of Iris showed that our shader pack code had been written from Iris's source. They are being written again from a description of the pack format only.
## Check it yourself
- **Read the code and its checks.** [The source](https://github.com/ludovic111/horizon), [the checks on every pull request](https://github.com/ludovic111/horizon/actions/workflows/ci.yml) and [the release builds](https://github.com/ludovic111/horizon/actions/workflows/release.yml).
- **Compare the fingerprints.** Put a release's files and its `SHA256SUMS` in one folder and run `sha256sum -c SHA256SUMS --ignore-missing` (on macOS `shasum -a 256 -c SHA256SUMS --ignore-missing`, on Windows `Get-FileHash <file>` in PowerShell). Each file should say `OK`.
- **Check the signature.** Download a whole release into a folder, then from a clone of the repository run `cargo run -p horizon-release-sign -- verify --tag <tag> <folder>`. The public key is in `crates/horizon-release-sign/release-keys.txt`.
- **Build it yourself.** The README explains how to [build from source](https://github.com/ludovic111/horizon#build-from-source).
-34
View File
@@ -1,34 +0,0 @@
# What Horizon is
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust. You install it with a small launcher, and it runs on the game files from your own copy of Minecraft. It is free, open source and unofficial.
## In one minute
- **A new game program, not a mod.** Horizon is written from scratch in Rust. It reads the same worlds, plays by the same rules and generates the same terrain as the Java game.
- **Checked against vanilla.** {{HARNESSES}} automated tests ("harnesses") feed the same inputs to Horizon and to the real game, then compare the results. Today every subsystem matches in every harness that compares it. That is {{PERCENT}} of what the harnesses compare, which is not proof that nothing else differs. The [status table](/#parity) lists what is covered.
- **Light and fast.** It starts quickly, uses little memory and draws efficiently. See [Performance](/docs/performance) for what is measured and what is not.
- **A few things vanilla doesn't have.** Its own settings, added to vanilla's menus the way mods add theirs, graphics presets, distant terrain, controller support, optional extras such as dynamic lights, official mods such as zoom and a minimap, an invite code to play your world with friends, and a server you can run without Java.
## What Horizon is not
- **Not Minecraft itself.** Horizon contains no Minecraft code, assets or data, and it never downloads them. You need your own legally obtained copy of Minecraft: Java Edition 26.3. Horizon builds its game files from that copy, on your computer.
- **Not a place where Java mods run.** Forge, NeoForge, Fabric and Quilt mod jars do **not** work in Horizon, and neither do Mixins. Horizon has its own mod system, and the launcher can port some Java mods' content. See [Mods and data packs](/docs/mods) and [Modpacks](/docs/modpacks).
- **Not for other versions.** Horizon plays Java Edition 26.3. Worlds from older versions (1.8 to 26.2) are upgraded the way vanilla upgrades them.
- **Not an official Minecraft product.** It is not approved by or associated with Mojang or Microsoft.
- **Not able to sign in to a Microsoft account yet.** Until it can, Horizon cannot join servers that check accounts. See [Multiplayer](/docs/multiplayer).
## Supported systems
- **Linux, x86-64.** A graphics card with Vulkan drivers. Builds are made on Ubuntu 22.04, so use a distribution at least that recent.
- **macOS, Apple Silicon.** Any M1 or newer, on macOS 11 or later.
- **Windows, x64.** Vulkan or DirectX 12. Horizon picks DirectX 12 by itself when Vulkan does not work.
There is no build for Intel Macs, 32-bit systems, or ARM Linux and Windows. The server can be built from source on other systems.
## Where to go next
- **Playing:** [install and first launch](/docs/install), [settings worth knowing](/docs/settings), [multiplayer](/docs/multiplayer) and [what to do when something breaks](/docs/troubleshooting).
- **Hosting:** [host a server](/docs/hosting), from the launcher or the command line.
- **Going further:** [modpacks](/docs/modpacks), [mods and data packs](/docs/mods) and [performance](/docs/performance).
- **Making mods:** [make a mod](/docs/make-a-mod), from the first line of code to the registry, and the [Mod API](/docs/api/mods).
- **Helping out:** [contribute](/docs/contributing) and the [FAQ](/docs/faq).
-52
View File
@@ -1,52 +0,0 @@
# Install and first launch
Download the launcher, let it install Horizon, then prepare the game from your own copy of Minecraft. It takes a few minutes, and you do the preparation once.
## What you need
- **Minecraft: Java Edition 26.3, played once.** Install it with the official Minecraft launcher (or Prism Launcher, MultiMC, the Modrinth App, ATLauncher or GDLauncher) and start 26.3 once, so it downloads its files. Horizon reads them from there and never copies them anywhere else.
- **Java 25, a JDK**, for the one-time preparation only (the game itself needs no Java). Nothing to install: the launcher uses the Java the Minecraft launcher downloaded for 26.3, or downloads that same Java from Mojang. A JDK you installed yourself (for example [Eclipse Temurin 25](https://adoptium.net/temurin/releases/?version=25)) works too.
- **A computer on the [supported list](/docs/#supported-systems).** On Linux and Windows that means a graphics card with Vulkan (or DirectX 12 on Windows).
## Download the launcher
Get the newest launcher from the [Download button on the home page](/), which picks the right file for your system, or from [the launcher releases on GitHub](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true).
- **Linux:** download `horizon-launcher-linux-x86_64.tar.gz`, extract it and run `horizon-launcher`.
- **macOS:** download `horizon-launcher-macos-arm64.dmg`, open it and drag Horizon onto Applications. The first time, macOS says it cannot check the app, because it is not signed with a paid Apple developer account. On macOS 14 and earlier, right-click Horizon, choose **Open**, then **Open** again. On macOS 15 and later, open it once, then go to **System Settings, Privacy & Security** and choose **Open Anyway**. After that it opens normally.
- **Windows:** download `horizon-launcher-windows-x64.zip`, extract it and run `horizon-launcher.exe`.
Every release also carries `SHA256SUMS` and a signature, `SHA256SUMS.sig`, if you want to check your download. The launcher does not check that signature for you yet.
## First launch
1. Open the launcher. On the **Play** page, if it says "No version installed yet", choose **Choose a version**. On the **Versions** page, install the newest one. The launcher installs it in its own folder.
2. Back on **Play**, the launcher says "Not prepared. Horizon needs the files of your own Minecraft 26.3." Choose **Prepare**. It finds your Minecraft folder by itself in the usual places. If it cannot, it lists where it looked, and **Choose…** next to the Minecraft folder lets you point it at yours.
3. Wait a few minutes. When it says "Prepared for Minecraft 26.3", choose **Play**.
The game opens on vanilla's title screen. Your worlds, options and logs live in the game folder, which is not inside a version's folder, so updating or removing a version never touches them:
- Linux: `~/.local/share/horizon`
- macOS: `~/Library/Application Support/horizon`
- Windows: `%APPDATA%\horizon`
> **No Microsoft sign-in yet.** The launcher's Settings page has an Accounts section, but signing in with a Microsoft account does not work for players today. It needs an app registration and an approval from Mojang that the project does not have yet. You do not need an account to play single player.
## Updating
- **Horizon.** The launcher looks for a new release when it starts and every 30 minutes while it is open. With **Install new versions automatically** on (the default), it installs one and moves you to it if you were on the newest. New releases come out many times a day, and old versions stay installed until you remove them on the **Versions** page.
- **The launcher.** It updates itself from its own releases, then shows **Restart** in its footer. A launcher from before 0.2 (0.16 or older) updates twice in a row: first to 0.17, then to 0.2. Then it installs Horizon 0.2 like any update.
## One version for everyone
There is one channel: every release is the version everyone gets. Before a release goes out, the game has to start and draw a world on our test machine, and nothing goes out while a serious problem is open. New releases come out many times a day, and you can install any older version by hand on the **Versions** page.
Launchers from before this change still show an **Updates** choice in Settings. Either option now gets every new release, and the choice goes away once the launcher has updated itself.
## The Versions page
It lists Horizon's releases, newest first. You can install a version, remove it, or choose which one **Play** starts. Each release's notes are on the **Patch notes** page, and on [GitHub](https://github.com/ludovic111/horizon/releases).
## Something went wrong
See [Logs, crashes and bug reports](/docs/troubleshooting).
-88
View File
@@ -1,88 +0,0 @@
# Make a mod
This guide takes you from nothing to a mod that other players can install from [playhorizon.dev/mods](/mods). You write the mod in Rust. It is compiled to WebAssembly and runs in a sandbox, so a player can see what it may do before turning it on. You need no Minecraft modding experience.
## The path
1. **Set up** (this page): install Rust and `cargo horizon-mod`, make your first mod and see it in the game.
2. **[The manifest and capabilities](/docs/mod-manifest)**: the file that names your mod and lists what it may do.
3. **[Write the code](/docs/mod-code)**: the hooks the game calls, what your code can call, and a worked example.
4. **[Content mods](/docs/content-mods)**: blocks, items, recipes and textures from data files, with no code at all.
5. **[Test it](/docs/test-a-mod)**: unit tests, the game's dev mode and the registry's own checks.
6. **[Publish it](/docs/publish-a-mod)**: package, sign and submit it to the registry.
The [Mod API](/docs/api/mods) lists everything a mod can do today, and its [reference](/docs/api/mods-reference) has every function and type.
## What a mod can do today
- **Client mods** run in the player's game. They read what the F3 screen shows (position, facing, health, food, dimension, the time), draw a text panel on the screen, show chat lines only that player sees, add chat commands, and keep small files of their own. They work in single player and on any server, and a server cannot see them.
- **Content mods** add blocks and items copied from vanilla templates, with their recipes, loot tables, tags, models, textures and names. They need no code.
- **Server mods** (rules, world events, server commands) are being built. The [Mod API](/docs/api/mods#not-there-yet) says what comes next.
A Horizon mod is not a Java mod. Fabric, NeoForge and Forge jars don't run in Horizon, and Horizon mods don't run in Java Minecraft.
## Install the tools
You need [Rust](https://rustup.rs) (the stable toolchain), its WebAssembly target and `cargo horizon-mod`, the tool that makes, builds and checks mods. On Windows, macOS or Linux:
```sh
rustup target add wasm32-unknown-unknown
cargo install --git https://github.com/ludovic111/horizon cargo-horizon-mod
```
The second command builds the tool from source, which takes a few minutes the first time. Run it again later to update the tool.
You also need Horizon itself, installed with the launcher ([Install](/docs/install)), and prepared at least once so the game has its files.
## Make your first mod
```sh
cargo horizon-mod new my-mod
cd my-mod
```
`new` makes a folder with a working mod in it: a `/hello` command that answers in chat. `--name "My Mod"` sets the name players see, and `--template content` makes a [content mod](/docs/content-mods) instead, with no code. The folder holds:
```
my-mod/
├── Cargo.toml the Rust crate (a cdylib depending on horizon-mod-sdk)
├── horizon-mod.toml the manifest: id, name, version, what the mod may do
├── .gitignore
└── src/lib.rs the code
```
Build it:
```sh
cargo horizon-mod build
```
This compiles the crate, checks that the code uses nothing its manifest doesn't declare, and writes `target/horizon-mod/my_mod.hmod`. A `.hmod` is a zip file holding the manifest and the compiled code (`mod.wasm`). It is the one file you give players: the same file runs on Windows, macOS and Linux.
## See it in the game
A **dev mod** is a mod the game loads from a path you give it. It is always on, and the game looks at its file once a second: build again and the game reloads it, saying "Mods reloaded" in chat. Its memory starts over; what it saved to its storage stays.
**From the launcher.** Open the launcher's **Settings** and put this in **Game arguments**, with the full path to your file:
```
--dev-mod "/path/to/my-mod/target/horizon-mod/my_mod.hmod"
```
Then press **Play**, open a world and type `/hello world` in chat. Leave the setting there while you work on the mod, and clear it when you are done. The launcher's command line does the same: `horizon-launcher play --arg --dev-mod --arg /path/to/my_mod.hmod`.
**With `cargo horizon-mod run`.** If you have a `horizon-client` you built from source, `cargo horizon-mod run` builds the mod and starts that game with it. It looks for `horizon-client` on your `PATH`, or takes `--game <path>` or the `HORIZON_CLIENT` variable; anything after `--` goes to the game:
```sh
cargo horizon-mod run --game ../horizon/target/fast/horizon-client -- --resources ../horizon/.local/horizon-client/26.3
```
Either way, edit `src/lib.rs`, run `cargo horizon-mod build`, and the game picks up the new file.
## Where things go
- **Your mod's messages** go to the game log as `[mod my_mod] …` (the `logs/latest.log` file in the game folder), and the game prints them in its terminal too.
- **Mods players install** go in the `horizon-mods` folder of the game folder (Linux `~/.local/share/horizon`, macOS `~/Library/Application Support/horizon`, Windows `%APPDATA%\horizon`). **Options, Mods...** in the game lists them, each with what it may do and a switch. They start off.
- **A mod that fails** to load, or that breaks while running, is switched off with the reason in red on its row of the Mods screen. It never takes the game down with it.
Next: [the manifest and capabilities](/docs/mod-manifest).
-190
View File
@@ -1,190 +0,0 @@
# Write the code
A mod is a Rust type that implements the `Mod` trait from `horizon-mod-sdk`. The game calls its methods, the hooks, at fixed points, and the mod calls back into the game through the parts of the API its capabilities allow. This page explains both, then builds a complete mod step by step.
## The shape of a mod
```rust
use horizon_mod_sdk::{Mod, Registration, chat};
struct Hello {
greeted: u32,
}
impl Mod for Hello {
fn init() -> Result<(Self, Registration), String> {
Ok((Hello { greeted: 0 }, Registration::new().command("hello")))
}
fn command(&mut self, _name: &str, args: &str) -> Result<Option<String>, String> {
self.greeted += 1;
chat::show(&format!("Hello {args}! ({} so far)", self.greeted));
Ok(None)
}
}
horizon_mod_sdk::export_mod!(Hello);
```
- **The state lives in your type.** `init` creates it, and every hook gets `&mut self`.
- **`export_mod!`** connects the type to the game. A mod has exactly one.
- **The game calls only the hooks you ask for.** `init` returns a `Registration` that lists them. The other methods keep their default, empty bodies, and a hook you didn't ask for costs nothing.
## Hooks
| Hook | Ask for it with | When the game calls it |
| --- | --- | --- |
| `init() -> Result<(Self, Registration), String>` | always called | Once, when the mod loads. An `Err` keeps the mod off and shows the message on its row. |
| `tick(&mut self, tick: u64)` | `.on_tick()` | Every game tick while the player is in a world: 20 times a second. |
| `frame(&mut self, seconds: f32)` | `.on_frame()` | Every frame the game draws, with the time since the last one. |
| `command(&mut self, name: &str, args: &str)` | `.command("name")` | When the player types `/name …` in chat. `args` is everything after the name. |
| `chat(&mut self, text: &str)` | `.on_chat()` | For each chat line the player receives, as plain text. |
**Commands.** Ask for as many as you like with `.command("…")`, without the slash. The chat box colours them and completes them with Tab, like vanilla's commands. Vanilla's names win: a mod can't take `/give`, and when two mods ask for the same name, the first one loaded keeps it. The game logs both cases. The return value says what happens next:
- `Ok(None)`: nothing more. Use it when the mod already answered with `chat::show`.
- `Ok(Some(text))`: the game shows `text` to the player.
- `Err(text)`: the game shows `text` in red, as a failed command.
**Prefer ticks to frames.** A tick happens 20 times a second whatever the frame rate. The HUD panel is kept by the game and drawn every frame for you, so most mods never need `frame`.
## What your code can call
Each module of the SDK is one part of the API, and each needs its capability in the manifest ([the list](/docs/mod-manifest#capabilities)):
| Module | Capability | Calls |
| --- | --- | --- |
| `game` | `game-read` | `get_player() -> Option<Player>` (`x`, `y`, `z`, `yaw`, `pitch`, `health`, `food`, `dimension`), `get_clock() -> Option<Clock>` (`game_time`, `day_time`). `None` outside a world. |
| `chat` | `chat` | `show(text)` |
| `hud` | `hud` | `set_panel(Corner, &[Line])` (`Line { text, color }`, colour as `0xRRGGBB`; corners `TopLeft`, `TopRight`, `BottomLeft`, `BottomRight`), `clear()` |
| `storage` | `storage` | `get(key) -> Option<Vec<u8>>`, `set(key, &[u8]) -> Result<(), String>`, `delete(key)` |
| `log` | none | `horizon_mod_sdk::info(text)` and `warn(text)`: lines in the game log |
What the game reads for you (`game`) is a snapshot taken once a tick, and again before a command runs. It never changes in the middle of a call. Everything else is plain Rust: your own types, maths and text, and the crates you add to `Cargo.toml`, as long as they build for `wasm32-unknown-unknown` without the operating system (no files, threads, network or clock).
## A worked example: Playtime
This mod counts how long you play, shows it in the top-right corner, and keeps the count when you quit. `/playtime` says it in chat; `/playtime hide`, `show` and `reset` do what they say. It uses three capabilities (`hud`, `chat`, `storage`) and two hooks (`tick`, `command`).
**1. Make the mod.**
```sh
cargo horizon-mod new playtime --name "Playtime"
cd playtime
```
**2. Declare what it may do.** In `horizon-mod.toml`, set the description and the capabilities:
```toml
description = "How long you have played, in a corner of the screen"
capabilities = ["hud", "chat", "storage"]
```
**3. The state and a helper.** Replace `src/lib.rs` with the code below, in parts. First the imports, the state and a function that turns ticks into hours and minutes. It touches no API, so an ordinary unit test can check it (step 7).
```rust
use horizon_mod_sdk::{
Mod, Registration, chat,
hud::{self, Corner, Line},
storage,
};
/// Ticks played, kept across sessions in the mod's storage.
struct Playtime {
ticks: u64,
shown: bool,
}
/// `1h 05m` from a number of ticks (20 a second).
fn format(ticks: u64) -> String {
let minutes = ticks / 20 / 60;
format!("{}h {:02}m", minutes / 60, minutes % 60)
}
```
**4. Saving and drawing.** `storage::set` writes bytes under a key of your choice; it can fail when the mod is over its quota, so the error goes to the log. The panel is set once and the game draws it until it changes, so redraw only when there is something new to show.
```rust
impl Playtime {
fn save(&self) {
if let Err(e) = storage::set("ticks", &self.ticks.to_le_bytes()) {
horizon_mod_sdk::warn(&format!("could not save: {e}"));
}
}
fn draw(&self) {
if self.shown {
let line = Line { text: format!("Played {}", format(self.ticks)), color: 0xFFFF55 };
hud::set_panel(Corner::TopRight, &[line]);
} else {
hud::clear();
}
}
}
```
**5. The hooks.** `init` reads what an earlier session saved, draws the panel and asks for one command and the tick hook. `tick` counts, and once a minute redraws and saves. `command` reads its arguments.
```rust
impl Mod for Playtime {
fn init() -> Result<(Self, Registration), String> {
// What an earlier session saved, or 0 the first time.
let ticks = storage::get("ticks")
.and_then(|bytes| bytes.try_into().ok())
.map_or(0, u64::from_le_bytes);
let state = Playtime { ticks, shown: true };
state.draw();
Ok((state, Registration::new().command("playtime").on_tick()))
}
fn tick(&mut self, _tick: u64) {
self.ticks += 1;
// Once a minute: redraw the panel and save.
if self.ticks % (20 * 60) == 0 {
self.draw();
self.save();
}
}
fn command(&mut self, _name: &str, args: &str) -> Result<Option<String>, String> {
match args.trim() {
"" => chat::show(&format!("You have played {}", format(self.ticks))),
"hide" | "show" => {
self.shown = args.trim() == "show";
self.draw();
}
"reset" => {
self.ticks = 0;
self.save();
self.draw();
return Ok(Some("Playtime reset".into()));
}
other => return Err(format!("Unknown option {other}: use hide, show or reset")),
}
Ok(None)
}
}
horizon_mod_sdk::export_mod!(Playtime);
```
**6. Try it.** `cargo horizon-mod build`, then start the game with it as a dev mod ([how](/docs/make-a-mod#see-it-in-the-game)). Open a world: "Played 0h 00m" sits in the top-right corner and goes up each minute. Type `/playtime`, then `/playtime hide`. `/playtime foo` answers in red. Quit and come back: the count goes on from where it was.
**7. Test the helper.** At the end of `src/lib.rs`:
```rust
#[cfg(test)]
mod tests {
#[test]
fn formats_hours_and_minutes() {
assert_eq!(super::format(0), "0h 00m");
assert_eq!(super::format(20 * 60 * 65), "1h 05m");
}
}
```
`cargo test` runs it on your computer, with no game. [Test it](/docs/test-a-mod) covers the rest.
**Where to go from here.** The SDK's examples are complete mods to read: [`coords-hud`](https://github.com/ludovic111/horizon/tree/main/crates/horizon-mod-sdk/examples/coords-hud) (coordinates, facing and the time on the HUD, `/where` and `/coords`) and [`ruby-content`](https://github.com/ludovic111/horizon/tree/main/crates/horizon-mod-sdk/examples/ruby-content) (a content mod). The [reference](/docs/api/mods-reference) lists every type and function, with its limits.
Next: [content mods](/docs/content-mods), or straight to [testing](/docs/test-a-mod) if your mod has no blocks or items.
-60
View File
@@ -1,60 +0,0 @@
# The manifest and capabilities
Every mod has a `horizon-mod.toml` at its root. It names the mod, says which version of the mod API it was made for, and lists its capabilities: what it may do. Players read that list before they turn the mod on, and the game holds the mod to it.
## The manifest
```toml
id = "playtime"
name = "Playtime"
version = "0.1.0"
description = "How long you have played, in a corner of the screen"
authors = ["You"]
api = "0.1"
side = "client"
capabilities = ["hud", "chat", "storage"]
```
| Key | What it is |
| --- | --- |
| `id` | The mod's id: 1 to 64 characters of `a-z`, `0-9` and `_`. It names the mod's setting, its log lines, its storage folder and, for content, its namespace. Keep it once you have published the mod. |
| `name` | The name players see. |
| `version` | Your mod's version, such as `0.1.0`. Each version you publish needs a new one. |
| `description` | One or two sentences for the Mods screen. |
| `authors` | A list of names. |
| `api` | The mod API line the mod was made for: `"0.1"` for code, `"0.2"` for content. The game refuses a line it doesn't serve, with a message that says so. |
| `side` | `"client"` for a mod that runs in the player's game. Content mods use `"both"`. |
| `capabilities` | What the mod may do: the list below. |
| `namespaces` | Content mods only: extra namespaces the mod's files use (see [Content mods](/docs/content-mods#namespaces)). |
Unknown keys are refused, so a typo is an error, not a permission that silently does nothing.
## Capabilities
A capability is a part of the API the mod may use. Each one shows on the Mods screen as a sentence that starts with "May". Writing to the game log needs no capability.
| Capability | The player reads | What the code can do |
| --- | --- | --- |
| `game-read` | May see your position, health and the time | `game::get_player()`: position, facing (yaw and pitch), health, food and dimension. `game::get_clock()`: the game time and the time of day. |
| `chat` | May show you chat messages | `chat::show(text)`: a chat line only this player sees, after the mod's name. At most 8 per call into the mod. |
| `hud` | May draw text on your screen | `hud::set_panel(corner, lines)` and `hud::clear()`: a panel of up to 16 coloured lines of 256 characters in one corner. The game keeps drawing it until you change it. |
| `storage` | May save its own settings to disk | `storage::get`, `set` and `delete`: small files in a folder of the mod's own. Up to 64 files and 4 MiB in all, and at most 1 MiB written per call. Keys are 1 to 64 characters of `a-z`, `0-9`, `_`, `.` and `-`. |
| `registries` | May add blocks and items to your worlds | Content mods: blocks and items from the mod's data files. No code. |
Ask only for what the mod uses: players are more likely to turn on a mod that asks for little.
**The game checks.** When you build, and again when a game loads the mod, the compiled code's imports are compared with the manifest. A mod whose code calls `chat::show` without `chat` in its capabilities is refused (`uses capability chat, missing from horizon-mod.toml`). The list players read is what the code can do, not a promise.
## What no mod can do
A mod runs in a WebAssembly sandbox. Whatever its capabilities:
- It cannot open files (apart from its own storage), the network, other programs or the clock.
- It sees the game only through the snapshot the game gives it once a tick, and changes it only through the calls above. It cannot patch the game's code.
- It has 64 MiB of memory and a time budget for each call. A mod that runs past its budget, runs out of memory or crashes is switched off, with the reason on its row of the Mods screen. The game goes on.
## API versions
The mod API is versioned apart from the game. A mod made for API `0.1` keeps loading on every game that serves `0.1`, and new functions arrive as additions. A change that would break mods comes as a new line, served beside the old one for at least one release cycle. The [stability page](/docs/api/stability) has the whole policy, and the [Mod API's changelog](/docs/api/mods#changelog) what each line added.
Next: [write the code](/docs/mod-code).
-52
View File
@@ -1,52 +0,0 @@
# Modpacks
Java mod jars do not run in Horizon. Instead, the launcher reads a modpack, tells you which of its mods Horizon can carry over, and brings over the ones it can. This page explains what that means today and where the limits are.
> **Early.** The Modpacks page works, but no large pack runs as a whole in Horizon yet. What carries over today is mods made of data and assets: blocks, items, recipes, loot tables, tags, models, textures and structures. Mods with Java code do not, yet.
## What the Modpacks page does
Open **Modpacks** in the launcher. (From the next launcher release, it is the **Modpacks** tab of the **Mods** page.)
1. **Import a pack.** Choose **Import a pack file…** for a Modrinth `.mrpack` or a CurseForge zip, or **Import an instance folder…** for a Prism, MultiMC or CurseForge instance. The **Mods** page can also find a Modrinth modpack and import it for you (see [Mods and data packs](/docs/mods#finding-resource-packs-and-modpacks)). From the next launcher release it also ports it in the same step, so you can press **Play** right away.
2. **Read the coverage report.** For every mod in the pack it says **Ported**, **Partial**, **Not yet** or **Not needed**, with the reason. You can filter the list by status.
3. **Port.** **Port N mods** gets the jars of the mods this computer can carry over today, and carries them over for the version you picked. It copies the jars from a folder you have, or downloads them from the pack's own links (or the same file from Modrinth's download network), and checks their hashes. CurseForge downloads are coming soon, so for files that only CurseForge has, choose **Use jars from my instance…** and point it at your installed instance.
4. **Play.** **Play with N mods** starts the game as the pack's own profile, with its own saves and options, and those mods on.
Nothing is uploaded or shared. The mod jars and what is made from them stay on your computer.
## What carries over, and what does not
**Carries over today:** content mods whose substance is data and assets. Blocks and items built on vanilla's shapes, recipes, loot tables, tags, models, textures and language files, and structures that are described in data, such as the structure mods that use jigsaw pools. Loot tables and structures from the mods you carry show up in your world.
**Not yet:**
- **Mods with Java code:** machines, magic, new mob behaviour, anything that needs the mod's own logic. Each of these must be ported by hand to Horizon's mod system (Rust compiled to WebAssembly), and checked against the original mod running in a Java test instance. That work is under way; none of it is something you can install yet.
- **World generation written in code:** custom biomes, terrain shaping and structure types that are Java classes.
- **Scripts** such as KubeJS, and recipe viewers and map mods that are built around Java.
**Not needed:** performance mods such as Sodium, Lithium or FerriteCore (Horizon is the replacement), and libraries whose job is to give mods a common API. The report says which is which.
So a pack is a list of mods, and today Horizon can bring over a part of that list. The coverage report is the honest answer for your pack.
If two ported mods use the same namespace, the game can only load one of them. The launcher keeps the first, leaves the other off, and lists it under the pack.
Measured on three packs: Vanilla Perfected (108 files) ports 2 mods, Slimes Adventure (105) ports 20, and All the Mods 10 (496) ports 42. Each opens a world, with no crash. Every ported mod is partial: its blocks, items, recipes and structures come over, its Java code doesn't. A world with All the Mods 10's 42 ported mods takes about 40 seconds to load, against about 5 without mods.
## Example: All the Mods 10
All the Mods 10 has 495 jars. Horizon's report sorts them: about 290 add content, 80 are libraries, and the rest are integrations, interface mods, tools and performance mods. A few dozen of the content mods carry over today. The measured numbers are in [docs/modpacks.md](https://github.com/ludovic111/horizon/blob/main/docs/modpacks.md).
## From the command line
```text
horizon-launcher modpack list
horizon-launcher modpack import <pack>
horizon-launcher modpack port <pack> [--from <dir>]
horizon-launcher modpack play <pack>
horizon-launcher modpack remove <pack>
```
## Licences
Mods have their own licences, and Horizon respects them: it does not redistribute anyone's jars. The ports are made on your computer, from jars you downloaded, and stay there.
-64
View File
@@ -1,64 +0,0 @@
# Mods and data packs
Horizon has its own mod system, written for Horizon and not compatible with Java mods. Data packs work in part. This page says what you can do with each today.
## Mods
Horizon mods are written in Rust, compiled to WebAssembly and run in a sandbox. A mod can only do what it asks for and you allow, and a mod that crashes is switched off without taking the game down.
> **Early.** The mod system is new. Reviewed mods are listed at [playhorizon.dev/mods](/mods), and any `.hmod` file can be put in the folder by hand.
**Playing with mods**
1. Put the mod's `.hmod` file in the `horizon-mods` folder of the game folder (see [Install](/docs/install#first-launch) for where that is), or use the **Open Mod Folder** button on the Mods screen.
2. Open **Options, Mods...**. Each mod is listed with what it may do ("May see your position, health and the time, draw text on your screen…") and a switch. Mods start off. Turn on the ones you trust.
3. A mod that failed to load shows why, in red, on its row.
**What a mod can do today**
- **Client mods** read the same things the F3 screen shows (your position, health, food, the time), draw text panels on the HUD, show chat lines only you see, and add chat commands. They work in single player and on any server, and a server cannot see them.
- **Content mods** add blocks and items from data and assets: models, textures, recipes, loot tables and tags, with no code. They work in single player today. This is also what the launcher's [modpack](/docs/modpacks) carry-over produces.
- **Server mods**, which run where the world is simulated, are planned.
**Making a mod**
[Make a mod](/docs/make-a-mod) is the full guide: the tools, your first mod in the game, the manifest and capabilities, the code with a worked example, content mods, testing, and publishing on [the registry](/mods). In short:
```sh
rustup target add wasm32-unknown-unknown
cargo install --git https://github.com/ludovic111/horizon cargo-horizon-mod
cargo horizon-mod new my-mod
cd my-mod
cargo horizon-mod build
```
`cargo horizon-mod test` runs the mod's test scripts (`tests/*.toml`: ticks, commands and the player's position in, chat and HUD lines checked) with no game open. The [Mod API](/docs/api/mods) lists what a mod can do today and what comes next, and its [reference](/docs/api/mods-reference) every function. Java mod jars don't run in Horizon: their code calls and patches Minecraft's Java classes, and Horizon has none. What carries over is the data and assets inside a jar ([modpacks](/docs/modpacks)).
## Finding resource packs and modpacks
The launcher's **Mods** page (launcher 0.17 and newer) searches [Modrinth](https://modrinth.com) for what Horizon can load: resource packs and modpacks. Each result says how well it runs in Horizon (**Works**, **Partly**, or **Doesn't run**), and by default the page shows only what runs. **Install** downloads the file, checks it against the checksum Modrinth lists and refuses anything that does not match, then puts it in the right folder, with the packs it needs. **Update** and **Remove** are there too. A resource pack still has to be turned on in the game's Resource Packs screen. A modpack is imported on the [Modpacks page](/docs/modpacks).
CurseForge support is coming soon: the page shows it, but doesn't search it yet. The same things work from a terminal: `horizon-launcher mods search`, `install`, `update` and `remove`.
## Java mods from the Mods page (next launcher release)
Java code does not run in Horizon, but much of what a mod adds is data: blocks, items, recipes, loot tables, tags, worldgen, textures. From the next launcher release, the Mods page also lists Fabric, NeoForge and Forge mods made for 26.3 and, before offering one, downloads its jar, checks it against Modrinth's checksum and reads it (without running it) to say what of it Horizon runs:
- **Works**: the mod is only content, and all of it carries over.
- **Partly**: its content carries over; the row lists what does not, such as machines and containers, mobs, blocks or items with behaviour of their own, recipe types of its own, or Mixins.
- **Doesn't run**: there is nothing to carry over (performance mods, libraries, mods that are all code). It is not offered.
**Install** brings the mod over into `horizon-mods` and turns it on; start the game again to load it. Blocks take the closest vanilla block's shape, hardness and sounds, so a carried-over block can break faster or slower than in Java. Most popular mods today are **Partly** or **Doesn't run**: the verdict is the honest answer for each one.
## Data packs
A world's data packs go in its `datapacks` folder, as folders or `.zip` files with a `pack.mcmeta`, as in vanilla.
**Works today (single player and open-to-LAN worlds):**
- **Functions** (`.mcfunction`, with macros and line continuation), **function tags** (such as `minecraft:load` and `minecraft:tick`), **predicates** and **item modifiers**.
- The `/function`, `/datapack` and `/reload` commands, and the choice of which packs are on, kept with the world.
**Not applied from data packs yet:** custom recipes, loot tables, advancements and world generation. Horizon reads those from the vanilla data it prepared, and from content mods.
Vanilla's own data is always on, and it matches vanilla: every command runs as vanilla does, and recipes, loot and world generation are checked against the real game.
-43
View File
@@ -1,43 +0,0 @@
# Multiplayer
You can play with friends from your own world, on your local network, or on a server. This page also says plainly what does not work yet: servers that check Minecraft accounts.
## Play with friends from your own world
This needs no server and no account.
1. In your world, open the pause menu and choose the **Friends** button (the icon with two people), then **Start Sharing**. Horizon opens the world to your network and shows an **invite code** with **Copy Code**.
2. Send the code to your friends.
3. On their side, your friends choose the **Friends** button on the title screen (or **Join a friend** on the launcher's **Play** page), paste the code and choose **Join**. Pasting the code in **Direct Connection** works too.
There is no Horizon server in the middle. The code holds the host's addresses and a secret key, and the game between you is encrypted with it. A code works for as long as you keep sharing: closing the world or quitting ends it.
**Can your friends reach you?** The Play with Friends screen says what the game found:
- On the same network, or over a VPN such as Tailscale or ZeroTier, it just works.
- Over the internet, it asks your router to open the port by itself (UPnP). If the router does not allow it, forward TCP port 25580 to your computer.
- Some internet providers share one address between many homes (CGNAT), and no port can be opened. Use a VPN such as [Tailscale](https://tailscale.com) instead; the code already contains your VPN address.
Guests join with the name their game uses. There is no account check, so only share the code with people you trust.
Guests can also use the host's workstations: the crafter, loom, stonecutter, grindstone, anvil, smithing table, enchanting table, beacon, lectern and cartography table.
## Open to LAN
As in vanilla, **Open to LAN** in the pause menu lets people on the same network join your world. They see it in their server list. A vanilla 26.3 client can join a Horizon LAN world, and Horizon can join a vanilla one. LAN worlds are in offline mode.
## Joining a server
The Multiplayer page works like vanilla's: **Add server** saves one, **Direct connect** takes an address, and your servers are listed with their message of the day and ping. Horizon joins vanilla 26.3 servers and Horizon's own.
**Online mode.** Most public servers run in online mode: they check that each player really owns Minecraft by asking Mojang. To pass that check, the game needs a Microsoft account sign-in, and Horizon cannot do that yet. It needs an app registration and an approval from Mojang that the project does not have. So today:
- **Horizon can join servers in offline mode only** (`online-mode=false`): your own, a friend's, or a server on your network or a VPN.
- **It cannot join servers that check accounts.** That is most public servers, and Realms.
- Skins served over https show the default skin unless they were already cached.
Anyone can join an offline server under any name, so run one only on your network or a VPN, or share it with people you trust. [Host a server](/docs/hosting) explains the details.
## Vanilla players on a Horizon server
A vanilla 26.3 client can join a Horizon server, and in online mode it signs in with its own account as on a vanilla server. Secure chat works for those players.
-10
View File
@@ -1,10 +0,0 @@
[
{ "group": "Start here", "pages": ["index", "install", "how-horizon-is-built"] },
{ "group": "Playing", "pages": ["settings", "multiplayer", "troubleshooting"] },
{ "group": "Hosting", "pages": ["hosting"] },
{ "group": "Going further", "pages": ["modpacks", "mods", "performance", "agents"] },
{ "group": "Make a mod", "pages": ["make-a-mod", "mod-manifest", "mod-code", "content-mods", "test-a-mod", "publish-a-mod"] },
{ "group": "Build on Horizon", "pages": ["apis", "api", "api/stability"] },
{ "group": "API reference", "pages": ["api/mods", "api/mods-reference", "api/mod-index", "api/server", "api/server-reference", "api/agent-control", "api/launcher", "api/data-formats", "api/shaders"] },
{ "group": "Project", "pages": ["contributing", "faq"] }
]
-74
View File
@@ -1,74 +0,0 @@
# Performance
Horizon is built to run Minecraft with less work: less memory, no pauses from a Java runtime, and a renderer that keeps the graphics card busy. This page says what Horizon does differently, and what has been measured. Where nothing has been published yet, it says so.
## What Horizon does differently
- **Native code.** Horizon is Rust, compiled for your system. There is no Java virtual machine, so no garbage-collection pauses and no warm-up while the code gets compiled as you play.
- **It ticks what vanilla ticks, and no more.** Mobs, minecarts and dropped items outside the simulation distance only check whether to despawn, as in vanilla. Before this was fixed, Horizon ticked seven times as many of them as vanilla at render distance 32.
- **Work off the main thread.** Chunks are generated, lit and meshed on worker threads, and unloaded chunks are written to disk in the background.
- **Fewer, bigger draw calls.** Terrain sections share one vertex arena and are drawn with one indirect multi-draw per pass. Mobs and block entities outside the view are skipped, and caves you cannot see into are culled.
- **Distant terrain.** The optional [LOD distance](/docs/settings#distant-terrain-lod) draws terrain up to 1024 chunks away without simulating it.
- **A server without Java.** `horizon-server` is one native program. See below.
## The client
The numbers below are Horizon before and after a round of performance work, on an RTX 4080, in a fresh creative world flown over a village, a forest, the ocean and a mountain (`tools/graphics_bench.py`). They compare Horizon with an earlier Horizon, not with vanilla.
- Render distance 8 over a village: **363 to 2,409 FPS**, and the slowest 1% of frames from 21.6 to 160.8 FPS.
- Render distance 16 over a village: **8 to 1,041 FPS**. In a forest: 28 to 316 FPS.
- Render distance 32 improved too (a village went from 4 to 48 FPS) but **still stutters in places**: the slow frames are game ticks while chunks load, not drawing.
The full tables, with the method and the Linux software-rendering run, are in [docs/graphics.md](https://github.com/ludovic111/horizon/blob/main/docs/graphics.md#results).
**Horizon against vanilla.** The benchmark exists: `tools/perf_bench.py` starts Horizon, vanilla 26.3 and vanilla with Sodium, Lithium, FerriteCore and ImmediatelyFast on the same machine, world, camera path and settings, and computes every number the same way. [docs/benchmark.md](https://github.com/ludovic111/horizon/blob/main/docs/benchmark.md) says how it keeps the comparison fair. Its results for the client, measured on the project's own machines, are on the [Benchmarks page](/benchmarks), with the places where Java is faster. It also estimates other hardware from them, as a range, and shows players' uploaded results once at least three installs share a graphics card and processor.
**Measure your own computer.** Horizon's half of that benchmark is built into the game. Close other heavy apps,
plug in a laptop, and run in a terminal:
```sh
horizon-launcher play --arg=--benchmark --arg=standard
```
Add `--arg=--render-distance --arg=16` to choose the render distance; your other settings are used as they are and
stored in the report. The game creates its own world, `horizon-benchmark` (it never touches your saves), flies over a
village, a forest, the ocean and a mountain, and quits by itself. `--arg=village` (or `forest`, `ocean`, `mountain`)
instead of `--arg=standard` flies one scene. For the run only, VSync is off and the frame rate unlimited; your options
are not changed.
The report is a `.json` file in the `benchmarks` folder of the game folder:
| System | Folder |
|---|---|
| Windows | `%APPDATA%\horizon\benchmarks` |
| macOS | `~/Library/Application Support/horizon/benchmarks` |
| Linux | `~/.local/share/horizon/benchmarks` |
It holds startup times, each scene's chunk load time, the frame rate with its 1% and 0.1% lows, memory, and your
machine (operating system, processor, memory, graphics card and driver). Its format is
[docs/benchmark-report.md](https://github.com/ludovic111/horizon/blob/main/docs/benchmark-report.md).
**Share it.** To add your computer to the [Benchmarks page](/benchmarks), either:
- upload it from the launcher: `horizon-launcher data benchmark <the report's path>` shows exactly what would be sent,
without your file paths, and asks before sending. Or allow benchmark uploads once (`horizon-launcher data allow
benchmarks`), and every later report is sent for you. See [Privacy](/privacy);
- or post the `.json` file in the Discord.
## Servers
The latest bot benchmark compares `horizon-server` with vanilla 26.3, Paper and vanilla on Fabric with Lithium. It ran on a 4-thread laptop CPU (an i7-7500U), with 1 to 50 bots walking, exploring and standing around spawn, on another machine. All four servers did the same work. In short:
- **Memory:** 0.7 GB with one player and 1.1 GB with 50, against 3.4 to 4.5 GB for the Java servers (each with a 4 GB heap). That is 3.8 to 4.8 times less.
- **Start:** ready in 8 to 9 seconds, against 15 for vanilla, 17 for Fabric and 19 to 22 for Paper.
- **CPU:** from 10 players up, the least of the four.
- **Average tick:** Paper's was shorter at every player count, and Fabric with Lithium's at 1 and 10 players. Horizon's was shorter than vanilla's, and than Fabric's from 25 players up.
- **Slow ticks:** from 10 players up, Horizon's slowest ticks were the shortest of the four.
- **50 players:** Horizon and Paper kept 20 TPS. Fabric with Lithium dropped to 17, and vanilla's watchdog stopped it.
For more than 50 active players, give it more cores. The full report is [benchmarks/server-four-way-2026-10-19](https://github.com/ludovic111/horizon/blob/main/benchmarks/server-four-way-2026-10-19/README.md).
## Where else to look
- [Benchmark report format](https://github.com/ludovic111/horizon/blob/main/docs/benchmark-report.md): what every benchmark writes.
- `HORIZON_SPIKES=MS` prints what made every frame slower than that many milliseconds slow; it is described in [docs/graphics.md](https://github.com/ludovic111/horizon/blob/main/docs/graphics.md#benchmark).
-57
View File
@@ -1,57 +0,0 @@
# Publish a mod
Horizon's registry lists mods, modpacks and resource packs made for Horizon. Players find them at [playhorizon.dev/mods](/mods) and on the launcher's Mods page, which checks every file before it installs it. Each project is reviewed by a person before it is published. This page takes your mod from a file on your computer to the registry.
## Before you submit
1. **Choose a version.** Set `version` in `horizon-mod.toml` (such as `1.0.0`). Every version you publish needs a new one, and a published file never changes.
2. **Build it for release.** `cargo horizon-mod build` (release is the default; `--debug` is for quick local builds) writes `target/horizon-mod/<id>.hmod`. That file is what you submit.
3. **Check it.** `cargo horizon-mod check target/horizon-mod/<id>.hmod` must pass ([Test a mod](/docs/test-a-mod#check-it-as-the-registry-does)).
4. **Publish the source.** Put the mod's source in a public repository, under a licence that lets players use it (MIT, Apache-2.0, GPL and so on, as an [SPDX](https://spdx.org/licenses/) name). Reviewers read it, and the file must be built from it.
## Sign it
Signing is optional and recommended. When your project has a public key, every later version must be signed with the matching secret key, so nobody else can publish an update in your name, even with access to your GitHub account.
Horizon uses [minisign](https://jedisct1.github.io/minisign/) signatures. Install minisign (`brew install minisign`, `scoop install minisign`, or your Linux distribution's package), then make a key pair once:
```sh
minisign -G -p horizon-mods.pub -s horizon-mods.key
```
Keep `horizon-mods.key` and its password private and backed up: losing it means changing the project's key by hand, through the registry's maintainers. The public key is the last line of `horizon-mods.pub` (it starts with `RW`).
Sign each file you publish, and check the signature as the registry will:
```sh
minisign -S -s horizon-mods.key -m target/horizon-mod/playtime.hmod
cargo horizon-mod check target/horizon-mod/playtime.hmod --key "RW…your public key…" --signature target/horizon-mod/playtime.hmod.minisig
```
The report then says "signature verified".
## Submit it
Open [playhorizon.dev/mods/submit](/mods/submit) and sign in with GitHub. The page reads only your public profile. Then fill in:
- **The file**: the kind (mod), the `.hmod`, its id (the manifest's `id`), its version, the Minecraft version (26.3) and what changed.
- **Its page**: the name, a one-line summary for the list, a description (Markdown), the authors, the licence, the source address and, optionally, a square PNG icon (at most 256 KiB).
- **Signature**: your public key (for a new project) and the file's `.minisig`.
**Submit for review** checks the file again and opens a pull request in [ludovic111/horizon-mods](https://github.com/ludovic111/horizon-mods). It adds `mods/<id>.toml`, with you as the project's owner. The repository's checks run the file in the sandbox and post their report on the pull request.
## Review and publication
A reviewer reads the pull request, your source and the checks' report. Questions and requests for changes come as comments on the pull request: answer them there, and submit a new version from the form when the file must change.
When the reviewer merges it, the file becomes a release of horizon-mods within minutes, and the project gets its page at `playhorizon.dev/mods/<id>` and shows in the launcher. The page lists what the mod may do, as the game's Mods screen does, with your source, licence and the file's SHA-256.
## Updates
An update is the same form with a new version: it adds the version to your project's entry. The project's owners publish updates without a reviewer's extra look, and if the project has a key, the new file must be signed with it.
Without the form, you can open the pull request by hand: the [horizon-mods README](https://github.com/ludovic111/horizon-mods) says how. The registry's file formats are in [the mod index reference](/docs/api/mod-index).
## Reports and takedowns
Anyone can report a project from its page (**Report it**). A project that does harm is taken down: the launcher then refuses its files and turns installed copies off, telling players why.
-70
View File
@@ -1,70 +0,0 @@
# Settings worth knowing
Horizon plays like vanilla out of the box. This page covers the settings that are different or new: graphics, distant terrain, resource packs, controllers and a few optional extras.
The game's menus are vanilla's. Horizon adds its own settings to vanilla's **Options** screen the way mods do: **Horizon Settings...** and **Mods...** take the last two buttons of the grid. **Horizon Settings** opens Graphics, Advanced Graphics, Controller Settings, Extras and Mods, each a screen of vanilla's buttons and sliders. The graphics buttons are also at the top of **Video Settings**, and Controller Settings at the bottom of **Controls**.
![Video Settings, with Horizon's graphics buttons at the top](/media/settings-800.webp)
## Graphics: presets, Simple and Advanced
**Options, Video Settings** starts with **Horizon Graphics...** and **Advanced Graphics...** (also in **Options, Horizon Settings**).
- **Horizon Graphics** has a preset (Performance, Balanced, Quality, Ultra or Custom) and LOD distance.
- **Advanced Graphics** has every Horizon setting, in groups: Rendering, Quality, Display, Preferences, Performance and Overlay.
Vanilla's own settings (render distance, field of view, frame rate, VSync, GUI scale and the rest) stay on Video Settings, where vanilla has them.
Every row has a one-line description and its cost in frame time: none, low, medium or high. Changes apply at once, except worker threads, which apply at the next start. A preset sets the quality settings together, and changing one of them turns the preset into **Custom**.
Two settings are worth knowing on laptops and handhelds. **Battery mode** (Performance group, off by default) caps the frame rate at 60 in a world and 30 in menus, and uses half the automatic worker threads. The **performance overlay** (Overlay group) shows the frame rate, frame times and a graph over the game.
Not available: anisotropic filtering, MSAA (FXAA is there instead) and exclusive fullscreen (fullscreen is borderless).
## Distant terrain (LOD)
**LOD Distance** draws far-away terrain as simplified columns, so you can see much farther than the render distance. It is **off by default**. Turn it on in the Video settings and choose 64, 128, 256, 512 or 1024 chunks.
- Keep the normal render distance modest next to it. For example 12 chunks with a LOD distance of 256. **Full Detail Distance** sets the nearby range.
- LOD does not raise the simulation distance: far terrain is only drawn, not simulated.
- Detail arrives gradually. A big radius does not mean everything is drawn right away.
- Far terrain is made from the world's seed, so places you have not visited are drawn too, close to what they will be. Small features, fluid edges and lighting can differ from the real chunks.
- Horizon caps far terrain on integrated graphics and holds it back on computers with four threads.
## Graphics API on Windows
Linux always uses Vulkan and macOS always uses Metal. On Windows there is a choice, in **Options, Video Settings, Advanced Graphics, Performance, Graphics API**:
- **Auto** (the default) uses Vulkan, and DirectX 12 when Vulkan has no graphics card or driver that works.
- **Vulkan** and **DirectX 12** force one of the two.
The change applies at the next start. If Vulkan gives you trouble, try DirectX 12.
## Shader packs
Shader packs have been removed for now. They will come back later, written again from scratch. Packs in your `shaderpacks` folder are left where they are.
## Resource packs
Java Edition resource packs work. Put the folder or `.zip` in the `resourcepacks` folder of the game folder and pick it on the **Resource Packs** screen, which is vanilla's screen. The game reloads every loader from the pack, as vanilla does.
The launcher's **Mods** page can download them from Modrinth (see [Mods and data packs](/docs/mods#finding-resource-packs-and-modpacks)).
[Resource packs: what works](https://github.com/ludovic111/horizon/blob/main/docs/resource-packs.md) lists everything a 26.3 resource pack can change and whether Horizon honours it.
## Controllers
Horizon plays with PlayStation (DualShock 4 and DualSense), Xbox (360, One and Series) and Nintendo Switch Pro controllers, and with the Steam Deck's built-in controls. Vanilla has no controller support at all, so this is Horizon's own. With no controller connected, nothing changes.
- Settings are in **Options, Controls, Controller Settings** (or **Options, Horizon Settings, Controller Settings**): on or off, vibration, look sensitivity, inverting the axes, deadzones, button glyphs and remapping.
- Menus, the inventory and chat work with a controller, with an on-screen keyboard for text.
- On a DualSense you can also set adaptive triggers and the lightbar.
- On the Steam Deck, Steam's default layout works with no setup.
## Extras
**Options, Horizon Settings, Extras** holds small, optional conveniences. Every one is off by default, so a fresh install behaves like vanilla: fullbright, auto-reconnect, Mouse Tweaks-style inventory controls and inventory sorting. The FPS counter, the keystroke display, zoom and the minimap are official mods now, in **Options, Mods**: if you had one on, the game installed it for you on its first start after the update, with your settings. One more is **dynamic lights**: a torch, lantern or other light source that you hold, that lies dropped as an item, or a burning mob, lights the blocks around it. It is client-side only, so other players do not see it.
## Where your settings are stored
In the game folder, next to `options.txt`. That file keeps vanilla's own keys and values, so a vanilla client can share the folder. Horizon's own settings are in `horizon-graphics.txt`, `horizon-controller.txt` and a few more beside it.
-59
View File
@@ -1,59 +0,0 @@
# Test a mod
Three kinds of checks catch most mistakes before players see them: unit tests for your own logic, the game's dev mode for how the mod looks and feels, and `cargo horizon-mod check`, which runs the same checks as the registry.
## Unit tests for your logic
Code that doesn't call the game (formatting, maths, parsing a command's arguments) is ordinary Rust. Put it in plain functions and test it with `#[test]`, as the [Playtime example](/docs/mod-code#a-worked-example-playtime) does with its `format` function:
```sh
cargo test
```
The tests run on your computer, with no game and no WebAssembly. Keeping the hooks thin and the logic in functions makes most of a mod testable this way.
## In the game, as a dev mod
Start the game with your mod as a dev mod ([how](/docs/make-a-mod#see-it-in-the-game)) and keep it running. After each edit:
```sh
cargo horizon-mod build
```
The game sees the new file within a second, says "Mods reloaded" in chat and starts the mod over. What it saved with `storage` stays. Things to try each time:
- Every command, with no arguments, with good ones and with wrong ones. An `Err` should say what to type instead.
- Leaving the world and opening another: `game::get_player()` is `None` on the title screen.
- Quitting and starting again, for a mod that saves.
- **Options, Mods...**: the mod's row lists what it may do. If it says the mod stopped, the reason is there.
The game log (`logs/latest.log` in the game folder) has your mod's `info` and `warn` lines as `[mod <id>]`, and the reason when the game refused or stopped the mod.
**Why a mod stops.** A mod that panics, runs past its time budget for one call (an endless loop, for example) or uses more than 64 MiB of memory is switched off. The rest of the game carries on. Fix the cause and build again: a dev mod starts over with the new file.
## Check it as the registry does
Before you publish, run what the registry runs on every submission:
```sh
cargo horizon-mod check target/horizon-mod/playtime.hmod
```
```
playtime 0.1.0 (mod, API 0.1, client)
12424 bytes, SHA-256 6561…ac13, unsigned
may draw text on your screen
may show you chat messages
may save its own settings to disk
trial run: started, 40 ticks, 120 frames, a chat line, /playtime: ok
```
It reads the manifest, refuses code that uses a capability the manifest doesn't list, then plays the mod in the sandbox, as the game does: 40 ticks with a moving player, 120 frames, a chat line and each of its commands once with no arguments. The mod must start and must never crash or overrun its budget. It fails with the reason when one of those goes wrong.
- `--json` prints the report as JSON, for scripts.
- `--key <public key> --signature <file>.minisig` also checks your signature ([Publish](/docs/publish-a-mod#sign-it)).
- It checks modpacks (`.hpack`) and resource packs (`.zip`) too; `--kind mod|modpack|resourcepack` when the file's extension doesn't say which.
A mod that passes `check` passes the registry's automatic checks. A person still reviews it (see [Publish](/docs/publish-a-mod)).
Next: [publish it](/docs/publish-a-mod).
-55
View File
@@ -1,55 +0,0 @@
# Logs, crashes and bug reports
When something breaks, Horizon writes down what happened. Send us that and the bug is much easier to fix. This page says where it is and how to report it.
## If the game crashes
The launcher notices. It shows what went wrong, in one line, with buttons:
- **Open report** opens the crash report.
- **Copy report** copies it, so you can paste it where you report the bug.
- **Open logs folder** opens the folder with the logs and crash reports.
- **Report on Discord** takes you to our Discord server.
## Where the files are
Everything is in the game folder, which is the same for every version you install:
- Linux: `~/.local/share/horizon`
- macOS: `~/Library/Application Support/horizon`
- Windows: `%APPDATA%\horizon`
Inside it:
- `logs/latest.log` is the log of the current or last run. Older runs are kept as `logs/<date>-<number>.log.gz`.
- `crash-reports/crash-<date>_<time>-client.txt` is written when the game crashes. It holds the error, where it happened, your computer, your graphics card, your render settings, your resource packs and the last 50 lines of the log.
The launcher's **Settings** page has a **Logs** row with an **Open** button. On the command line, `horizon-launcher logs --crash` prints the newest crash report and `horizon-launcher logs --lines 200` the end of the log.
## What to try first
- **The game does not start, or crashes straight away.** Update the launcher and Horizon, and your graphics drivers. On Windows, try **Graphics API: DirectX 12** (see [Settings](/docs/settings#graphics-api-on-windows)).
- **"Not prepared" or "Minecraft 26.3 was not found".** Play 26.3 once in the official Minecraft launcher, or point the launcher at your Minecraft folder (**Choose…** next to Minecraft folder on the Play page), then choose **Prepare** again.
- **Preparation says Java 25 is needed.** Update the launcher: it then uses the Java the Minecraft launcher downloaded for 26.3, or downloads it from Mojang. If it still can't (no internet, say), install a Java 25 JDK, for example Eclipse Temurin 25.
- **Slow or stuttering.** Lower the render distance, turn LOD off, or pick the **Performance** preset. The performance overlay (Video, Advanced, Overlay) shows what is slow.
- **A resource pack broke something.** Switch it off, or take it out of its folder.
## Reporting a bug
Good places, in this order:
1. **GitHub issues:** [github.com/ludovic111/horizon/issues](https://github.com/ludovic111/horizon/issues). Search first; someone may have reported it already.
2. **Discord:** the bugs channel on [our Discord server](https://discord.gg/HcTeMZxe5E), if you would rather talk it through.
Include:
- what you did and what you expected;
- your Horizon version (shown on the launcher's Play page) and the launcher's version;
- your system and graphics card;
- the crash report, or the end of `latest.log`, if there is one. Read it first: logs can hold your player name and server addresses, and lines of chat.
A Horizon bug is any place where it differs from vanilla 26.3, so "vanilla does X and Horizon does Y" is the best report there is.
## Data the launcher can send
The launcher can send performance statistics, crash reports, bug reports and benchmarks, but only the kinds you allow, and everything is off until you say yes. See [Privacy](/privacy) for what is sent, how long it is kept and how to delete it.
-642
View File
@@ -1,642 +0,0 @@
{
"name": "horizon-website",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "horizon-website",
"devDependencies": {
"playwright": "^1.56.0",
"sharp": "^0.34.4",
"wawoff2": "^2.0.1"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/playwright": {
"version": "1.64.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz",
"integrity": "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.64.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright-core": {
"version": "1.64.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz",
"integrity": "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"optional": true
},
"node_modules/wawoff2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/wawoff2/-/wawoff2-2.0.1.tgz",
"integrity": "sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"woff2_compress.js": "bin/woff2_compress.js",
"woff2_decompress.js": "bin/woff2_decompress.js"
}
}
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"name": "horizon-website",
"private": true,
"type": "module",
"description": "horizon's website: a static page in the launcher's design",
"scripts": {
"build": "node build.mjs",
"start": "node serve.mjs",
"dev": "node build.mjs && node serve.mjs",
"assets": "node scripts/assets.mjs",
"shots": "node scripts/shots.mjs",
"test": "node --test src/ scripts/"
},
"devDependencies": {
"sharp": "^0.34.4",
"wawoff2": "^2.0.1",
"playwright": "^1.56.0"
}
}
-523
View File
@@ -1,523 +0,0 @@
{
"format": "horizon-benchmark-summary",
"version": 1,
"generated": "2026-10-23T01:09:54Z",
"min_installs": 3,
"groups": [
{
"gpu": "Apple M4 Pro",
"cpu": "Apple M4 Pro",
"render_distance": 12,
"game": "horizon",
"source": "us",
"machine": "Mac mini (M4 Pro)",
"measured": "2026-10-22",
"version": "v2026.10.22.04",
"backend": "metal",
"os": "macos",
"window": [
1280,
720
],
"repeats": 3,
"load_avg_1m": 55.691,
"note": "On macOS this Horizon build presents its frames in step with the 144 Hz display even with VSync off, so its average frame rate here is capped at 144: compare the 1% lows and frame times instead. The Mac mini was also running other jobs during the runs (CI, simulators; 1-minute load average 20 to 56 on 14 threads), which costs both games, vanilla more at render distance 32.",
"capped_fps": 144,
"medians": {
"avg_fps": 144,
"low_1pct_fps": 97,
"p95_ms": 7.7,
"world_ms": 11075,
"chunk_load_ms": 9438,
"rss_peak_mb": 1332,
"rss_steady_mb": 1201,
"cpu_p50_ms": 0.7,
"gpu_p50_ms": null
},
"scenes": {
"village": {
"avg_fps": 144,
"low_1pct_fps": 97,
"chunk_load_ms": 6990
},
"forest": {
"avg_fps": 144,
"low_1pct_fps": 100,
"chunk_load_ms": 10500
},
"ocean": {
"avg_fps": 144,
"low_1pct_fps": 98,
"chunk_load_ms": 10672
},
"mountain": {
"avg_fps": 144,
"low_1pct_fps": 94,
"chunk_load_ms": 9588
}
}
},
{
"gpu": "Apple M4 Pro",
"cpu": "Apple M4 Pro",
"render_distance": 32,
"game": "horizon",
"source": "us",
"machine": "Mac mini (M4 Pro)",
"measured": "2026-10-22",
"version": "v2026.10.22.04",
"backend": "metal",
"os": "macos",
"window": [
1280,
720
],
"repeats": 3,
"load_avg_1m": 34.828,
"capped_fps": 144,
"medians": {
"avg_fps": 144,
"low_1pct_fps": 109,
"p95_ms": 7.7,
"world_ms": 16377,
"chunk_load_ms": 16379,
"rss_peak_mb": 2267,
"rss_steady_mb": 1892,
"cpu_p50_ms": 1.5,
"gpu_p50_ms": null
},
"scenes": {
"village": {
"avg_fps": 144,
"low_1pct_fps": 101,
"chunk_load_ms": 12453
},
"forest": {
"avg_fps": 144,
"low_1pct_fps": 114,
"chunk_load_ms": 16309
},
"ocean": {
"avg_fps": 144,
"low_1pct_fps": 119,
"chunk_load_ms": 14322
},
"mountain": {
"avg_fps": 144,
"low_1pct_fps": 100,
"chunk_load_ms": 22432
}
}
},
{
"gpu": "Apple M4 Pro",
"cpu": "Apple M4 Pro",
"render_distance": 12,
"game": "vanilla",
"source": "us",
"machine": "Mac mini (M4 Pro)",
"measured": "2026-10-22",
"version": "26.3",
"backend": "opengl",
"os": "macos",
"window": [
1280,
720
],
"repeats": 3,
"load_avg_1m": 19.995,
"medians": {
"avg_fps": 123,
"low_1pct_fps": 29,
"p95_ms": 17,
"world_ms": 22268,
"chunk_load_ms": 10264,
"rss_peak_mb": 2445,
"rss_steady_mb": 2283,
"cpu_p50_ms": null,
"gpu_p50_ms": null
},
"scenes": {
"village": {
"avg_fps": 136,
"low_1pct_fps": 40,
"chunk_load_ms": 8374
},
"forest": {
"avg_fps": 115,
"low_1pct_fps": 22,
"chunk_load_ms": 10976
},
"ocean": {
"avg_fps": 127,
"low_1pct_fps": 29,
"chunk_load_ms": 7219
},
"mountain": {
"avg_fps": 112,
"low_1pct_fps": 23,
"chunk_load_ms": 14487
}
}
},
{
"gpu": "Apple M4 Pro",
"cpu": "Apple M4 Pro",
"render_distance": 32,
"game": "vanilla",
"source": "us",
"machine": "Mac mini (M4 Pro)",
"measured": "2026-10-22",
"version": "26.3",
"backend": "opengl",
"os": "macos",
"window": [
1280,
720
],
"repeats": 3,
"load_avg_1m": 24.818,
"medians": {
"avg_fps": 43,
"low_1pct_fps": 8,
"p95_ms": 88,
"world_ms": 65831,
"chunk_load_ms": 50534,
"rss_peak_mb": 2947,
"rss_steady_mb": 2932,
"cpu_p50_ms": null,
"gpu_p50_ms": null
},
"scenes": {
"village": {
"avg_fps": 48,
"low_1pct_fps": 6,
"chunk_load_ms": 23987
},
"forest": {
"avg_fps": 54,
"low_1pct_fps": 13,
"chunk_load_ms": 36379
},
"ocean": {
"avg_fps": 48,
"low_1pct_fps": 8,
"chunk_load_ms": 18553
},
"mountain": {
"avg_fps": 23,
"low_1pct_fps": 3,
"chunk_load_ms": 123217
}
}
},
{
"gpu": "Intel HD Graphics 620 (KBL GT2)",
"cpu": "Intel Core i7-7500U CPU @ 2.70GHz",
"render_distance": 12,
"game": "horizon",
"source": "us",
"machine": "ThinkPad (i7-7500U, HD Graphics 620)",
"measured": "2026-10-22",
"version": "v2026.10.22.04",
"backend": "vulkan",
"os": "linux",
"window": [
1280,
720
],
"repeats": 2,
"load_avg_1m": 0.988,
"note": "A 2016 laptop: 2 cores / 4 threads and Intel integrated graphics, Linux with Mesa 26.1.8 (Horizon on Vulkan, vanilla on OpenGL). It has no screen attached, so both games drew to a GPU compositor (sway on the Intel GPU) in a 1280x720 window. Horizon ran two flights per scene, vanilla two separate games of one. Render distance 32 is not shown: in 5 minutes neither game finished loading the spawn, and given 25, Horizon kept only about 900 to 1,400 chunk columns resident on this machine instead of the 3,200 a full view holds, so the two games would not have drawn the same world.",
"medians": {
"avg_fps": 209,
"low_1pct_fps": 39,
"p95_ms": 14,
"world_ms": 50794,
"chunk_load_ms": 59726,
"rss_peak_mb": 986,
"rss_steady_mb": 797,
"cpu_p50_ms": 2.6,
"gpu_p50_ms": 4.5
},
"scenes": {
"village": {
"avg_fps": 273,
"low_1pct_fps": 46,
"chunk_load_ms": 44860
},
"forest": {
"avg_fps": 246,
"low_1pct_fps": 48,
"chunk_load_ms": 67527
},
"ocean": {
"avg_fps": 204,
"low_1pct_fps": 36,
"chunk_load_ms": 51485
},
"mountain": {
"avg_fps": 111,
"low_1pct_fps": 26,
"chunk_load_ms": 75032
}
}
},
{
"gpu": "Mesa Intel HD Graphics 620 (KBL GT2)",
"cpu": "Intel Core i7-7500U CPU @ 2.70GHz",
"render_distance": 12,
"game": "vanilla",
"source": "us",
"machine": "ThinkPad (i7-7500U, HD Graphics 620)",
"measured": "2026-10-22",
"version": "26.3",
"backend": "opengl",
"os": "linux",
"window": [
1280,
720
],
"repeats": 2,
"load_avg_1m": 5.749,
"medians": {
"avg_fps": 126,
"low_1pct_fps": 29,
"p95_ms": 17,
"world_ms": 68127,
"chunk_load_ms": 46879,
"rss_peak_mb": 2144,
"rss_steady_mb": 2074,
"cpu_p50_ms": null,
"gpu_p50_ms": null
},
"scenes": {
"village": {
"avg_fps": 91,
"low_1pct_fps": 22,
"chunk_load_ms": 52101
},
"forest": {
"avg_fps": 104,
"low_1pct_fps": 28,
"chunk_load_ms": 47218
},
"ocean": {
"avg_fps": 104,
"low_1pct_fps": 25,
"chunk_load_ms": 33421
},
"mountain": {
"avg_fps": 205,
"low_1pct_fps": 42,
"chunk_load_ms": 54778
}
}
},
{
"gpu": "NVIDIA GeForce RTX 4080",
"cpu": "13th Gen Intel Core i7-13700KF",
"render_distance": 12,
"game": "horizon",
"source": "us",
"machine": "Desktop (RTX 4080, i7-13700KF)",
"measured": "2026-10-23",
"version": "v2026.10.22.13",
"backend": "vulkan",
"os": "linux",
"window": [
1280,
720
],
"repeats": 3,
"load_avg_1m": 17.934,
"note": "Linux (NixOS) with the NVIDIA 595 driver. The desktop is in daily use, so both games drew to a separate GPU compositor (a headless sway rendering on the RTX 4080) in a 1280x720 window, never on its screen: Horizon through Wayland, vanilla through Xwayland, both on Vulkan. Other jobs were running during the runs (1-minute load average 18 to 25 on 24 threads), which costs both games, chunk loading most.",
"medians": {
"avg_fps": 2993,
"low_1pct_fps": 311,
"p95_ms": 0.5,
"world_ms": 17973,
"chunk_load_ms": 1797,
"rss_peak_mb": 1414,
"rss_steady_mb": 1202,
"cpu_p50_ms": 0.3,
"gpu_p50_ms": 0.1
},
"scenes": {
"village": {
"avg_fps": 2594,
"low_1pct_fps": 254,
"chunk_load_ms": 1260
},
"forest": {
"avg_fps": 3427,
"low_1pct_fps": 328,
"chunk_load_ms": 1592
},
"ocean": {
"avg_fps": 3277,
"low_1pct_fps": 388,
"chunk_load_ms": 2660
},
"mountain": {
"avg_fps": 2673,
"low_1pct_fps": 275,
"chunk_load_ms": 1675
}
}
},
{
"gpu": "NVIDIA GeForce RTX 4080",
"cpu": "13th Gen Intel Core i7-13700KF",
"render_distance": 32,
"game": "horizon",
"source": "us",
"machine": "Desktop (RTX 4080, i7-13700KF)",
"measured": "2026-10-23",
"version": "v2026.10.22.13",
"backend": "vulkan",
"os": "linux",
"window": [
1280,
720
],
"repeats": 3,
"load_avg_1m": 22.354,
"medians": {
"avg_fps": 672,
"low_1pct_fps": 89,
"p95_ms": 3.6,
"world_ms": 8999,
"chunk_load_ms": 4677,
"rss_peak_mb": 1957,
"rss_steady_mb": 1646,
"cpu_p50_ms": 1.1,
"gpu_p50_ms": 0.3
},
"scenes": {
"village": {
"avg_fps": 686,
"low_1pct_fps": 93,
"chunk_load_ms": 2702
},
"forest": {
"avg_fps": 590,
"low_1pct_fps": 76,
"chunk_load_ms": 4823
},
"ocean": {
"avg_fps": 868,
"low_1pct_fps": 113,
"chunk_load_ms": 3629
},
"mountain": {
"avg_fps": 544,
"low_1pct_fps": 73,
"chunk_load_ms": 7552
}
}
},
{
"gpu": "NVIDIA GeForce RTX 4080",
"cpu": "13th Gen Intel Core i7-13700KF",
"render_distance": 12,
"game": "vanilla",
"source": "us",
"machine": "Desktop (RTX 4080, i7-13700KF)",
"measured": "2026-10-23",
"version": "26.3",
"backend": "vulkan",
"os": "linux",
"window": [
1280,
720
],
"repeats": 3,
"load_avg_1m": 25.007,
"medians": {
"avg_fps": 2600,
"low_1pct_fps": 312,
"p95_ms": 0.5,
"world_ms": 30470,
"chunk_load_ms": 10837,
"rss_peak_mb": 3078,
"rss_steady_mb": 2883,
"cpu_p50_ms": null,
"gpu_p50_ms": null
},
"scenes": {
"village": {
"avg_fps": 2482,
"low_1pct_fps": 385,
"chunk_load_ms": 8003
},
"forest": {
"avg_fps": 2368,
"low_1pct_fps": 209,
"chunk_load_ms": 9654
},
"ocean": {
"avg_fps": 2727,
"low_1pct_fps": 247,
"chunk_load_ms": 10442
},
"mountain": {
"avg_fps": 2824,
"low_1pct_fps": 407,
"chunk_load_ms": 15249
}
}
},
{
"gpu": "NVIDIA GeForce RTX 4080",
"cpu": "13th Gen Intel Core i7-13700KF",
"render_distance": 32,
"game": "vanilla",
"source": "us",
"machine": "Desktop (RTX 4080, i7-13700KF)",
"measured": "2026-10-23",
"version": "26.3",
"backend": "vulkan",
"os": "linux",
"window": [
1280,
720
],
"repeats": 3,
"load_avg_1m": 21.652,
"medians": {
"avg_fps": 849,
"low_1pct_fps": 112,
"p95_ms": 3.1,
"world_ms": 58554,
"chunk_load_ms": 31819,
"rss_peak_mb": 3484,
"rss_steady_mb": 3419,
"cpu_p50_ms": null,
"gpu_p50_ms": null
},
"scenes": {
"village": {
"avg_fps": 881,
"low_1pct_fps": 110,
"chunk_load_ms": 19818
},
"forest": {
"avg_fps": 1072,
"low_1pct_fps": 165,
"chunk_load_ms": 28927
},
"ocean": {
"avg_fps": 894,
"low_1pct_fps": 142,
"chunk_load_ms": 19246
},
"mountain": {
"avg_fps": 551,
"low_1pct_fps": 33,
"chunk_load_ms": 59283
}
}
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2021 The Pixelify Sans Project Authors (https://github.com/eifetx/Pixelify-Sans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff. Show more