Retire the legacy Java optimization mod and performance packs

Preserve Java parity oracles and comparison benchmark instrumentation. Move their remaining dependency locks under tools/benchmark-deps and remove legacy product packaging, fixtures, and CI checks.

Co-Authored-By: OpenAI Codex gpt-6.1-sol <[email protected]>
This commit is contained in:
rdbtCVSandOpenAI Codex gpt-6.1-sol committed 2026-10-24 14:50:55 +01:00
1 parent 535f2d2a04
commit 06ec15f3a0
108 files changed
+232 -7261

No files matched your search

+1 -95
View File
@@ -430,7 +430,7 @@ jobs:
env:
RUSTUP_TOOLCHAIN: stable
run: python tools/test_float_log_parity.py
portable-mod:
jfr-analysis:
if: github.event_name == 'workflow_dispatch'
runs-on: [self-hosted, linux, horizon-game]
steps:
@@ -442,101 +442,7 @@ jobs:
with:
distribution: temurin
java-version: '25'
- name: Compile original mod and test metrics
run: python tools/build_mod.py
- name: Verify streaming JFR analysis against the JDK exporter
run: python tools/test_jfr.py
- name: Verify ZGC pause and allocation event interpretation
run: python tools/test_jfr.py --gc zgc
- name: Build pinned portable packs
run: python tools/build_pack.py
- name: Measure isolated worker pacing on this platform
run: python -c "from tools.build_mod import benchmark_workers; benchmark_workers()"
- name: Measure isolated debounce queue polling
run: python -c "from tools.build_mod import benchmark_debounce; benchmark_debounce()"
- name: Measure wakeable idle queue latency
run: python -c "from tools.build_mod import benchmark_idle; benchmark_idle()"
- name: Measure object allocations with compact headers off and on
run: python -c "from tools.build_mod import benchmark_headers; benchmark_headers()"
- name: Test pack integrity and comparisons
run: python -m unittest discover -s tests -v
- name: Start recommended OpenGL macOS clients and verify framebuffer readback
if: runner.os == 'macOS'
run: |
python tools/test_client_graphics.py --native-display
cp dist/client-graphics-smoke.json dist/client-graphics-macos-title.json
python tools/test_client_graphics.py --native-display --world
cp dist/client-graphics-smoke.json dist/client-graphics-macos-world.json
- name: Compile the private dispatch fixture
run: python -c "from tools.build_mod import build_dispatch_fixture; build_dispatch_fixture()"
- name: Compile the private integrated-server graphics fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('server-graphics', 'dev.horizonlab.metrics.ServerGraphicsIntegration', fixture_dependencies={})"
- name: Compile the private tick-manager state fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('tick-state', 'dev.horizonlab.metrics.TickStateIntegration')"
- name: Compile the private terrain tick membership fixture
run: python -c "from tools.build_mod import build_tick_dedup_fixture; build_tick_dedup_fixture()"
- name: Compile the private culling whitelist fixture
run: python -c "from tools.build_mod import build_culling_empty_fixture; build_culling_empty_fixture()"
- name: Compile the private culling ray geometry fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('culling-rays', 'dev.horizonlab.dispatch.CullingRayIntegration', ['entityculling'], ['CullingRayAccess'])"
- name: Compile the private terrain position lookup fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('terrain-position', 'dev.horizonlab.dispatch.PositionIntegration')"
- name: Compile the private terrain quad reuse fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('quad-reuse', 'dev.horizonlab.dispatch.QuadReuseIntegration')"
- name: Compile the private object-size agent and terrain quad fixture
run: python -c "from tools.test_quad_sizes import build_agent; from tools.build_mod import build_integration_fixture; build_agent(); build_integration_fixture('quad-size', 'dev.horizonlab.dispatch.QuadSizeIntegration')"
- name: Compile the private terrain read shutdown fixture
run: python -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('terrain-read', 'dev.horizonlab.dispatch.TerrainReadIntegration')"
- name: Install headless graphics libraries for applied-hook checks
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb libgl1 libegl1 libvulkan1 mesa-vulkan-drivers libasound2t64 libxkbcommon0
- name: Verify integrated-server graphics report against the actual device
run: python tools/test_server_graphics.py ${{ runner.os == 'macOS' && '--native-display' || '' }}
- name: Exercise the applied dispatcher hooks and upstream rejection path
if: runner.os == 'Linux'
run: python tools/test_dispatch.py
- name: Verify applied tick membership transitions and allocation
if: runner.os == 'Linux'
run: python tools/test_tick_dedup.py
- name: Verify live culling whitelist registration and empty iteration
if: runner.os == 'Linux'
run: python tools/test_culling_empty.py
- name: Compare applied culling ray geometry and provider queries
if: runner.os == 'Linux'
run: python tools/test_culling_rays.py
- name: Verify actual full-data position lookup equivalence
if: runner.os == 'Linux'
run: python tools/test_terrain_position.py
- name: Verify actual terrain quad geometry and allocation
if: runner.os == 'Linux'
run: python tools/test_quad_reuse.py
- name: Measure actual builder and quad shallow sizes in both header modes
if: runner.os == 'Linux'
run: python tools/test_quad_sizes.py
- name: Verify interrupted DH reads and preserved shared futures
if: runner.os == 'Linux'
run: python tools/test_terrain_read.py
- name: Upload original mod and pack manifests only
uses: actions/upload-artifact@v4
with:
name: horizon-linux
path: |
dist/*.mrpack
dist/horizon-*.jar
dist/worker-pacing-benchmark.txt
dist/debounce-queue-benchmark.txt
dist/idle-queue-benchmark.txt
dist/object-header-benchmark.txt
dist/dispatch-integration.json
dist/tick-dedup-integration.json
dist/culling-empty-integration.json
dist/terrain-read-integration.json
dist/culling-rays-integration.json
dist/terrain-position-integration.json
dist/quad-reuse-integration.json
dist/quad-size-integration.json
dist/server-graphics-smoke.json
dist/client-graphics-smoke.json
dist/client-graphics-macos-title.json
dist/client-graphics-macos-world.json
retention-days: 3
@@ -131,8 +131,8 @@ average) and reached the ThinkPad over the tailnet.
It ran with Paper's defaults except `connection-throttle: -1` in `bukkit.yml`. Every bot connects from one
address, and the default throttle (4 s) refused all but the first. Paperclip patched the pinned server jar
from its cache. Same Java and heap.
- Fabric + Lithium: the pinned server jar on Fabric loader 0.19.5 (`packs/fabric.lock.json`) with Lithium
`mc26.3-0.26.2-fabric` (`packs/mods.lock.json`, checked against its SHA-512). Lithium needs no Fabric API.
- Fabric + Lithium: the pinned server jar on Fabric loader 0.19.5 (`tools/benchmark-deps/fabric.lock.json`) with Lithium
`mc26.3-0.26.2-fabric` (`tools/benchmark-deps/mods.lock.json`, checked against its SHA-512). Lithium needs no Fabric API.
Same Java and heap.
The Paper and Fabric jars are dev-only downloads into `.local/server-bench/jars` and are never committed or
+1 -1
View File
@@ -48,4 +48,4 @@ To play, use [the launcher](../launcher/README.md); the project site is [playhor
## The earlier Java track (no longer developed)
- [Java performance packs](java-packs.md), with [Vulkan on your Mac](vulkan.md), [collector comparisons](collectors.md), [profiling](profiling.md), [process memory](process-memory.md) and [tick state](tick-state.md). The Fabric server starter is retired: [host a server](hosting.md) with horizon-server.
- [Retired Java performance packs](java-packs.md); [JFR analysis for Java comparisons](profiling.md).
+1 -1
View File
@@ -27,7 +27,7 @@ file selects ~103.
| 10 | Launcher (124) | `launcher/src/` (`main.rs`, `backend.rs`, `cli.rs`, `mcp.rs`, `servers_page.rs`) | `cd launcher && cargo test`; `python3 tools/launcher_mcp_e2e.py` |
| 11 | Mob types and species (117) | `L/mobs.rs`, `L/mobs/species.rs`, `L/mobs/data.rs`, `L/mobs/living.rs`, `L/mobs/drops.rs`, `L/mobs/gear.rs` | mob_attributes, entity_attribute, entity_death_loot, mob_ai |
| 12 | Guests' actions on a server (98) | `G/lan_actions.rs` (`fn as_guest`, `fn tick_guests`), `G/lan_{attributes,pvp,chat,crafting,stations,trading,mount_menu,waypoints}.rs` | server_player_combat, server_player_damage, server_player_targets, lan_e2e (all) |
| 13 | Modpack porting, mod content (97) | `crates/horizon-port-pack/`, `L/mod_content.rs`, `tools/port_*.py`, `packs/` | `cargo test -p horizon-port-pack`; `python3 tools/mod_port_harness.py` |
| 13 | Modpack porting, mod content (97) | `crates/horizon-port-pack/`, `L/mod_content.rs`, `tools/port_*.py` | `cargo test -p horizon-port-pack`; `python3 tools/mod_port_harness.py` |
| 14 | Mob AI (96) | `L/mobs/ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `control.rs` | mob_ai (probe `examples/mob_ai_probe.rs`; known failure on main: drowned_*, #290) |
| 15 | Items and item use (96) | `L/items.rs`, `L/item_data.rs`, `G/hand.rs`, `G/consume.rs`, `G/potions.rs`, `L/effects.rs`; game.rs `fn use_hand`, `fn use_item` | use_item_on, item_use, item_use_lifecycle, consumable, consume_effect, status_effect |
| 16 | Options and settings screens (93) | `L/options_ui.rs`, `G/options.rs`, `L/settings.rs`, `L/options_file.rs`, `L/graphics_ui.rs`, `L/graphics.rs` | options, toggle_keys |
+1 -1
View File
@@ -21,7 +21,7 @@ One game is started per window size (1280x720 and 1920x1080 by default) and rend
- **Warm-up excluded.** Each scene's flight starts only after its chunks are loaded and drawn and 2 seconds have passed; the load itself is reported separately. JIT warm-up of the JVM during the first scene is part of vanilla's real behaviour and is not removed.
- **The same statistics code.** Horizon returns its raw frame times (`horizon-ctl fly ... raw`) and the vanilla mod writes its own; `stats()` in the driver computes every figure for both.
- **Vanilla as players run it.** Your own 26.3 client jar and libraries (as the official launcher installs them for your OS), the official launcher's default JVM arguments (`-Xmx2G` and its G1 options), Java 25. The game's assets index is not used, so neither game plays sound (Horizon runs with `HORIZON_NO_AUDIO=1`, vanilla with OpenAL's null device); textures, models and shaders come from the client jar as usual.
- **Performance mods.** `--game vanilla-mods` adds the Fabric builds for 26.3 of Sodium, Lithium, FerriteCore and ImmediatelyFast, the versions pinned with their hashes in `packs/mods.lock.json` (downloaded from Modrinth, never committed), plus Fabric API if one of them needs it.
- **Performance mods.** `--game vanilla-mods` adds the Fabric builds for 26.3 of Sodium, Lithium, FerriteCore and ImmediatelyFast, the versions pinned with their hashes in `tools/benchmark-deps/mods.lock.json` (downloaded from Modrinth, never committed), plus Fabric API if one of them needs it.
## Known caveats
-48
View File
@@ -1,48 +0,0 @@
# Compare Java collectors
Use the balanced pack first with the launcher's existing JVM policy. Java 25
HotSpot supports explicit G1 (`-XX:+UseG1GC`) and ZGC (`-XX:+UseZGC`) selections;
the [Java command reference](https://docs.oracle.com/en/java/javase/25/docs/specs/man/java.html)
documents both. A collector choice can change CPU, heap headroom, collection
behavior and frame pacing. Lower pauses alone do not establish lower RAM or
higher FPS. The packs do not automatically select a collector.
For a Mac comparison, duplicate one instance. In Edit > Settings > Java, enable
the Java Arguments override checkbox. Preserve existing non-collector arguments,
but use exactly one collector selector in each instance. Choose G1 in one and
ZGC in the other. Use the same native Java 25 ARM64, maximum/minimum heap, compact
header setting, world copy, resolution, shaders, distances and warmed camera route.
Alternate at least three 120-second runs rather than comparing unrelated captures.
Remove the explicit selector to restore the launcher's policy, then restart.
The viewer shows the actual collector names captured by the game. Check them:
setting a launcher's text field does not establish which flags reached the VM.
Prism's per-instance Java Arguments and Memory settings have separate override
checkboxes. A global collector selection may need removal or replacement when
creating an explicit per-instance experiment.
Record FPS, slowest-1% rate, frame spikes, heap and process RSS together. RSS does
not represent all shared GPU memory on Apple Silicon. `gc_time_ms` sums collector
MXBean counters; concurrent cycles and overlapping phases can contribute, so it
is not a measured pause total. Use JFR root pause events for pause analysis.
The streaming reader is checked against the JDK's event exporter with both
default-GC and ZGC fixtures in CI on Windows, Linux and ARM macOS.
The developer demo launcher accepts `--gc default`, `--gc g1` or `--gc zgc`.
For example:
```sh
python3 tools/runtime.py demo --instance .local/runtime/collector-test --heap 6G --gc zgc --compact-headers
python3 tools/test_jfr.py --gc zgc
```
Prepare demo mods/worlds before comparing.
Options affect only the launched process; they do not change global Java settings.
The collector and compact-header experiments can be tested independently.
A local 3 GiB, compact-header ZGC client completed a 120-second game recording.
That recording also encountered a distant-terrain rejection storm. Its workload
and cache history differed from G1 captures, so the larger memory footprint and
different frame rate cannot be attributed to the collector alone. The dispatcher
experiment addresses the identified retry loop separately. No M2 collector
performance winner is established.
+12 -106
View File
@@ -1,109 +1,15 @@
# Java performance packs (no longer developed)
# Retired Java performance packs
Before the Rust rewrite, Horizon was a lab for Minecraft Java client performance: pinned Fabric performance packs, distant terrain, an original frame and tick recorder, and client and server starter downloads (the server starter is gone: [horizon-server](hosting.md) replaces it). This track is **no longer developed**. No release downloads are published; build the packs and starters locally with `python3 tools/build_release.py` (see [Build and run](#build-and-run)). This page keeps its documentation.
Before the Rust rewrite, Horizon shipped a Fabric optimization mod, performance pack profiles
and a Prism Launcher Mac starter. That product is retired. Its patches, profiles, builders,
starter, benchmark viewer and product-specific checks have been removed; use Git history
for the former implementation. Horizon's Rust game does not use those Java patches.
## Java packs: Mac starter (out of scope)
Build **horizon-mac-starter-0.21.0.zip** with `python3 tools/build_release.py` (it lands in `dist/`), extract it and open `Mac-Start.command`. It verifies and installs official universal Prism Launcher in `~/Applications` if needed, then opens the balanced pack import. Sign into Microsoft **inside Prism yourself**. This project needs no credentials.
Use native **Java 25 ARM64**, maximum heap **6144 MiB**, minimum **512 MiB** on a 16 GB M2 Pro Mac. Prism can download Java automatically. Start at 1280×800 or 1920×1200; full Retina resolution is a larger GPU workload. [START-HERE.txt](../packs/START-HERE.txt) has details. The same `.mrpack` files import into Prism on Windows and Linux.
Core and balanced passed macOS title and demo-world rendering probes on the hosted
ARM64 software renderer, including readback, saves and clean exits. Physical M2
GPU performance and macOS 27 were never tested.
| Profile | Full-detail chunks | Distant LOD radius | Suggested maximum heap |
| --- | ---: | ---: | ---: |
| baseline | 12 | none | 6144 MiB |
| core-performance | 12 | none | 6144 MiB |
| m2-balanced | 12 | 128 | 6144 MiB |
| adaptive (experiment) | 12 | 128 | 6144 MiB |
| worker-pacing (experiment) | 12 | 128 | 6144 MiB |
| adaptive-pacing (experiment) | 12 | 128 | 6144 MiB |
| queue-wait (experimental) | 12 | 128 | 6144 MiB |
| adaptive-queue (experimental) | 12 | 128 | 6144 MiB |
| dispatch-retry (experimental) | 12 | 128 | 6144 MiB |
| tick-dedup (experimental) | 12 | 128 | 6144 MiB |
| culling-empty (experimental) | 12 | 128 | 6144 MiB |
| quad-reuse (experimental) | 12 | 128 | 6144 MiB |
| terrain-position (experimental) | 12 | 128 | 6144 MiB |
| culling-rays (experimental) | 12 | 128 | 6144 MiB |
| foreground-first (combined experiment) | 12 | 128 | 6144 MiB |
| foreground-vulkan (combined experiment) | 12 | 128 | 6144 MiB |
| low-memory | 8 | none | 3072 MiB |
| far-distance | 12 | 256 | 8192 MiB, with sufficient physical RAM |
| cinematic | 12 | 128 | 6144 MiB |
| vulkan | 12 | 128 | 6144 MiB |
Balanced combines [Sodium](https://modrinth.com/mod/sodium), [Lithium](https://modrinth.com/mod/lithium), [FerriteCore](https://modrinth.com/mod/ferrite-core), [ImmediatelyFast](https://modrinth.com/mod/immediatelyfast), [EntityCulling](https://modrinth.com/mod/entityculling), and [Distant Horizons](https://modrinth.com/mod/distanthorizons). Cinematic adds [Iris](https://modrinth.com/mod/iris) and [Complementary Reimagined](https://modrinth.com/shader/complementary-reimagined), initially disabled. Vulkan independently experiments with the game's own backend and fallback, without Iris.
Distant LOD is simplified terrain. Nearby blocks retain full detail. Initial generation costs CPU/disk/memory and needs warm-up. DH adjusts clouds, chunk fade and transparency to blend its renderer; assess appearance as well as speed. Shaders cost GPU time and have [Mac-specific limits](https://shaders.properties/current/reference/miscellaneous/macos/). Universal CPU/GPU support and exact vanilla equivalence remain validation goals. Simulation distance stays 12 in every profile; normal 20 TPS rules are retained.
For the first comparison at the same view distance, use `baseline` versus
`core-performance`: both select 12 full-detail chunks and no distant LOD or shaders.
The latter adds only the pinned core performance mods and leaves every original
controller disabled. Then test balanced separately for its additional distant
terrain. Matching captured settings does not prove pixel-identical rendering.
The profiles marked *experiment* each isolate one change to Distant Horizons,
EntityCulling or the JVM (adaptive terrain throttling, precise worker pacing,
debounce-queue waits, rejected-dispatch retries, empty culling whitelists,
culling ray scratch vectors, terrain quad reuse, terrain position arithmetic,
tick membership storage, a smaller heap, compact object headers). Each kept the
upstream behaviour it replaced and was measured only on Linux software rendering,
where none showed a gain large enough to enable by default; balanced keeps them
all off. Their write-ups were removed once the Rust rewrite replaced this track;
they remain in Git history. Matched software-rendered pairs showed `core-performance`
14–21% faster than `baseline`, and Vulkan about 1.5% more FPS for about 12% more
CPU; neither is evidence for a Mac GPU.
To try the game's own Vulkan backend on a Mac, see [Vulkan on your Mac](vulkan.md);
to compare garbage collectors, see [collector comparisons](collectors.md).
## Measure on your hardware
Press **F8** during gameplay to start a 120-second recording, or again to stop early. Rebind in Controls if needed. This also works with restricted chat accounts. Alternatively: `/horizon bench <label> 120`, `/horizon stop`, `/horizon status`.
Use separate identical world copies and the same camera route, resolution, render/simulation distance, heap, VSync and FPS limit. Warm up, then alternate at least three baseline/optimized runs. Test fresh chunk exploration separately. For everyday play cap FPS or enable VSync to reduce power usage.
The instance's `horizon-benchmarks/` folder receives JSON/CSV frame and integrated-server tick timings: average FPS, slowest-1%-average FPS, p95/p99 durations, stalls, JVM heap/GC, sampled process RSS when available, GPU/backend/driver and render settings. Frames include presentation/limiting waits and exclude menus/pauses/unfocused time. Tick `meanRate` is execution capacity, **not TPS**; observed tick counts are separate. Version 0.20 [captures sampled tick-manager state](tick-state.md), including restored freeze/step/sprint/rate changes; this does not measure completed world work. Resident process memory is sampled on a shared background thread only during recordings; it includes native resident pages, while heap measures JVM objects. Neither is a complete measurement of unified GPU memory. [Memory measurement details](process-memory.md).
```sh
python3 tools/compare_benchmarks.py baseline-frames.json optimized-frames.json --strict
```
Open `Compare-Benchmarks.html` from the starter ZIP or [the source file](../tools/Compare-Benchmarks.html) in your browser to compare without Python. Select each JSON and optionally its matching CSV. It exports summary JSON, flags settings and duration differences, and charts frames by recorded active time or ticks by sample number. It is self-contained and makes no network requests.
The comparison flags captured setting differences, but cannot verify route, world, shader state or cache warm-up. Software-renderer test FPS is not evidence of a Mac/GPU speedup.
For CPU/allocation investigation, [capture and stream a private JFR summary](profiling.md).
It distinguishes directly observed allocation counters from sampled hot methods;
profiling runs remain separate from normal timing comparisons.
## Build and run
Requires Python 3.10+ and JDK 25. No Gradle, Rust or native compilation is required for playable packs.
```sh
python3 tools/build_mod.py
python3 tools/build_release.py
```
Gitignored `dist/` contains twenty packs, our original instrumentation JAR, the client starter ZIP and SHA256SUMS. [mods.lock.json](../packs/mods.lock.json) pins mod versions, exact dependency closure, SHA-1/SHA-512 and source/license; [fabric.lock.json](../packs/fabric.lock.json) pins loader/library SHA-256. Packs use the [Modrinth format](https://support.modrinth.com/en/articles/8802351-modrinth-modpack-format-mrpack). They embed only original project code; third-party mods, game and assets download from their publishers.
The Fabric server starter (Lithium/FerriteCore, `tools/server.py`) was retired on 2026-10-19. To host a server, use [horizon-server](hosting.md) ([playhorizon.dev/server](https://playhorizon.dev/server)); its benchmarks are in [server.md](server.md), section 9.
Local smoke testing uses the game's built-in limited demo; normal play uses Prism's licensed account authentication:
```sh
python3 tools/build_pack.py --profile m2-balanced --instance .local/runtime/m2-balanced
vscreen run python3 tools/runtime.py demo --instance .local/runtime/m2-balanced
```
The standalone demo launcher applies the pinned official manifest's conditional
JVM flags, including macOS `-XstartOnFirstThread` and the Windows heap-dump-path
option. Prism applies its own official manifest. Server JVM policy is separate;
this launcher correction is not a measured graphics performance improvement.
The Java parity oracles in `mods/horizon/src/test/java/dev/horizonlab/parity/` remain part of
[the Rust parity workflow](parity-workflow.md). The Fabric and NeoForge benchmark source sets
also remain: [comparison benchmarks](benchmark.md) still run real Java Minecraft against Rust.
Their pinned third-party dependencies live in `tools/benchmark-deps/`; they do not include the
retired Horizon optimization mod. [JFR analysis](profiling.md) remains available for Java comparisons.
For the current game, see [build from source](../README.md#build-from-source).
For server hosting, use [horizon-server](hosting.md).
+1 -1
View File
@@ -49,7 +49,7 @@ Source: [FerriteCore's summary of its changes](https://github.com/malte0811/Ferr
| String deduplication in model locations | Duplicate `String`s from the constructor's round-trips | No `ModelResourceLocation` objects, so the round-trip doesn't exist | **n/a** |
| Partly loaded chunk NBT kept between loading steps | The whole tag is kept | Chunks are decoded into Horizon's own structures | **n/a** |
Horizon's process memory is tracked against vanilla in `docs/process-memory.md`.
Horizon's process memory is tracked against vanilla in [comparison benchmarks](../benchmark.md).
## ImmediatelyFast (client draw submission)
-53
View File
@@ -1,53 +0,0 @@
# Measuring resident process memory
Horizon 0.6.0 records process RSS alongside JVM heap and nonheap pools. RSS
counts resident pages belonging to the Minecraft process, including native
allocations and mapped pages. It helps detect cases where reducing heap use
merely moves memory elsewhere. It is not unique physical-memory ownership or
a complete accounting of GPU allocations, shared pages, compressed memory,
swap, or Apple Silicon unified memory. Do not add heap to RSS: heap pages can
already be included in it.
Minecraft 26.3 already ships OSHI 6.9.0 and JNA on both client and server. The
original mod uses those dependencies without embedding them. Native reader
initialization and refresh happen on a low-priority daemon thread, never on
the render or server-tick thread. One reader serves overlapping frame and
tick recordings. Reads run about once per second only while recordings are
active; the scheduler stops polling when the last recording finishes. Ordinary
gameplay without a benchmark performs no resident-memory reads.
Reports include these optional schema-1 fields:
| Field | Meaning |
| --- | --- |
| `process_rss_peak_sampled_bytes` | Largest successful resident-memory sample during this recording |
| `process_rss_last_sampled_bytes` | Last successful sample, which can precede recording completion |
| `process_rss_samples` | Number of successful samples delivered to this recording |
| `process_rss_sample_interval_ms` | Requested 1,000 ms interval; reads can take longer |
| `process_rss_last_sample_age_ms` | Time from the last successful sample to report completion |
Unsupported or failed reads leave these fields absent if no sample succeeded;
zero is not used as a substitute. Frames and ticks continue recording. A read
that completes after a recording finishes is ignored. Sampled peaks can miss
brief spikes, and the initial read can be delayed by native-library startup.
The offline viewer compares RSS separately from heap/nonheap and warns about
stale resident samples. Keep recording duration, workload and warm-up matched
when comparing peaks. Process CPU counters cover the whole process over the
recording wall time, including native work and the integrated server; they do
not identify which thread or mod caused CPU use.
`gc_time_ms` sums collector MXBean collection-time counters. With concurrent
collectors these can include cycle duration and overlapping phases; it is not a
direct stop-the-world pause measurement. The viewer calls it collection time.
Use [JFR pause events](profiling.md) when investigating GC-induced stalls.
A three-pair heap experiment compared 3 GiB and 1.5 GiB
maximum heaps with the same core stack. It recorded variable RSS savings, nearly
unchanged average FPS and increased GC accounting; playable defaults stayed
unchanged.
The native-read test exercises the real library, overlapping recordings,
background execution, idle cancellation and unavailable-reader behavior on
the build platform. This validates measurement availability, not an FPS or
RAM optimization result.
+4 -12
View File
@@ -1,7 +1,7 @@
# Find the next bottleneck
Frame and tick recordings describe player-visible timings. A private Java Flight
Recorder capture helps identify the code doing work or allocating objects. Use
For Java comparison runs, a private Java Flight Recorder capture helps identify
the code doing work or allocating objects. Use
native Java 25, with `jcmd` and `java` from a JDK. Keep normal benchmark runs separate
from profiling runs because sampling adds overhead.
@@ -53,8 +53,7 @@ allocated bytes or proving that one object type owns most of the allocation.
GC pause totals/maximum use top-level `jdk.GCPhasePause`, excluding nested phases
and concurrent collection-cycle duration. `gc_cycles` is a separate count.
`heap_used_peak_at_gc_bytes` considers recorded GC heap summaries and can miss
peaks between collections. It is separate from the game's once-per-second heap
and process-RSS measurements.
peaks between collections. It is separate from externally sampled process RSS.
The reader ignores unselected event types and never extracts properties or
arguments. `event_span_seconds` is the span of all observed event timestamps,
@@ -70,12 +69,5 @@ python3 tools/test_jfr.py
This creates a real JDK recording of bounded allocations and GC, then compares
the reader's counter differences, observation counts, GC counts and heap summaries
against `jfr print --json`. It also verifies escaped thread names and that sampled
weights do not appear as numeric allocation totals. CI runs it on Windows, Linux
and Apple Silicon macOS. The reader was independently checked against the older
weights do not appear as numeric allocation totals. The manual `jfr-analysis` CI job runs both collector fixtures on the Linux game runner. The reader was independently checked against the older
real-game profile's JDK JSON export for 85 threads.
The post-queue software-rendered profile confirms remaining allocations on the
integrated-server and chunk-meshing threads. It is not a matched comparison with
the older capture: world/cache state, heap and object-header flags differ. It
cannot establish M2 frame gains. Mac captures are needed before choosing a larger
renderer or native-kernel replacement.
+3 -4
View File
@@ -177,7 +177,6 @@ To be written as `docs/porting-from-fabric.md` once API 0.2 lands:
- NeoForge documentation (`docs.neoforged.net`): events, registries,
networking.
- Modrinth's most-downloaded Fabric mods, for the porting list.
- Horizon's own Java test mod (`mods/horizon`, built by `tools/build_mod.py`
and the CI `portable-mod` job) is a Fabric mod used by the benchmarks and
parity oracles: it shows the Mixin pattern (`FrameHookMixin`) this page
describes, and stays a developer tool, not a player-facing mod.
- Horizon's Fabric benchmark instrumentation (`mods/horizon/src/bench/`) remains
a developer tool for Java comparisons. The former Java optimization mod is
[retired](../java-packs.md); the parity oracles remain in `mods/horizon/src/test/`.
+1 -1
View File
@@ -467,7 +467,7 @@ reproducible by anyone, and show the same gameplay on both sides.
- Horizon (`horizon-server`);
- vanilla 26.3 (the owner's `server.jar`);
- Paper (a jar the owner downloads; Paper's defaults are kept, and noted as non-vanilla);
- optionally Fabric + Lithium (the old starter's setup, via the pinned `packs/mods.lock.json`).
- optionally Fabric + Lithium (via the pinned `tools/benchmark-deps/mods.lock.json`).
Java servers run on Java 25 with `-Xms` = `-Xmx` (the same heap for all, 4 GB by default), and the heap is
reported.
-26
View File
@@ -1,26 +0,0 @@
# Interpreting measured server ticks
Tick execution time omits scheduler sleep. `stats.meanRate` is the inverse average duration, not actual TPS. `observed_ticks_per_second` already counts accepted plus dropped measured invocations divided by wall-clock recording duration. Frozen tick invocations count; paused wall time stays in the denominator. Neither number measures completed simulation work.
Version 0.20 observes the server tick manager after each measured invocation on the server thread. Reports record the first and last sampled configured rate, frozen/stepping/sprinting flags and `runs_game_elements`, plus counters for samples with each nonstandard state. This is read-only instrumentation: it does not set a tick rate, freeze worlds or change game rules. Frame reports do not claim to sample server state; use their accompanying integrated-server tick reports.
The recorder retains which state fields changed between samples. A 20 → 40 → 20 rate change or freeze → unfreeze sequence stays visible even when first and last states match. Both comparison tools mark such recordings as changed workloads, mark differing sampled boundary states, and warn about frozen, stepping, sprinting, suppressed-game-element and nonstandard-rate samples. A consistently configured custom rate can match another report at the same rate, with an explicit warning about that workload. Older, absent or invalid state metadata remains unknown, rather than normal by assumption.
State and tick duration are accepted under the same recording lock, including buffer overflow. The observation path retains scalar values and allocates no maps or lists per invocation; report serialization creates them once. Non-finite or sub-minimum rates are counted as invalid observations without aborting duration capture. The state object is only allocated for tick recordings.
Sampling has limits. These are first/last sampled server states, not exact recording boundary states. Changes entirely between samples can be missed. An integrated server paused by the game menu does not call the measured superclass method, so state counters do not enumerate paused time. The manager flag also does not establish that every entity/chunk was simulated, or detect all other reasons a server skips work. A final stepping tick may already have decremented the remaining-step counter to zero while `runs_game_elements` is still true. Compare world copies, entity populations, completed work and real player workloads separately.
## Validation
Pure recording checks exercise restored intermediate changes, distinct frozen/suppressed states, late observations, absent/invalid manager state and wall-clock TPS calculation under G1, compact headers and ZGC. Offline comparison checks cover map ordering, changed/custom states and legacy/malformed input. The desktop and 390-pixel viewer checks also load the actual discarded paused capture (12 ticks over approximately 120 seconds), display its roughly 0.1 observed TPS and distinguish it from execution capacity. That capture remains excluded from performance evidence.
The private `TickStateIntegration` fixture runs only on a disposable demo-world copy. It starts a short recording, samples normal ticking, changes to 40 configured ticks/second and freezes, steps ten ticks, restores 20/unfrozen state, and requests a five-tick sprint. It checks that sampled boundary states match, all five intermediate state changes/counters survive, and accepted state/duration counts agree. The fixture is compiled in platform CI and excluded from the production JAR and downloadable packs. The [final local applied-hook observation](../benchmarks/tick-state-2026-10-04/live-observations.json) records 285 invocations, with 161 frozen, 9 stepping, 5 sprinting, 151 suppressed-game-element and 161 nonstandard-rate samples. All five change fields survive restored boundary states; CSV count/mean and zero invalid/drop observations are verified, and the client exits with code zero. This is functional evidence rather than a performance comparison. Compilation is not a live Mac result.
Compile the private fixture after building the original mod:
```sh
python3 tools/build_mod.py
python3 -c "from tools.build_mod import build_integration_fixture; build_integration_fixture('tick-state', 'dev.horizonlab.metrics.TickStateIntegration')"
```
Install `.local/mod-build/tick-state-fixture.jar` only into a disposable locally prepared balanced demo instance. Open its copied demo world and dismiss the demo screen. The fixture validates the report and closes the client after its sequence. Never add it to a normal play instance or starter package.
-32
View File
@@ -1,32 +0,0 @@
# Compare the Vulkan renderer on your Mac
The `vulkan` profile asks Minecraft 26.3 to use its existing Vulkan backend, with
its original OpenGL fallback. It uses balanced mods/distances and omits Iris.
The game renderer handles the backend; this project has not written a separate
Metal renderer. The benchmark records the actual backend and driver.
The checksum-verified 26.3 manifest includes LWJGL Vulkan 3.4.3 Mac ARM64 natives.
Inspecting that official native JAR confirmed `libMoltenVK.dylib`; its paired
LWJGL loader first tries MoltenVK on macOS. [MoltenVK](https://github.com/KhronosGroup/MoltenVK)
implements Vulkan functionality over Apple's Metal framework. This verifies a
bundled route to Metal, rather than proving that a particular Mac backend starts
or performs better. No separately installed Vulkan SDK is required for the bundled
library; driver/backend compatibility still needs a real Mac test.
Import `m2-balanced` and `vulkan` as separate Prism instances. Keep Java 25 ARM64,
heap, header/collector flags, world copies, resolution, view/simulation distance
and camera route equal. Warm terrain and shader/pipeline caches before comparing.
Run at least three alternating 120-second F8 recordings. Inspect nearby blocks,
distant terrain, transparency, water, lighting and reloads as well as timings.
Use the viewer's actual backend field: OpenGL fallback is an OpenGL result.
After the isolated test works, `foreground-first` and `foreground-vulkan` compare
the two backends with the same combined terrain controls. Their 120 FPS adaptive
goal controls background duty cycle; rendering remains uncapped in pack options.
Do not add Iris or shaders to one side of an isolated backend comparison.
Linux software rendering checks reached the actual Vulkan backend, rendered the
demo world and recorded matching CSV/JSON frame/tick samples. An unavailable
Vulkan-driver test reached the menu using OpenGL fallback. These tests do not
validate MoltenVK, real GPU speeds, all visual details or macOS 27. The user's
M2 Pro remains the priority device for those checks.
@@ -1,78 +0,0 @@
package dev.horizonlab;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import dev.horizonlab.metrics.Recording;
import dev.horizonlab.metrics.Reports;
import dev.horizonlab.metrics.ProcessMemoryMonitor;
import dev.horizonlab.metrics.ResidentMemory;
import java.nio.file.Path;
import java.util.Map;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.permissions.Permissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class HorizonLab implements ModInitializer {
public static final Logger LOGGER = LoggerFactory.getLogger("horizonlab");
private static volatile Recording ticks;
@Override public void onInitialize() {
ProcessMemoryMonitor.configure(new ResidentMemory());
CommandRegistrationCallback.EVENT.register((dispatcher, context, selection) -> dispatcher.register(
Commands.literal("horizonserver")
.requires(source -> source.permissions().hasPermission(Permissions.COMMANDS_ADMIN))
.then(Commands.literal("bench")
.then(Commands.argument("label", StringArgumentType.word())
.then(Commands.argument("seconds", IntegerArgumentType.integer(10, 600))
.executes(command -> {
boolean started = startTicks(StringArgumentType.getString(command, "label"),
IntegerArgumentType.getInteger(command, "seconds"), Path.of("horizon-benchmarks"), "server",
command.getSource().getServer().isDedicatedServer() ? "dedicated" : "integrated", Map.of());
command.getSource().sendSuccess(() -> Component.literal(started
? "Horizon: recording tick times; run the intended workload now."
: "Horizon: a tick recording is already active."), false);
return started ? 1 : 0;
}))))
.then(Commands.literal("stop").executes(command -> { finishTicks(); return 1; }))));
LOGGER.info("Horizon ready. Simulation rules and tick rate are unchanged.");
}
public static synchronized boolean startTicks(String label, int seconds, Path output, String profile) {
return startTicks(label, seconds, output, profile, "unknown", Map.of());
}
public static synchronized boolean startTicks(String label, int seconds, Path output, String profile,
String runtimeKind, Map<String, ?> clientContext) {
if (ticks != null) return false;
Map<String, Object> context = dev.horizonlab.metrics.SoftwareContext.capture();
context.put("profile", profile);
context.put("minecraft", "26.3");
context.putAll(dev.horizonlab.metrics.ServerContext.capture(runtimeKind, clientContext));
ticks = new Recording(label, "ticks", output, seconds, context);
return true;
}
public static void tick(long durationNs, MinecraftServer server) {
Recording recording = ticks;
if (recording == null) return;
var tickRate = server.tickRateManager();
recording.addTick(durationNs, server.getPlayerList().getPlayerCount(), tickRate.tickrate(), tickRate.isFrozen(), tickRate.isSteppingForward(),
tickRate.isSprinting(), tickRate.runsNormally());
if (System.nanoTime() >= recording.deadlineNs) finishTicks();
}
public static synchronized void finishTicks() {
Recording recording = ticks;
if (recording == null) return;
ticks = null;
Reports.write(recording.finish()).whenComplete((path, error) -> {
if (error != null) LOGGER.error("Tick report failed", error);
else LOGGER.info("Horizon tick benchmark saved to {}", path);
});
}
}
@@ -1,14 +0,0 @@
package dev.horizonlab.budget;
/** Wait only while an unchanged debounce deadline blocks progress. */
public final class DebounceWait {
public static final long MAX_WAIT_NS = 1_000_000L;
private DebounceWait() {}
public static long delay(long now, long firstQueued, long debounce, boolean preprocessingPending) {
if (preprocessingPending || debounce <= 0) return 0;
long age = now - firstQueued;
if (age < 0 || age >= debounce) return 0;
return Math.min(MAX_WAIT_NS, debounce - age);
}
}
@@ -1,14 +0,0 @@
package dev.horizonlab.budget;
/** Per-picker state, used only while its existing dispatch lock is held. */
public final class DispatchRetry {
private boolean rejectedExecutor;
private boolean rejectedPass;
public void beginPass() { rejectedExecutor = false; rejectedPass = false; }
public void beginExecutor() { rejectedExecutor = false; }
public void rejected() { rejectedExecutor = true; rejectedPass = true; }
public int visibleSlots(int occupied) { return rejectedExecutor ? Integer.MAX_VALUE : occupied; }
public boolean needsRetry(boolean original, boolean shutdown) { return !shutdown && (original || rejectedPass); }
public long retryMilliseconds(long original) { return rejectedPass ? 1 : original; }
}
@@ -1,38 +0,0 @@
package dev.horizonlab.budget;
/** Foreground-first background duty cycle. Inputs are two-second observation windows. */
public final class TerrainBudget {
private double ratio;
private int healthyWindows;
public TerrainBudget(double ceiling) { ratio = ceiling; }
public double update(double ceiling, double meanFrameMs, double targetFrameMs,
double lateFraction, double heapFraction) {
if (!Double.isFinite(ceiling) || ceiling < 0.01 || ceiling > 1.0
|| !Double.isFinite(meanFrameMs) || meanFrameMs < 0
|| !Double.isFinite(targetFrameMs) || targetFrameMs <= 0
|| !Double.isFinite(lateFraction) || lateFraction < 0 || lateFraction > 1
|| !Double.isFinite(heapFraction) || heapFraction < 0 || heapFraction > 1) {
throw new IllegalArgumentException("Invalid budget observations");
}
double floor = Math.min(ceiling, 0.05);
ratio = Math.min(ratio, ceiling);
boolean pressure = meanFrameMs > targetFrameMs * 1.35 || lateFraction > 0.20 || heapFraction > 0.85;
boolean healthy = meanFrameMs < targetFrameMs * 1.10 && lateFraction < 0.05 && heapFraction < 0.75;
if (pressure) {
healthyWindows = 0;
ratio = Math.max(floor, ratio * 0.65);
} else if (healthy) {
if (++healthyWindows >= 3) {
ratio = Math.min(ceiling, ratio + 0.025);
healthyWindows = 0;
}
} else {
healthyWindows = 0;
}
return ratio;
}
public double ratio() { return ratio; }
}
@@ -1,27 +0,0 @@
package dev.horizonlab.budget;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
/** Allow the pinned DH generation pool's shutdown interrupt to release a read-only wait. */
public final class TerrainReadWait {
private TerrainReadWait() {}
public static <T> T join(CompletableFuture<T> future) {
// Ordinary game callers retain join's interrupt behavior. Completed reads use the original path.
if (future.isDone() || !Thread.currentThread().getName().startsWith("DH-World Gen Thread["))
return future.join();
try {
return future.get();
} catch (InterruptedException shutdown) {
Thread.currentThread().interrupt();
// DH recognizes this cause as shutdown. The cached future belongs to other callers too.
throw new CompletionException(shutdown);
} catch (ExecutionException | CancellationException failure) {
// Now completed: retain join's original exception identity and wrapping semantics.
return future.join();
}
}
}
@@ -1,15 +0,0 @@
package dev.horizonlab.budget;
import java.util.concurrent.locks.LockSupport;
/** A bounded idle park, preserving Thread.sleep's InterruptedException/cleared-flag contract. */
public final class WakeableIdleWait {
private WakeableIdleWait() { }
public static void pause(Object blocker, long nanoseconds) throws InterruptedException {
if (nanoseconds < 0) throw new IllegalArgumentException("Negative idle wait");
if (Thread.interrupted()) throw new InterruptedException();
LockSupport.parkNanos(blocker, nanoseconds);
if (Thread.interrupted()) throw new InterruptedException();
}
}
@@ -1,36 +0,0 @@
package dev.horizonlab.budget;
/** Per-worker primitive state; amortizes short waits without discarding fractional milliseconds. */
public final class WorkerPacer {
public static final long QUANTUM_NS = 250_000;
public static final long MAX_CREDIT_NS = 50_000_000;
private long started;
private long completed;
private double debtNs;
private double previousRatio = 1;
public void begin(long now) {
if (completed != 0 && now - completed > 1_000_000_000L) debtNs = 0;
started = now;
}
public long finish(long now, double ratio) {
long workNs = Math.max(0, now - started);
completed = now;
if (ratio != previousRatio) debtNs = 0;
previousRatio = ratio;
if (!Double.isFinite(ratio) || ratio <= 0 || ratio >= 1) {
debtNs = 0;
return 0;
}
debtNs += workNs * (1.0 / ratio - 1.0);
return debtNs < QUANTUM_NS ? 0 : (long) Math.min(debtNs, Long.MAX_VALUE / 4.0);
}
public void pauseCompleted(long now) {
debtNs = Math.max(-MAX_CREDIT_NS, debtNs - Math.max(0, now - completed));
completed = now;
}
public void interrupted(long now) { debtNs = 0; completed = now; }
}
@@ -1,74 +0,0 @@
package dev.horizonlab.client;
import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import dev.horizonlab.HorizonLab;
import dev.horizonlab.budget.TerrainBudget;
/** Loaded only when DH is installed. Overrides are temporary, never written to its TOML. */
final class DistantTerrainBudget {
private IDhApiConfigValue<Double> setting;
private TerrainBudget policy;
private Double owned;
private boolean yielded;
private long windowStart;
private long totalNs;
private int count;
private int late;
private int changes;
void observe(long now, long frameNs, double targetMs) {
if (yielded || DhApi.Delayed.configs == null) return;
if (setting == null) {
setting = DhApi.Delayed.configs.multiThreading().threadRuntimeRatio();
if (!setting.getCanBeOverrodeByApi() || setting.getApiValue() != null) {
yielded = true;
HorizonLab.LOGGER.info("Adaptive terrain budget yielded to an existing DH override");
return;
}
policy = new TerrainBudget(setting.getTrueValue());
}
if (!java.util.Objects.equals(setting.getApiValue(), owned)) {
yielded = true;
owned = null;
HorizonLab.LOGGER.info("Adaptive terrain budget yielded after another DH override changed");
return;
}
if (windowStart == 0) windowStart = now;
if (frameNs > 0) {
totalNs += frameNs;
count++;
if (frameNs / 1e6 > targetMs * 2) late++;
}
if (now - windowStart < 2_000_000_000L || count == 0) return;
Runtime runtime = Runtime.getRuntime();
double heap = (runtime.totalMemory() - runtime.freeMemory()) / (double) runtime.maxMemory();
double next = policy.update(setting.getTrueValue(), totalNs / 1e6 / count, targetMs,
late / (double) count, heap);
if (owned == null || Math.abs(next - owned) > 0.0001) {
if (setting.setValue(next, "Horizon adaptive budget")) {
owned = next;
changes++;
HorizonLab.LOGGER.info("Adaptive terrain background runtime ratio: {}", next);
}
}
resetWindow();
}
void suspend() {
if (setting != null && owned != null && java.util.Objects.equals(setting.getApiValue(), owned)) {
setting.clearValue();
HorizonLab.LOGGER.info("Adaptive terrain override released; effective runtime ratio: {}", setting.getValue());
}
owned = null;
policy = setting == null ? null : new TerrainBudget(setting.getTrueValue());
resetWindow();
}
private void resetWindow() { windowStart = 0; totalNs = 0; count = 0; late = 0; }
String status() {
return yielded ? "yielded to another mod" : owned == null ? "waiting for active gameplay"
: "DH runtime ratio " + owned + "; adjustments " + changes;
}
}
@@ -1,46 +0,0 @@
package dev.horizonlab.client;
import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfig;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import java.util.LinkedHashMap;
import java.util.Map;
/** Read effective API values once at capture boundaries; never change terrain settings. */
final class DistantTerrainSettings {
private DistantTerrainSettings() {}
static Map<String, Object> read() { return read(DhApi.Delayed.configs); }
static Map<String, Object> read(IDhApiConfig configs) {
if (configs == null) return Map.of("dh_capture_status", "unavailable");
Map<String, Object> values = new LinkedHashMap<>();
try {
var graphics = configs.graphics();
put(values, "dh_rendering_enabled", graphics.renderingEnabled());
put(values, "dh_renderer_mode", graphics.renderingMode());
put(values, "dh_lod_radius_chunks", graphics.chunkRenderDistance());
put(values, "dh_horizontal_quality", graphics.horizontalQuality());
put(values, "dh_vertical_quality", graphics.verticalQuality());
put(values, "dh_max_horizontal_resolution", graphics.maxHorizontalResolution());
put(values, "dh_transparency", graphics.transparency());
put(values, "dh_lod_shading", graphics.lodShading());
put(values, "dh_lod_only_mode", graphics.lodOnlyMode());
put(values, "dh_camera_quality_dropoff", graphics.useCameraPositionForQualityDropOff());
put(values, "dh_ambient_occlusion", graphics.ambientOcclusion().enabled());
put(values, "dh_thread_count", configs.multiThreading().threadCount());
put(values, "dh_runtime_ratio", configs.multiThreading().threadRuntimeRatio());
values.put("dh_capture_status", "available");
return values;
} catch (RuntimeException | LinkageError unavailable) {
// Missing/changed APIs must not look like disabled rendering or a zero distance.
return Map.of("dh_capture_status", "unavailable");
}
}
private static void put(Map<String, Object> target, String name, IDhApiConfigValue<?> setting) {
Object value = setting.getValue(); // Includes active overrides; getTrueValue reads only the user base.
if (value == null) throw new IllegalStateException("Unavailable configuration value");
target.put(name, value instanceof Enum<?> choice ? choice.name() : value);
}
}
@@ -1,212 +0,0 @@
package dev.horizonlab.client;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.platform.InputConstants;
import dev.horizonlab.HorizonLab;
import dev.horizonlab.metrics.Recording;
import dev.horizonlab.metrics.Reports;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Options;
import net.minecraft.client.KeyMapping;
import net.minecraft.network.chat.Component;
import static net.fabricmc.fabric.api.client.command.v2.ClientCommands.argument;
import static net.fabricmc.fabric.api.client.command.v2.ClientCommands.literal;
public final class HorizonClient implements ClientModInitializer {
private static Recording frames;
private static long previousStart;
private static String profile = "custom";
private static DistantTerrainBudget terrainBudget;
private static long previousBudgetFrame;
private static int targetFps = 60;
@Override public void onInitializeClient() {
KeyMapping benchmarkKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
"key.horizonlab.benchmark", InputConstants.KEY_F8, KeyMapping.Category.MISC));
ClientTickEvents.END_CLIENT_TICK.register(client -> {
while (benchmarkKey.consumeClick()) {
if (client.level != null && client.gui.screen() == null) {
if (frames == null) start(profile, 120); else finish();
}
}
});
Path profileFile = FabricLoader.getInstance().getConfigDir().resolve("horizon-profile.txt");
try {
if (Files.exists(profileFile)) profile = Files.readString(profileFile).strip();
} catch (Exception error) { HorizonLab.LOGGER.warn("Profile label could not be read", error); }
Path budgetFile = FabricLoader.getInstance().getConfigDir().resolve("horizon-adaptive.properties");
try {
if (Files.exists(budgetFile)) {
var properties = new java.util.Properties();
try (var reader = Files.newBufferedReader(budgetFile)) { properties.load(reader); }
targetFps = Math.clamp(Integer.parseInt(properties.getProperty("targetFps", "60")), 10, 240);
if (Boolean.parseBoolean(properties.getProperty("enabled", "false"))) enableBudget();
}
} catch (Exception error) { HorizonLab.LOGGER.warn("Adaptive budget disabled: invalid configuration", error); }
ClientCommandRegistrationCallback.EVENT.register((dispatcher, context) -> dispatcher.register(
literal("horizon")
.then(literal("bench")
.executes(command -> start("benchmark", 120))
.then(argument("label", StringArgumentType.word())
.executes(command -> start(StringArgumentType.getString(command, "label"), 120))
.then(argument("seconds", IntegerArgumentType.integer(10, 600))
.executes(command -> start(StringArgumentType.getString(command, "label"),
IntegerArgumentType.getInteger(command, "seconds"))))))
.then(literal("stop").executes(command -> { finish(); return 1; }))
.then(literal("adaptive")
.then(literal("on").executes(command -> {
if (frames != null) { message("Finish the recording before changing the budget"); return 0; }
message(enableBudget() ? "Adaptive terrain budget enabled" : "Distant Horizons is required");
return 1;
}))
.then(literal("off").executes(command -> {
if (frames != null) { message("Finish the recording before changing the budget"); return 0; }
if (terrainBudget != null) terrainBudget.suspend();
terrainBudget = null;
previousBudgetFrame = 0;
message("Adaptive terrain budget disabled"); return 1;
})))
.then(literal("status").executes(command -> {
message("Profile: " + profile + "; recording: " + (frames != null)
+ "; adaptive: " + (terrainBudget == null ? "off" : terrainBudget.status())); return 1;
}))));
HorizonLab.LOGGER.info("Horizon client profile: {}. F8 or /horizon bench <label> [seconds]", profile);
}
private static int start(String label, int seconds) {
if (frames != null) { message("A benchmark is already recording; /horizon stop finishes it."); return 0; }
Minecraft client = Minecraft.getInstance();
Path output = client.gameDirectory.toPath().resolve("horizon-benchmarks");
Map<String, Object> context = new LinkedHashMap<>();
context.put("minecraft", "26.3");
context.put("profile", profile);
context.put("adaptive_budget_enabled", terrainBudget != null);
context.put("precise_worker_pacing", dev.horizonlab.mixin.HorizonMixinPlugin.workerPacingEnabled());
context.put("debounce_queue_wait", dev.horizonlab.mixin.HorizonMixinPlugin.debounceWaitEnabled());
context.put("wakeable_idle_wait", dev.horizonlab.mixin.HorizonMixinPlugin.debounceWaitEnabled());
context.put("dispatch_retry_backoff", dev.horizonlab.mixin.HorizonMixinPlugin.dispatchRetryEnabled());
context.put("tick_membership_dedup", dev.horizonlab.mixin.HorizonMixinPlugin.tickDedupEnabled());
context.put("terrain_quad_reuse", dev.horizonlab.mixin.HorizonMixinPlugin.quadReuseEnabled());
context.put("terrain_position_arithmetic", dev.horizonlab.mixin.HorizonMixinPlugin.terrainPositionEnabled());
context.put("culling_ray_scratch", dev.horizonlab.mixin.HorizonMixinPlugin.cullingRayScratchEnabled());
context.put("empty_culling_whitelist", dev.horizonlab.mixin.HorizonMixinPlugin.cullingEmptyEnabled());
context.put("interruptible_terrain_read", dev.horizonlab.mixin.HorizonMixinPlugin.terrainReadWaitEnabled());
context.put("adaptive_target_fps", targetFps);
context.putAll(visualSettings(client));
var device = RenderSystem.getDevice().getDeviceInfo();
context.put("gpu", device.name());
context.put("graphics_backend", device.backendName());
context.put("graphics_driver", device.driverInfo());
context.putAll(dev.horizonlab.metrics.SoftwareContext.capture());
frames = new Recording(label, "frames", output, seconds, context);
previousStart = 0;
if (client.getSingleplayerServer() != null) HorizonLab.startTicks(label, seconds, output, profile, "integrated", context);
message("Recording " + seconds + " seconds. Close chat, keep the game focused, and run your route.");
return 1;
}
private static Map<String, Object> visualSettings(Minecraft client) {
Map<String, Object> context = new LinkedHashMap<>();
if (client.level != null) context.put("world_difficulty", client.level.getDifficulty().name());
context.put("render_distance", client.options.renderDistance().get());
context.put("simulation_distance", client.options.simulationDistance().get());
context.put("vsync", client.options.enableVsync().get());
context.put("fps_limit", client.options.framerateLimit().get());
context.put("fps_limit_unlimited", client.options.framerateLimit().get() >= Options.UNLIMITED_FRAMERATE_CUTOFF);
context.put("framebuffer_width", client.getWindow().getWidth());
context.put("framebuffer_height", client.getWindow().getHeight());
context.put("graphics_preset", client.options.graphicsPreset().get().toString());
context.put("ambient_occlusion", client.options.ambientOcclusion().get());
context.put("improved_transparency", client.options.improvedTransparency().get());
context.put("mipmap_levels", client.options.mipmapLevels().get());
context.put("entity_distance_scaling", client.options.entityDistanceScaling().get());
context.put("entity_shadows", client.options.entityShadows().get());
context.put("particles", client.options.particles().get().toString());
context.put("clouds", client.options.cloudStatus().get().toString());
context.put("cloud_range", client.options.cloudRange().get());
context.put("fov", client.options.fov().get());
var loader = FabricLoader.getInstance();
if (loader.isModLoaded("distanthorizons")) {
try { context.putAll(DistantTerrainSettings.read()); }
catch (RuntimeException | LinkageError unavailable) { context.put("dh_capture_status", "unavailable"); }
} else context.put("dh_capture_status", "not_installed");
context.putAll(ShaderSettings.read(loader.isModLoaded("iris")));
return context;
}
public static void frameStart() {
Recording recording = frames;
if (recording == null && terrainBudget == null) return;
long now = System.nanoTime();
if (recording != null && now >= recording.deadlineNs) { finish(); recording = null; }
Minecraft client = Minecraft.getInstance();
if (client.level == null || client.isPaused() || !client.isWindowActive() || client.gui.screen() != null) {
if (recording != null) recording.exclude();
if (terrainBudget != null && previousBudgetFrame != 0) terrainBudget.suspend();
previousBudgetFrame = 0;
previousStart = 0;
return;
}
if (terrainBudget != null) {
int cap = client.options.framerateLimit().get();
double targetMs = 1000.0 / Math.min(targetFps, Math.max(1, cap));
terrainBudget.observe(now, previousBudgetFrame == 0 ? 0 : now - previousBudgetFrame, targetMs);
previousBudgetFrame = now;
}
if (recording != null) {
if (previousStart != 0) recording.add(now - previousStart);
previousStart = now;
}
}
private static boolean enableBudget() {
if (!FabricLoader.getInstance().isModLoaded("distanthorizons")) return false;
if (terrainBudget == null) terrainBudget = new DistantTerrainBudget();
return true;
}
private static void finish() {
Recording recording = frames;
if (recording == null) return;
frames = null;
previousStart = 0;
HorizonLab.finishTicks();
Map<String, Object> end = visualSettings(Minecraft.getInstance());
java.util.List<String> changed = new java.util.ArrayList<>();
for (var setting : end.entrySet()) {
// Adaptive duty cycle is expected to change; visual quality changes are separate.
if (!setting.getKey().equals("dh_runtime_ratio")
&& !java.util.Objects.equals(recording.context.get(setting.getKey()), setting.getValue()))
changed.add(setting.getKey());
}
for (String setting : recording.context.keySet()) {
if ((setting.startsWith("dh_") || setting.startsWith("iris_")) && !end.containsKey(setting))
changed.add(setting);
}
recording.context.put("visual_settings_end", end);
recording.context.put("visual_settings_changed", changed);
Reports.write(recording.finish()).whenComplete((path, error) -> {
if (error != null) HorizonLab.LOGGER.error("Frame report failed", error);
else {
HorizonLab.LOGGER.info("Horizon frame benchmark saved to {}", path);
Minecraft.getInstance().execute(() -> message("Benchmark saved: " + path.getFileName()));
}
});
}
private static void message(String text) {
Minecraft client = Minecraft.getInstance();
if (client.gui != null) client.gui.hud.getChat().addClientSystemMessage(Component.literal("[Horizon] " + text));
}
}
@@ -1,28 +0,0 @@
package dev.horizonlab.client;
import java.util.LinkedHashMap;
import java.util.Map;
/** Optional Iris reflection keeps baseline clients independent of shader-mod classes. */
final class ShaderSettings {
private ShaderSettings() {}
static Map<String, Object> read(boolean irisPresent) {
if (!irisPresent) return Map.of("iris_capture_status", "not_installed", "iris_shaders_enabled", false);
try {
Class<?> api = Class.forName("net.irisshaders.iris.api.v0.IrisApi");
Object instance = api.getMethod("getInstance").invoke(null);
boolean enabled = (Boolean) api.getMethod("isShaderPackInUse").invoke(instance);
Map<String, Object> result = new LinkedHashMap<>();
result.put("iris_capture_status", "available");
result.put("iris_shaders_enabled", enabled);
if (enabled) {
Class<?> iris = Class.forName("net.irisshaders.iris.Iris");
result.put("iris_shader_pack", iris.getMethod("getCurrentPackName").invoke(null));
}
return result;
} catch (ReflectiveOperationException | RuntimeException | LinkageError unavailable) {
return Map.of("iris_capture_status", "unavailable");
}
}
}
@@ -1,30 +0,0 @@
package dev.horizonlab.geometry;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
/** Numeric-only conversion; preserve the pinned library's truncation and negative-coordinate bias. */
public final class RelativeTerrainPosition {
private RelativeTerrainPosition() {}
public static long resolve(long position, byte outputLevel) {
int delta = outputLevel - DhSectionPos.getDetailLevel(position);
int shift = delta & 31;
// Invalid levels keep the publisher exception; a sign-bit divisor keeps its original arithmetic.
if (delta < 0 || shift == 31) {
return DhSectionPos.getDhSectionRelativePositionForDetailLevel(position, outputLevel);
}
int divisorMask = (1 << shift) - 1;
int negativeBias = divisorMask | ((divisorMask - 1) >>> 31);
return DhSectionPos.encode(outputLevel,
coordinate(DhSectionPos.getX(position), shift, divisorMask, negativeBias),
coordinate(DhSectionPos.getZ(position), shift, divisorMask, negativeBias));
}
private static int coordinate(int value, int shift, int divisorMask, int negativeBias) {
int negative = value >> 31;
int adjusted = value + (negative & negativeBias);
// Adding the divisor mask only for a negative adjusted value gives truncation toward zero.
int truncated = (adjusted + ((adjusted >> 31) & divisorMask)) >> shift;
return (truncated + negative) & 63;
}
}
@@ -1,53 +0,0 @@
package dev.horizonlab.metrics;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;
/** One background reader for this process, active only while recordings exist. */
public final class ProcessMemoryMonitor {
private static final Set<Recording> ACTIVE = ConcurrentHashMap.newKeySet();
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(task -> {
Thread thread = new Thread(task, "Horizon process memory");
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
private static volatile LongSupplier reader;
private static ScheduledFuture<?> polling;
private ProcessMemoryMonitor() {}
public static synchronized void configure(LongSupplier residentMemoryReader) {
if (!ACTIVE.isEmpty()) throw new IllegalStateException("Configure before starting recordings");
reader = residentMemoryReader;
}
static synchronized void track(Recording recording) {
if (reader == null) return;
ACTIVE.add(recording);
if (polling == null) polling = EXECUTOR.scheduleWithFixedDelay(ProcessMemoryMonitor::poll, 0, 1, TimeUnit.SECONDS);
}
static synchronized void untrack(Recording recording) {
ACTIVE.remove(recording);
if (ACTIVE.isEmpty() && polling != null) {
polling.cancel(false);
polling = null;
}
}
private static void poll() {
Recording[] recordings = ACTIVE.toArray(Recording[]::new);
if (recordings.length == 0) return;
long bytes;
try { bytes = reader.getAsLong(); }
catch (RuntimeException | LinkageError unavailable) { return; }
if (bytes <= 0) return;
for (Recording recording : recordings) recording.residentMemory(bytes);
}
}
@@ -1,159 +0,0 @@
package dev.horizonlab.metrics;
import java.lang.management.ManagementFactory;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
/** One preallocated bounded buffer per recording, no per-sample allocations or disk I/O. */
public final class Recording {
public static final int MAX_SAMPLES = 300_000;
public final String label;
public final String kind;
public final Path output;
public final long startedNs;
public final long deadlineNs;
public final Map<String, Object> context;
private final long[] samples = new long[MAX_SAMPLES];
private int count;
private int dropped;
private int excluded;
private int peakPlayers;
private final long initialGcCount;
private final long initialGcMs;
private final java.lang.management.MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
private final com.sun.management.OperatingSystemMXBean operatingSystem =
ManagementFactory.getOperatingSystemMXBean() instanceof com.sun.management.OperatingSystemMXBean bean ? bean : null;
private final long initialCpuNs;
private long nextResourceSampleNs;
private long peakHeap;
private long peakNonHeap;
private int resourceSamples;
private long residentEnd;
private long residentPeak;
private int residentSamples;
private long residentSampleNs;
private boolean finished;
private final TickState tickState;
public Recording(String label, String kind, Path output, int seconds, Map<String, Object> context) {
if (seconds < 1 || seconds > 600) throw new IllegalArgumentException("Duration must be 1..600 seconds");
String safeLabel = label.replaceAll("[^A-Za-z0-9_-]", "_");
this.label = safeLabel.substring(0, Math.min(safeLabel.length(), 64));
this.kind = kind;
this.tickState = kind.equals("ticks") ? new TickState() : null;
this.output = output;
this.context = new LinkedHashMap<>(context);
this.startedNs = System.nanoTime();
this.deadlineNs = startedNs + seconds * 1_000_000_000L;
this.initialGcCount = gcCount();
this.initialGcMs = gcMs();
this.initialCpuNs = operatingSystem == null ? -1 : operatingSystem.getProcessCpuTime();
sampleResources(startedNs);
ProcessMemoryMonitor.track(this);
}
public synchronized void add(long ns) {
if (ns <= 0 || finished) return;
long now = System.nanoTime();
if (now >= nextResourceSampleNs) sampleResources(now);
if (count == samples.length) { dropped++; return; }
samples[count++] = ns;
}
public synchronized void exclude() { excluded++; }
public synchronized void players(int players) { peakPlayers = Math.max(players, peakPlayers); }
public synchronized void addTick(long ns, int players, float rate, boolean frozen, boolean stepping, boolean sprinting, boolean runsGameElements) {
if (ns <= 0 || finished || !kind.equals("ticks")) return;
tickState.observe(rate, frozen, stepping, sprinting, runsGameElements);
add(ns);
players(players);
}
public synchronized Snapshot finish() {
finished = true;
ProcessMemoryMonitor.untrack(this);
sampleResources(System.nanoTime());
Map<String, Object> metadata = new LinkedHashMap<>(context);
metadata.put("label", label);
metadata.put("kind", kind);
metadata.put("schema", 1);
double elapsed = (System.nanoTime() - startedNs) / 1e9;
metadata.put("elapsed_seconds", elapsed);
if (kind.equals("ticks")) {
metadata.put("observed_ticks_per_second", (count + (long) dropped) / elapsed);
metadata.putAll(tickState.metadata());
}
metadata.put("dropped_samples", dropped);
metadata.put("excluded_frames", excluded);
metadata.put("peak_players", peakPlayers);
metadata.put("java_version", System.getProperty("java.version"));
metadata.put("os", System.getProperty("os.name"));
metadata.put("os_version", System.getProperty("os.version"));
metadata.put("architecture", System.getProperty("os.arch"));
metadata.put("logical_processors", Runtime.getRuntime().availableProcessors());
metadata.put("garbage_collectors", ManagementFactory.getGarbageCollectorMXBeans().stream()
.map(java.lang.management.MemoryManagerMXBean::getName).sorted().toList());
try {
var diagnostic = ManagementFactory.getPlatformMXBean(com.sun.management.HotSpotDiagnosticMXBean.class);
if (diagnostic != null) metadata.put("compact_object_headers",
Boolean.parseBoolean(diagnostic.getVMOption("UseCompactObjectHeaders").getValue()));
} catch (RuntimeException | LinkageError unavailable) {
// Other VM implementations may not expose this option; absence is distinct from false.
}
var endHeap = memory.getHeapMemoryUsage();
peakHeap = Math.max(peakHeap, endHeap.getUsed());
metadata.put("heap_used_end_bytes", endHeap.getUsed());
metadata.put("heap_committed_end_bytes", endHeap.getCommitted());
metadata.put("heap_max_bytes", Runtime.getRuntime().maxMemory());
metadata.put("heap_used_peak_sampled_bytes", peakHeap);
metadata.put("nonheap_used_peak_sampled_bytes", peakNonHeap);
metadata.put("resource_samples", resourceSamples);
metadata.put("resource_sample_interval_ms", 1000);
if (residentSamples > 0) {
metadata.put("process_rss_last_sampled_bytes", residentEnd);
metadata.put("process_rss_peak_sampled_bytes", residentPeak);
metadata.put("process_rss_samples", residentSamples);
metadata.put("process_rss_sample_interval_ms", 1000);
metadata.put("process_rss_last_sample_age_ms", (System.nanoTime() - residentSampleNs) / 1e6);
}
long endCpuNs = operatingSystem == null ? -1 : operatingSystem.getProcessCpuTime();
if (initialCpuNs >= 0 && endCpuNs >= initialCpuNs) {
double cpuSeconds = (endCpuNs - initialCpuNs) / 1e9;
metadata.put("process_cpu_seconds", cpuSeconds);
metadata.put("process_cpu_mean_cores", cpuSeconds / elapsed);
}
metadata.put("gc_collections", gcCount() - initialGcCount);
metadata.put("gc_time_ms", gcMs() - initialGcMs);
return new Snapshot(label, kind, output, Arrays.copyOf(samples, count), metadata);
}
synchronized void residentMemory(long bytes) {
if (finished || bytes <= 0) return;
residentEnd = bytes;
residentPeak = Math.max(residentPeak, bytes);
residentSamples++;
residentSampleNs = System.nanoTime();
}
private void sampleResources(long now) {
peakHeap = Math.max(peakHeap, memory.getHeapMemoryUsage().getUsed());
peakNonHeap = Math.max(peakNonHeap, memory.getNonHeapMemoryUsage().getUsed());
resourceSamples++;
nextResourceSampleNs = now + 1_000_000_000L;
}
private static long gcCount() {
return ManagementFactory.getGarbageCollectorMXBeans().stream()
.mapToLong(gc -> Math.max(0, gc.getCollectionCount())).sum();
}
private static long gcMs() {
return ManagementFactory.getGarbageCollectorMXBeans().stream()
.mapToLong(gc -> Math.max(0, gc.getCollectionTime())).sum();
}
public record Snapshot(String label, String kind, Path output, long[] samples, Map<String, Object> metadata) {}
}
@@ -1,43 +0,0 @@
package dev.horizonlab.metrics;
import com.google.gson.GsonBuilder;
import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public final class Reports {
private Reports() {}
public static CompletableFuture<Path> write(Recording.Snapshot snapshot) {
return CompletableFuture.supplyAsync(() -> {
try {
Files.createDirectories(snapshot.output());
String name = snapshot.label() + "-" + snapshot.kind() + "-" + Instant.now().toEpochMilli();
Path csv = snapshot.output().resolve(name + ".csv");
try (BufferedWriter writer = Files.newBufferedWriter(csv, StandardCharsets.UTF_8)) {
writer.write("sample,duration_ns\n");
for (int i = 0; i < snapshot.samples().length; i++) {
writer.write(i + "," + snapshot.samples()[i] + "\n");
}
}
SampleStats stats = SampleStats.of(snapshot.samples(), snapshot.samples().length);
Map<String, Object> report = new LinkedHashMap<>(snapshot.metadata());
report.put("stats", stats);
report.put("samples_file", csv.getFileName().toString());
report.put("note", snapshot.kind().equals("frames")
? "Frame start intervals include frame limiting and presentation waits; menus, pauses and unfocused frames are excluded. Not GPU timings. Process RSS, when available, is sampled separately."
: "Tick execution durations exclude scheduler sleep. meanRate is execution capacity, not observed TPS.");
Path json = snapshot.output().resolve(name + ".json");
Files.writeString(json, new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n");
return json;
} catch (Exception exception) {
throw new IllegalStateException("Could not write Horizon benchmark", exception);
}
});
}
}
@@ -1,20 +0,0 @@
package dev.horizonlab.metrics;
import java.util.function.LongSupplier;
import oshi.SystemInfo;
import oshi.software.os.OSProcess;
/** Uses Minecraft's existing OSHI dependency; initialization stays on the sampler thread. */
public final class ResidentMemory implements LongSupplier {
private OSProcess process;
private boolean initialized;
@Override public long getAsLong() {
if (!initialized) {
initialized = true;
var os = new SystemInfo().getOperatingSystem();
process = os.getProcess(os.getProcessId());
}
return process != null && process.updateAttributes() ? process.getResidentSetSize() : -1;
}
}
@@ -1,36 +0,0 @@
package dev.horizonlab.metrics;
import java.util.Arrays;
/** Original bounded measurement utilities; no Minecraft dependency. */
public record SampleStats(int count, double meanMs, double p50Ms, double p95Ms,
double p99Ms, double maxMs, double meanRate, double lowOnePercentRate,
int over16Ms, int over33Ms, int over50Ms, int over100Ms) {
public static SampleStats of(long[] samples, int count) {
if (count < 0 || count > samples.length) throw new IllegalArgumentException("Invalid sample count");
if (count == 0) return new SampleStats(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
long[] sorted = Arrays.copyOf(samples, count);
double sum = 0;
int over16 = 0, over33 = 0, over50 = 0, over100 = 0;
for (long ns : sorted) {
if (ns <= 0) throw new IllegalArgumentException("Sample duration must be positive");
sum += ns;
if (ns > 16_666_667L) over16++;
if (ns > 33_333_333L) over33++;
if (ns > 50_000_000L) over50++;
if (ns > 100_000_000L) over100++;
}
Arrays.sort(sorted);
int tail = Math.max(1, (int) Math.ceil(count * 0.01));
double tailSum = 0;
for (int i = count - tail; i < count; i++) tailSum += sorted[i];
return new SampleStats(count, sum / count / 1e6, quantile(sorted, 0.50) / 1e6,
quantile(sorted, 0.95) / 1e6, quantile(sorted, 0.99) / 1e6,
sorted[count - 1] / 1e6, 1e9 * count / sum, 1e9 * tail / tailSum,
over16, over33, over50, over100);
}
private static long quantile(long[] sorted, double fraction) {
return sorted[Math.max(0, (int) Math.ceil(sorted.length * fraction) - 1)];
}
}
@@ -1,27 +0,0 @@
package dev.horizonlab.metrics;
import java.util.LinkedHashMap;
import java.util.Map;
/** Copies a recording-start snapshot; never accesses a graphics device on a server thread. */
public final class ServerContext {
private ServerContext() {}
public static Map<String, Object> capture(String runtimeKind, Map<String, ?> clientContext) {
Map<String, Object> result = new LinkedHashMap<>();
String kind = switch (runtimeKind) {
case "integrated", "dedicated" -> runtimeKind;
default -> "unknown";
};
result.put("server_runtime_kind", kind);
boolean captured = kind.equals("integrated") && clientContext != null;
for (String key : new String[]{"gpu", "graphics_backend", "graphics_driver"}) {
Object value = clientContext == null ? null : clientContext.get(key);
if (!(value instanceof String text) || text.isBlank()) captured = false;
}
result.put("client_graphics_capture_status", captured ? "captured_at_recording_start" : "not_supplied");
if (captured) for (String key : new String[]{"gpu", "graphics_backend", "graphics_driver"})
result.put(key, clientContext.get(key));
return Map.copyOf(result);
}
}
@@ -1,21 +0,0 @@
package dev.horizonlab.metrics;
import java.util.LinkedHashMap;
import java.util.Map;
import net.fabricmc.loader.api.FabricLoader;
/** Software identity captured once before a client or server recording starts. */
public final class SoftwareContext {
private SoftwareContext() {}
public static Map<String, Object> capture() {
Map<String, Object> context = new LinkedHashMap<>();
context.put("java_vendor", System.getProperty("java.vendor"));
context.put("java_vm_name", System.getProperty("java.vm.name"));
context.put("java_runtime_version", System.getProperty("java.runtime.version"));
context.put("mods", FabricLoader.getInstance().getAllMods().stream()
.map(mod -> mod.getMetadata().getId() + "@" + mod.getMetadata().getVersion().getFriendlyString())
.sorted().toList());
return context;
}
}
@@ -1,76 +0,0 @@
package dev.horizonlab.metrics;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
/** Scalar-only observations of the tick manager after each measured server invocation. */
public final class TickState {
private static final int FROZEN = 1;
private static final int STEPPING = 2;
private static final int SPRINTING = 4;
private static final int RUNS_GAME_ELEMENTS = 8;
private long count;
private float firstRate;
private float lastRate;
private int firstFlags;
private int lastFlags;
private int changedFlags;
private boolean rateChanged;
private long frozen;
private long stepping;
private long sprinting;
private long suppressed;
private long nonstandardRate;
private long invalid;
public void observe(float rate, boolean isFrozen, boolean isStepping, boolean isSprinting, boolean runsGameElements) {
if (!Float.isFinite(rate) || rate < 1) { invalid++; return; }
int flags = (isFrozen ? FROZEN : 0) | (isStepping ? STEPPING : 0)
| (isSprinting ? SPRINTING : 0) | (runsGameElements ? RUNS_GAME_ELEMENTS : 0);
if (count == 0) {
firstRate = rate;
firstFlags = flags;
} else {
rateChanged |= rate != lastRate;
changedFlags |= flags ^ lastFlags;
}
lastRate = rate;
lastFlags = flags;
count++;
if (isFrozen) frozen++;
if (isStepping) stepping++;
if (isSprinting) sprinting++;
if (!runsGameElements) suppressed++;
if (rate != 20) nonstandardRate++;
}
public Map<String, Object> metadata() {
Map<String, Object> result = new LinkedHashMap<>();
result.put("tick_state_capture_status", invalid > 0 ? "invalid_samples" : count == 0 ? "unavailable" : "sampled_server_ticks");
result.put("tick_state_samples", count);
result.put("tick_state_invalid_samples", invalid);
if (count == 0) return result;
result.put("tick_state_first", state(firstRate, firstFlags));
result.put("tick_state_last", state(lastRate, lastFlags));
var changed = new ArrayList<String>();
if (rateChanged) changed.add("configured_tick_rate");
if ((changedFlags & FROZEN) != 0) changed.add("frozen");
if ((changedFlags & STEPPING) != 0) changed.add("stepping");
if ((changedFlags & SPRINTING) != 0) changed.add("sprinting");
if ((changedFlags & RUNS_GAME_ELEMENTS) != 0) changed.add("runs_game_elements");
result.put("tick_state_changed", changed);
result.put("tick_state_frozen_samples", frozen);
result.put("tick_state_stepping_samples", stepping);
result.put("tick_state_sprinting_samples", sprinting);
result.put("tick_state_suppressed_samples", suppressed);
result.put("tick_state_nonstandard_rate_samples", nonstandardRate);
return result;
}
private static Map<String, Object> state(float rate, int flags) {
return Map.of("configured_tick_rate", rate, "frozen", (flags & FROZEN) != 0,
"stepping", (flags & STEPPING) != 0, "sprinting", (flags & SPRINTING) != 0,
"runs_game_elements", (flags & RUNS_GAME_ELEMENTS) != 0);
}
}
@@ -1,19 +0,0 @@
package dev.horizonlab.mixin;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/** Empty dynamic whitelists have no handlers to visit; nonempty sets retain their own iterator. */
@Mixin(targets = "dev.tr7zw.entityculling.EntityCullingModBase", remap = false)
public abstract class CullingEmptyWhitelistMixin {
@Redirect(method = {"isEntityDynamicWhitelisted", "isBlockEntityDynamicWhitelisted"},
at = @At(value = "INVOKE", target = "Ljava/util/Set;iterator()Ljava/util/Iterator;"))
private Iterator<?> horizon$iterateDynamicHandlers(Set<?> handlers) {
// Check the live set on every call. Registration, order, callbacks and nonempty iteration stay upstream.
return handlers.isEmpty() ? Collections.emptyIterator() : handlers.iterator();
}
}
@@ -1,28 +0,0 @@
package dev.horizonlab.mixin;
import com.logisticscraft.occlusionculling.util.Vec3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/** Keep temporary numeric vectors on the already mutable, single-caller culling instance. */
@Mixin(targets = "com.logisticscraft.occlusionculling.OcclusionCullingInstance", remap = false)
public abstract class CullingRayScratchMixin {
@Unique private final Vec3d horizon$rayDirection = new Vec3d(0, 0, 0);
@Unique private final Vec3d horizon$inverseDirection = new Vec3d(0, 0, 0);
@Redirect(method = "isVisible", at = @At(value = "NEW",
target = "(DDD)Lcom/logisticscraft/occlusionculling/util/Vec3d;"))
private Vec3d horizon$direction(double x, double y, double z) {
horizon$rayDirection.set(x, y, z);
return horizon$rayDirection;
}
@Redirect(method = "rayIntersection", at = @At(value = "NEW",
target = "(DDD)Lcom/logisticscraft/occlusionculling/util/Vec3d;"))
private Vec3d horizon$inverse(double x, double y, double z) {
horizon$inverseDirection.set(x, y, z);
return horizon$inverseDirection;
}
}
@@ -1,77 +0,0 @@
package dev.horizonlab.mixin;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkPosQueue;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import dev.horizonlab.budget.DebounceWait;
import dev.horizonlab.budget.WakeableIdleWait;
import java.util.concurrent.locks.LockSupport;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Pseudo
@Mixin(targets = "com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager", remap = false)
public abstract class DhDebounceWaitMixin {
@Shadow @Final public ChunkPosQueue preUpdateQueue;
@Unique private volatile Thread horizonQueueWorker;
@Unique private long horizonLastIdleCleanup;
@Inject(method = "runQueueingLoop", at = @At("HEAD"), remap = false)
private void horizonRememberWorker(CallbackInfo callback) {
horizonQueueWorker = Thread.currentThread();
horizonLastIdleCleanup = System.nanoTime();
}
@Inject(method = "runQueueingLoop", at = @At("RETURN"), remap = false)
private void horizonReleaseWorker(CallbackInfo callback) {
horizonQueueWorker = null;
}
@Redirect(method = "runQueueingLoop", at = @At(value = "INVOKE",
target = "Ljava/lang/Thread;sleep(J)V"), require = 1, allow = 1, remap = false)
private void horizonWakeableIdle(long milliseconds) throws InterruptedException {
if (milliseconds != 50 || Thread.currentThread() != horizonQueueWorker) {
Thread.sleep(milliseconds);
return;
}
long remaining = 50_000_000L - (System.nanoTime() - horizonLastIdleCleanup);
WakeableIdleWait.pause(this, Math.max(0, remaining));
}
// Early wakeups must not turn the former 50 ms maintenance cadence into per-arrival work.
@Redirect(method = "runQueueingLoop", at = @At(value = "INVOKE",
target = "Lcom/google/common/cache/Cache;cleanUp()V"), require = 1, allow = 1, remap = false)
private void horizonBoundIdleMaintenance(com.google.common.cache.Cache<?, ?> cache) {
long now = System.nanoTime();
if (Thread.currentThread() != horizonQueueWorker || now - horizonLastIdleCleanup >= 50_000_000L) {
cache.cleanUp();
horizonLastIdleCleanup = System.nanoTime();
}
}
// Ordinal zero is specifically the debounce retry. Executor rejection retries remain unchanged.
@Redirect(method = "tryDispatchReadyChunkUpdate", at = @At(value = "INVOKE",
target = "Lcom/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkPosQueue;addItem(Lcom/seibel/distanthorizons/core/pos/DhChunkPos;Lcom/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkUpdateData;)V",
ordinal = 0), require = 1, allow = 1, remap = false)
private void horizonWaitForDebounce(ChunkPosQueue queue, DhChunkPos position, ChunkUpdateData update) {
queue.addItem(position, update);
if (Thread.currentThread() != horizonQueueWorker) return;
long delay = DebounceWait.delay(System.nanoTime(), update.firstQueuedNanoTime,
ChunkUpdateQueueManager.UPDATE_DEBOUNCE_NANOS, !preUpdateQueue.isEmpty());
if (delay > 0) LockSupport.parkNanos(this, delay);
}
@Inject(method = "addItemToQueue", at = @At("RETURN"), remap = false)
private void horizonWakeForNewWork(CallbackInfo callback) {
Thread worker = horizonQueueWorker;
if (worker != null && worker != Thread.currentThread()) LockSupport.unpark(worker);
}
}
@@ -1,76 +0,0 @@
package dev.horizonlab.mixin;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import dev.horizonlab.budget.DispatchRetry;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Pseudo
@Mixin(targets = "com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker", remap = false)
public abstract class DhPriorityDispatchMixin {
@Shadow @Final private AtomicInteger occupiedThreadsRef;
@Shadow @Final private AtomicBoolean isShutDownRef;
@Unique private final DispatchRetry horizonDispatch = new DispatchRetry();
// This site is after the original lock acquisition, including the failed-tryLock early return.
@Inject(method = "startNextTask", at = @At(value = "INVOKE",
target = "Lcom/seibel/distanthorizons/core/util/threading/PriorityTaskPicker;getExecutorIteratorSortedByShortestTotalRunTime()Ljava/util/Iterator;"),
require = 1, allow = 1, remap = false)
private void horizonBeginDispatchPass(boolean blocking, CallbackInfo callback) {
horizonDispatch.beginPass();
}
@Redirect(method = "startNextTask", at = @At(value = "INVOKE",
target = "Lcom/seibel/distanthorizons/core/util/threading/PriorityTaskPicker$Executor;canRun()Z"),
require = 1, allow = 1, remap = false)
private boolean horizonBeginExecutor(PriorityTaskPicker.Executor executor) {
horizonDispatch.beginExecutor();
return executor.canRun();
}
@Redirect(method = "startNextTask", at = @At(value = "INVOKE",
target = "Lcom/seibel/distanthorizons/core/util/threading/PriorityTaskPicker$Executor;runTask(Ljava/lang/Runnable;)V"),
require = 1, allow = 1, remap = false)
private void horizonMarkRejected(PriorityTaskPicker.Executor executor, Runnable task) {
try {
executor.runTask(task);
} catch (RejectedExecutionException rejected) {
horizonDispatch.rejected();
throw rejected; // Original catch requeues the same task or clears it during shutdown.
}
}
@Redirect(method = "startNextTask", at = @At(value = "INVOKE",
target = "Ljava/util/concurrent/atomic/AtomicInteger;get()I"), require = 1, allow = 1, remap = false)
private int horizonLeaveRejectedExecutor(AtomicInteger counter) {
int occupied = counter.get();
return counter == occupiedThreadsRef
? horizonDispatch.visibleSlots(occupied) : occupied;
}
// Inspected DH 3.3.4 local slot 2 is the existing "executor paused" retry flag.
@ModifyVariable(method = "startNextTask", at = @At("LOAD"), index = 2,
require = 1, allow = 1, remap = false)
private boolean horizonRetryRejectedPass(boolean original) {
return horizonDispatch.needsRetry(original, isShutDownRef.get());
}
@ModifyConstant(method = "startNextTask", constant = @Constant(longValue = 1000L),
require = 1, allow = 1, remap = false)
private long horizonRetryDelay(long original) {
return horizonDispatch.retryMilliseconds(original);
}
}
@@ -1,31 +0,0 @@
package dev.horizonlab.mixin;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.BufferMergeDirectionEnum;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.BufferQuad;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/** Reuse one discarded successful-premerge quad only when the existing pool needs a new object. */
@Mixin(targets = "com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodQuadBuilder", remap = false)
public abstract class DhQuadReuseMixin {
@Unique private BufferQuad horizon$discardedQuad;
@Redirect(method = "getOrCreateBufferQuad", at = @At(value = "NEW",
target = "com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad"), require = 2, allow = 2)
private BufferQuad horizon$freshQuad() {
BufferQuad available = horizon$discardedQuad;
horizon$discardedQuad = null;
return available == null ? new BufferQuad() : available;
}
@Redirect(method = "addQuadAdj", at = @At(value = "INVOKE",
target = "Lcom/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad;tryMerge(Lcom/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad;Lcom/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferMergeDirectionEnum;)Z"),
require = 2, allow = 2)
private boolean horizon$merge(BufferQuad receiver, BufferQuad incoming, BufferMergeDirectionEnum direction) {
boolean merged = receiver.tryMerge(incoming, direction);
if (merged && horizon$discardedQuad == null) horizon$discardedQuad = incoming;
return merged;
}
}
@@ -1,16 +0,0 @@
package dev.horizonlab.mixin;
import dev.horizonlab.geometry.RelativeTerrainPosition;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/** Replace only the numeric conversion in the sampled full-data lookup. */
@Mixin(targets = "com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2", remap = false)
public abstract class DhRelativePositionMixin {
@Redirect(method = "getDataPointAtBlockPos", at = @At(value = "INVOKE",
target = "Lcom/seibel/distanthorizons/core/pos/DhSectionPos;getDhSectionRelativePositionForDetailLevel(JB)J"))
private long horizon$relativePosition(long position, byte outputLevel) {
return RelativeTerrainPosition.resolve(position, outputLevel);
}
}
@@ -1,18 +0,0 @@
package dev.horizonlab.mixin;
import dev.horizonlab.budget.TerrainReadWait;
import java.util.concurrent.CompletableFuture;
import net.minecraft.world.level.chunk.storage.IOWorker;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/** Only blending metadata reads on pinned DH workers become interruptible; saves stay upstream. */
@Mixin(IOWorker.class)
public abstract class DhTerrainReadWaitMixin {
@Redirect(method = "isOldChunkAround", at = @At(value = "INVOKE",
target = "Ljava/util/concurrent/CompletableFuture;join()Ljava/lang/Object;"))
private Object horizon$waitForBlendingMetadata(CompletableFuture<?> future) {
return TerrainReadWait.join(future);
}
}
@@ -1,30 +0,0 @@
package dev.horizonlab.mixin;
import com.seibel.distanthorizons.core.render.QuadTree.QuadTreeTickNodeHolder;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import java.util.HashSet;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Replace only the holder's private membership test; published action sets keep their order. */
@Mixin(value = QuadTreeTickNodeHolder.class, remap = false)
public abstract class DhTickDedupMixin {
@Unique private final ObjectOpenHashSet<Object> horizon$presentNodes = new ObjectOpenHashSet<>();
@Redirect(method = {"addEnableNode", "addDisableNode", "addEnableDeleteChildrenNode", "removeEnableAndDisableNode"},
at = @At(value = "INVOKE", target = "Ljava/util/HashSet;add(Ljava/lang/Object;)Z", ordinal = 0))
private boolean horizon$recordPresentNode(HashSet<?> original, Object node) {
// In pinned DH 3.3.4 each method's first add targets presentNodes.
// The original branches and action collections remain responsible for every transition.
return horizon$presentNodes.add(node);
}
@Inject(method = "clear", at = @At("HEAD"))
private void horizon$clearPresentNodes(CallbackInfo callback) {
horizon$presentNodes.clear(); // Release references at the same boundary; retain reusable table capacity.
}
}
@@ -1,42 +0,0 @@
package dev.horizonlab.mixin;
import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import dev.horizonlab.budget.WorkerPacer;
import java.util.concurrent.TimeUnit;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Pseudo
@Mixin(targets = "com.seibel.distanthorizons.core.util.threading.RateLimitedThreadPoolExecutor", remap = false)
public abstract class DhWorkerPacingMixin {
@Shadow @Final public ConfigEntry<Double> runTimeRatioConfig;
@Unique private final ThreadLocal<WorkerPacer> horizonPacing = ThreadLocal.withInitial(WorkerPacer::new);
// The inspected superclass's hooks are no-ops. Neither hook changes the runnable or task queue.
@Inject(method = "beforeExecute", at = @At("HEAD"), cancellable = true, remap = false)
private void horizonBeforeTask(Thread thread, Runnable task, CallbackInfo callback) {
horizonPacing.get().begin(System.nanoTime());
callback.cancel();
}
@Inject(method = "afterExecute", at = @At("HEAD"), cancellable = true, remap = false)
private void horizonAfterTask(Runnable task, Throwable error, CallbackInfo callback) {
WorkerPacer pacing = horizonPacing.get();
long delay = pacing.finish(System.nanoTime(), runTimeRatioConfig.get());
if (delay > 0) {
try {
TimeUnit.NANOSECONDS.sleep(delay);
pacing.pauseCompleted(System.nanoTime());
} catch (InterruptedException interrupted) {
pacing.interrupted(System.nanoTime());
}
}
callback.cancel();
}
}
@@ -1,16 +0,0 @@
package dev.horizonlab.mixin;
import dev.horizonlab.client.HorizonClient;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Minecraft.class)
public abstract class FrameMixin {
@Inject(method = "runTick", at = @At("HEAD"))
private void horizonFrame(boolean advanceGameTime, CallbackInfo callback) {
HorizonClient.frameStart();
}
}
@@ -1,193 +0,0 @@
package dev.horizonlab.mixin;
import java.nio.file.Files;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import net.fabricmc.loader.api.FabricLoader;
import org.objectweb.asm.tree.ClassNode;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
/** Internal hooks are opt-in and limited to the inspected, pinned DH version. */
public final class HorizonMixinPlugin implements IMixinConfigPlugin {
private static boolean quadReuse;
private static boolean terrainPosition;
private static boolean workerPacing;
private static boolean debounceWait;
private static boolean dispatchRetry;
private static boolean tickDedup;
private static boolean cullingEmpty;
private static boolean terrainReadWait;
private static boolean cullingRayScratch;
@Override public void onLoad(String mixinPackage) {
var loader = FabricLoader.getInstance();
var file = loader.getConfigDir().resolve("horizon-worker-pacing.properties");
try {
Properties properties = new Properties();
if (Files.exists(file)) {
try (var reader = Files.newBufferedReader(file)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("distanthorizons")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("3.3.4")).orElse(false);
workerPacing = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"Precise DH worker pacing: {} (requires pinned DH 3.3.4)", workerPacing ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Worker pacing disabled: invalid configuration", error);
}
try {
Properties properties = new Properties();
var queueFile = loader.getConfigDir().resolve("horizon-queue-wait.properties");
if (Files.exists(queueFile)) {
try (var reader = Files.newBufferedReader(queueFile)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("distanthorizons")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("3.3.4")).orElse(false);
debounceWait = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"DH debounce queue wait: {} (requires pinned DH 3.3.4)", debounceWait ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Debounce queue wait disabled: invalid configuration", error);
}
try {
Properties properties = new Properties();
var dispatchFile = loader.getConfigDir().resolve("horizon-dispatch-retry.properties");
if (Files.exists(dispatchFile)) {
try (var reader = Files.newBufferedReader(dispatchFile)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("distanthorizons")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("3.3.4")).orElse(false);
dispatchRetry = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"DH rejected dispatch retry: {} (requires pinned DH 3.3.4)", dispatchRetry ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Dispatch retry disabled: invalid configuration", error);
}
try {
Properties properties = new Properties();
var dedupFile = loader.getConfigDir().resolve("horizon-tick-dedup.properties");
if (Files.exists(dedupFile)) {
try (var reader = Files.newBufferedReader(dedupFile)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("distanthorizons")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("3.3.4")).orElse(false);
tickDedup = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"DH tick membership deduplication: {} (requires pinned DH 3.3.4)", tickDedup ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Tick membership deduplication disabled: invalid configuration", error);
}
try {
Properties properties = new Properties();
var cullingFile = loader.getConfigDir().resolve("horizon-culling-empty.properties");
if (Files.exists(cullingFile)) {
try (var reader = Files.newBufferedReader(cullingFile)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("entityculling")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("1.11.2")).orElse(false);
cullingEmpty = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"Empty culling whitelist iteration: {} (requires pinned Entity Culling 1.11.2)", cullingEmpty ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Empty culling whitelist iteration disabled: invalid configuration", error);
}
try {
Properties properties = new Properties();
var readFile = loader.getConfigDir().resolve("horizon-terrain-read-wait.properties");
if (Files.exists(readFile)) {
try (var reader = Files.newBufferedReader(readFile)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("distanthorizons")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("3.3.4")).orElse(false)
&& loader.getModContainer("minecraft")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("26.3")).orElse(false);
terrainReadWait = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"Interruptible DH blending metadata reads: {} (requires Minecraft 26.3 and DH 3.3.4)",
terrainReadWait ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Interruptible terrain read disabled: invalid configuration", error);
}
try {
Properties properties = new Properties();
var rayFile = loader.getConfigDir().resolve("horizon-culling-rays.properties");
if (Files.exists(rayFile)) {
try (var reader = Files.newBufferedReader(rayFile)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("entityculling")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("1.11.2")).orElse(false);
cullingRayScratch = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"Culling ray scratch vectors: {} (requires pinned Entity Culling 1.11.2)", cullingRayScratch ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Culling ray scratch vectors disabled: invalid configuration", error);
}
try {
Properties properties = new Properties();
var positionFile = loader.getConfigDir().resolve("horizon-terrain-position.properties");
if (Files.exists(positionFile)) {
try (var reader = Files.newBufferedReader(positionFile)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("distanthorizons")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("3.3.4")).orElse(false);
terrainPosition = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"Relative terrain position arithmetic: {} (requires pinned DH 3.3.4)", terrainPosition ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Terrain position arithmetic disabled: invalid configuration", error);
}
try {
Properties properties = new Properties();
var quadFile = loader.getConfigDir().resolve("horizon-quad-reuse.properties");
if (Files.exists(quadFile)) {
try (var reader = Files.newBufferedReader(quadFile)) { properties.load(reader); }
}
boolean requested = Boolean.parseBoolean(properties.getProperty("enabled", "false"));
boolean supported = loader.getModContainer("distanthorizons")
.map(mod -> mod.getMetadata().getVersion().getFriendlyString().equals("3.3.4")).orElse(false);
quadReuse = requested && supported;
if (requested) LoggerFactory.getLogger("horizonlab").info(
"Discarded terrain quad reuse: {} (requires pinned DH 3.3.4)", quadReuse ? "enabled" : "unavailable");
} catch (Exception error) {
LoggerFactory.getLogger("horizonlab").warn("Terrain quad reuse disabled: invalid configuration", error);
}
}
public static boolean quadReuseEnabled() { return quadReuse; }
public static boolean terrainPositionEnabled() { return terrainPosition; }
public static boolean workerPacingEnabled() { return workerPacing; }
public static boolean debounceWaitEnabled() { return debounceWait; }
public static boolean dispatchRetryEnabled() { return dispatchRetry; }
public static boolean tickDedupEnabled() { return tickDedup; }
public static boolean cullingEmptyEnabled() { return cullingEmpty; }
public static boolean cullingRayScratchEnabled() { return cullingRayScratch; }
public static boolean terrainReadWaitEnabled() { return terrainReadWait; }
@Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
if (mixinClassName.endsWith("DhWorkerPacingMixin")) return workerPacing;
if (mixinClassName.endsWith("DhDebounceWaitMixin")) return debounceWait;
if (mixinClassName.endsWith("DhPriorityDispatchMixin")) return dispatchRetry;
if (mixinClassName.endsWith("DhTickDedupMixin")) return tickDedup;
if (mixinClassName.endsWith("CullingEmptyWhitelistMixin")) return cullingEmpty;
if (mixinClassName.endsWith("CullingRayScratchMixin")) return cullingRayScratch;
if (mixinClassName.endsWith("DhTerrainReadWaitMixin")) return terrainReadWait;
if (mixinClassName.endsWith("DhRelativePositionMixin")) return terrainPosition;
if (mixinClassName.endsWith("DhQuadReuseMixin")) return quadReuse;
return true;
}
@Override public String getRefMapperConfig() { return null; }
@Override public List<String> getMixins() { return null; }
@Override public void acceptTargets(Set<String> mine, Set<String> others) {}
@Override public void preApply(String name, ClassNode node, String mixin, IMixinInfo info) {}
@Override public void postApply(String name, ClassNode node, String mixin, IMixinInfo info) {}
}
@@ -1,25 +0,0 @@
package dev.horizonlab.mixin;
import dev.horizonlab.HorizonLab;
import java.util.function.BooleanSupplier;
import net.minecraft.server.MinecraftServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MinecraftServer.class)
public abstract class ServerTickMixin {
@Unique private long horizonTickStart;
@Inject(method = "tickServer", at = @At("HEAD"))
private void horizonBegin(BooleanSupplier haveTime, CallbackInfo callback) {
horizonTickStart = System.nanoTime();
}
@Inject(method = "tickServer", at = @At("RETURN"))
private void horizonEnd(BooleanSupplier haveTime, CallbackInfo callback) {
HorizonLab.tick(System.nanoTime() - horizonTickStart, (MinecraftServer) (Object) this);
}
}
@@ -1,3 +0,0 @@
{
"key.horizonlab.benchmark": "Start / stop performance recording (120 seconds)"
}
@@ -1,29 +0,0 @@
{
"schemaVersion": 1,
"id": "horizonlab",
"version": "0.21.0",
"name": "Horizon",
"description": "Client frame pacing and server tick measurements for reproducible optimization experiments.",
"authors": [
"Ludovic"
],
"license": "MIT",
"environment": "*",
"entrypoints": {
"main": [
"dev.horizonlab.HorizonLab"
],
"client": [
"dev.horizonlab.client.HorizonClient"
]
},
"mixins": [
"horizonlab.mixins.json"
],
"depends": {
"fabricloader": ">=0.19.5",
"fabric-api": "*",
"minecraft": "26.3",
"java": ">=25"
}
}
@@ -1,24 +0,0 @@
{
"required": true,
"package": "dev.horizonlab.mixin",
"plugin": "dev.horizonlab.mixin.HorizonMixinPlugin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"ServerTickMixin"
],
"client": [
"FrameMixin",
"DhWorkerPacingMixin",
"DhDebounceWaitMixin",
"DhPriorityDispatchMixin",
"DhTickDedupMixin",
"CullingEmptyWhitelistMixin",
"DhTerrainReadWaitMixin",
"CullingRayScratchMixin",
"DhRelativePositionMixin",
"DhQuadReuseMixin"
],
"injectors": {
"defaultRequire": 1
}
}
@@ -1,50 +0,0 @@
package dev.horizonlab.budget;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkPosQueue;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import java.lang.management.ManagementFactory;
import java.util.concurrent.locks.LockSupport;
/** Measures the actual upstream queue during a synthetic pending-debounce interval, not game FPS. */
public final class DebounceQueueBenchmark {
private static void run(boolean waiting, int size, boolean report) {
ChunkPosQueue queue = new ChunkPosQueue();
DhChunkPos closest = new DhChunkPos(0, 0);
ChunkUpdateData closestData = null;
for (int i = 0; i < size; i++) {
ChunkUpdateData update = new ChunkUpdateData(null, null);
if (i == 0) closestData = update;
queue.addItem(new DhChunkPos(i, 0), update);
}
var threads = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
long id = Thread.currentThread().threadId();
long allocated = threads.getThreadAllocatedBytes(id), cpu = threads.getCurrentThreadCpuTime();
long start = System.nanoTime(), debounce = 250_000_000L, loops = 0;
while (System.nanoTime() - start < debounce) {
ChunkUpdateData update = queue.popClosest();
if (update != closestData) throw new AssertionError("Queue priority or data changed");
queue.addItem(closest, update);
loops++;
if (waiting) {
long delay = DebounceWait.delay(System.nanoTime(), start, debounce, false);
if (delay > 0) LockSupport.parkNanos(delay);
}
}
long elapsed = System.nanoTime() - start;
cpu = threads.getCurrentThreadCpuTime() - cpu;
allocated = threads.getThreadAllocatedBytes(id) - allocated;
if (queue.getQueuedCount() != size || queue.popClosest() != closestData)
throw new AssertionError("Pending queue data changed");
if (report) System.out.printf(java.util.Locale.ROOT,
"mode=%s size=%d polls=%d worker_cpu_ms=%.3f wall_ms=%.3f worker_allocated_bytes=%d%n",
waiting ? "wait" : "upstream", size, loops, cpu / 1e6, elapsed / 1e6, allocated);
}
public static void main(String[] args) {
for (int size : new int[]{1, 64}) {
for (int i = 0; i < 2; i++) { run(false, size, false); run(true, size, false); }
for (int i = 0; i < 3; i++) { run(false, size, true); run(true, size, true); }
}
}
}
@@ -1,30 +0,0 @@
package dev.horizonlab.budget;
public final class DebounceWaitTest {
public static void main(String[] args) {
long debounce = 250_000_000L;
check(DebounceWait.delay(0, 0, debounce, false) == 1_000_000L, "bounded wait for a fresh update");
check(DebounceWait.delay(249_900_000L, 0, debounce, false) == 100_000L, "remaining deadline limits wait");
check(DebounceWait.delay(debounce, 0, debounce, false) == 0, "ready update is not delayed");
check(DebounceWait.delay(debounce + 1, 0, debounce, false) == 0, "overdue update is not delayed");
check(DebounceWait.delay(1, 0, debounce, true) == 0, "pending preprocessing is not delayed");
check(DebounceWait.delay(0, 1, debounce, false) == 0, "future timestamp does not create a wait");
check(DebounceWait.delay(0, 0, 0, false) == 0, "disabled debounce is not delayed");
long wrapped = Long.MAX_VALUE - 500_000L;
check(DebounceWait.delay(wrapped + 1_000_000L, wrapped, debounce, false) == 1_000_000L, "monotonic timer wrap");
long now = 0;
int waits = 0;
while (now < debounce) {
long wait = DebounceWait.delay(now, 0, debounce, false);
check(wait > 0 && wait <= DebounceWait.MAX_WAIT_NS && now + wait <= debounce, "deadline unchanged");
now += wait;
waits++;
}
check(now == debounce && waits == 250, "debounce deadline is reached without extending it");
System.out.println("Debounce wait policy tests passed.");
}
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -1,23 +0,0 @@
package dev.horizonlab.budget;
public final class DispatchRetryTest {
private static void check(boolean condition) { if (!condition) throw new AssertionError(); }
public static void main(String[] args) {
DispatchRetry policy = new DispatchRetry();
policy.beginPass();
policy.beginExecutor();
check(policy.visibleSlots(0) == 0 && policy.visibleSlots(3) == 3);
check(!policy.needsRetry(false, false) && policy.needsRetry(true, false));
check(policy.retryMilliseconds(1000) == 1000);
policy.rejected();
check(policy.visibleSlots(0) == Integer.MAX_VALUE && policy.needsRetry(false, false));
check(policy.retryMilliseconds(1000) == 1);
policy.beginExecutor();
check(policy.visibleSlots(1) == 1); // Another executor can consume a real free slot.
check(policy.needsRetry(false, false)); // Still retry the earlier rejected executor.
check(!policy.needsRetry(false, true) && !policy.needsRetry(true, true));
policy.beginPass();
check(!policy.needsRetry(false, false) && policy.retryMilliseconds(1000) == 1000);
System.out.println("Dispatch retry policy tests passed.");
}
}
@@ -1,63 +0,0 @@
package dev.horizonlab.budget;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkPosQueue;
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
/** Isolates wakeup after synthetic incoming work; does not measure terrain rendering or debounce. */
public final class IdleQueueBenchmark {
private static void run(boolean wakeable, boolean report, int trial) throws Exception {
var queue = new ChunkPosQueue();
var update = new ChunkUpdateData(null, null);
var ready = new CountDownLatch(1);
var published = new AtomicLong();
var idleStarted = new AtomicLong();
var latency = new AtomicLong();
var failure = new AtomicReference<Throwable>();
Thread worker = new Thread(() -> {
try {
idleStarted.set(System.nanoTime());
ready.countDown();
while (queue.isEmpty()) {
if (wakeable) WakeableIdleWait.pause(queue, 50_000_000L);
else Thread.sleep(50);
}
var item = queue.popClosest();
latency.set(System.nanoTime() - published.get());
if (item != update || queue.getQueuedCount() != 0)
throw new AssertionError("Queued work identity/count changed");
} catch (Throwable error) { failure.set(error); }
}, "Horizon idle queue benchmark");
worker.setDaemon(true);
worker.start();
try {
if (!ready.await(5, TimeUnit.SECONDS)) throw new AssertionError("Worker did not start");
LockSupport.parkNanos(5_000_000L);
long start = System.nanoTime();
published.set(start);
queue.addItem(new DhChunkPos(0, 0), update);
if (wakeable) LockSupport.unpark(worker);
worker.join(5_000);
if (worker.isAlive()) throw new AssertionError("Incoming work did not reach worker");
if (failure.get() != null) throw new AssertionError("Worker failed", failure.get());
if (latency.get() < 0) throw new AssertionError("Invalid timestamp ordering");
if (report) System.out.printf(Locale.ROOT,
"mode=%s trial=%d incoming_after_idle_ms=%.3f publication_to_pop_ms=%.3f%n",
wakeable ? "wakeable" : "upstream", trial, (start - idleStarted.get()) / 1e6, latency.get() / 1e6);
} finally {
worker.interrupt();
worker.join(1_000);
}
}
public static void main(String[] args) throws Exception {
for (int warmup = 0; warmup < 4; warmup++) { run(false, false, warmup); run(true, false, warmup); }
for (int trial = 0; trial < 20; trial++) { run(false, true, trial); run(true, true, trial); }
}
}
@@ -1,30 +0,0 @@
package dev.horizonlab.budget;
public final class TerrainBudgetTest {
private static void check(boolean value, String reason) { if (!value) throw new AssertionError(reason); }
public static void main(String[] args) {
TerrainBudget budget = new TerrainBudget(0.5);
for (int i = 0; i < 100; i++) budget.update(0.5, 40, 16.667, 0.5, 0.3);
check(budget.ratio() == 0.05, "Sustained pressure reaches a nonzero floor");
for (int i = 0; i < 2; i++) budget.update(0.5, 10, 16.667, 0, 0.3);
check(budget.ratio() == 0.05, "Recovery requires three healthy windows");
budget.update(0.5, 10, 16.667, 0, 0.3);
check(Math.abs(budget.ratio() - 0.075) < 1e-9, "Recovery is gradual");
for (int i = 0; i < 1000; i++) budget.update(0.5, 10, 16.667, 0, 0.3);
check(budget.ratio() == 0.5, "Recovery never exceeds the user's ceiling");
check(budget.update(0.02, 40, 16.667, 0.5, 0.3) == 0.02, "Respect a user ceiling below our floor");
budget = new TerrainBudget(0.5);
check(budget.update(0.5, 10, 16.667, 0, 0.9) < 0.5, "Heap pressure reduces work");
budget = new TerrainBudget(0.5);
check(budget.update(0.5, 33.3, 1000.0 / 30, 0, 0.3) == 0.5, "Respect a 30 FPS cap");
budget = new TerrainBudget(0.5);
for (int i = 0; i < 100; i++) {
budget.update(0.5, 40, 16.667, 0.5, 0.3);
budget.update(0.5, 10, 16.667, 0, 0.3);
}
check(budget.ratio() == 0.05, "Alternating pressure cannot trigger rapid recovery");
try { budget.update(0.5, Double.NaN, 16, 0, 0); throw new AssertionError("NaN accepted"); }
catch (IllegalArgumentException expected) { }
System.out.println("Terrain budget tests passed");
}
}
@@ -1,70 +0,0 @@
package dev.horizonlab.budget;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
public final class WakeableIdleWaitTest {
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
private static void workerCase(boolean interrupt, boolean beforePark) throws Exception {
var ready = new CountDownLatch(1);
var proceed = new AtomicBoolean(!beforePark);
var failure = new AtomicReference<Throwable>();
var completed = new AtomicBoolean();
Thread worker = new Thread(() -> {
ready.countDown();
// No blocking operation may consume the early unpark permit before the tested wait.
while (!proceed.get() && !Thread.currentThread().isInterrupted()) Thread.onSpinWait();
try {
WakeableIdleWait.pause(completed, TimeUnit.SECONDS.toNanos(30));
check(!interrupt, "Interrupt must throw");
completed.set(true);
} catch (InterruptedException expected) {
if (!interrupt) failure.set(expected);
else if (Thread.currentThread().isInterrupted()) failure.set(new AssertionError("Throw must clear interrupt status"));
else completed.set(true);
} catch (Throwable error) { failure.set(error); }
}, "Horizon idle-wait test");
worker.setDaemon(true);
worker.start();
try {
check(ready.await(5, TimeUnit.SECONDS), "Worker did not start");
if (!beforePark) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (worker.getState() != Thread.State.TIMED_WAITING && worker.isAlive() && System.nanoTime() < deadline)
Thread.onSpinWait();
check(worker.getState() == Thread.State.TIMED_WAITING, "Worker did not park");
}
if (interrupt) worker.interrupt(); else LockSupport.unpark(worker);
proceed.set(true);
worker.join(5_000);
check(!worker.isAlive(), "Wakeup was lost or shutdown stalled");
if (failure.get() != null) throw new AssertionError("Worker failed", failure.get());
check(completed.get(), "Wait did not complete");
} finally {
proceed.set(true);
worker.interrupt();
worker.join(1_000);
}
}
public static void main(String[] args) throws Exception {
Thread.currentThread().interrupt();
try {
WakeableIdleWait.pause(null, 1_000_000);
throw new AssertionError("Pending interrupt must throw");
} catch (InterruptedException expected) {
check(!Thread.currentThread().isInterrupted(), "Pending interrupt must be cleared");
}
workerCase(false, true); // Producer arrived between empty check and park.
workerCase(false, false); // Producer arrived while worker was idle.
workerCase(true, true);
workerCase(true, false);
System.out.println("Wakeable idle-wait arrival races and interruption tests passed.");
}
}
@@ -1,48 +0,0 @@
package dev.horizonlab.budget;
public final class WorkerPacerTest {
private static void check(boolean value, String reason) { if (!value) throw new AssertionError(reason); }
public static void main(String[] args) {
for (double ratio : new double[]{0.01, 0.05, 0.1, 0.5, 0.9, 1}) {
for (long work : new long[]{100, 10_000, 1_000_000, 100_000_000}) {
for (long overshoot : new long[]{80_000, 1_000_000, 15_000_000}) {
WorkerPacer pacer = new WorkerPacer();
long now = 1;
long active = 0;
long slept = 0;
for (int i = 0; i < 1000; i++) {
pacer.begin(now);
now += work;
active += work;
long delay = pacer.finish(now, ratio);
check(delay == 0 || delay >= WorkerPacer.QUANTUM_NS, "Small waits are batched");
if (delay != 0) {
// Simulate timer overshoot; future tasks must receive credit for it.
delay += overshoot;
now += delay;
slept += delay;
pacer.pauseCompleted(now);
}
}
double expectedSleep = active * (1.0 / ratio - 1);
check(Math.abs(slept - expectedSleep) <= WorkerPacer.QUANTUM_NS + overshoot + 1,
"Duty cycle retained across job sizes and timer overshoot: " + ratio + "/" + work);
}
}
}
WorkerPacer pacer = new WorkerPacer();
pacer.begin(1); pacer.finish(10_001, 0.5);
pacer.begin(20_001);
check(pacer.finish(30_001, 1) == 0, "A full-duty configuration discards old delay debt");
for (double invalid : new double[]{0, -1, Double.NaN, Double.POSITIVE_INFINITY}) {
pacer.begin(40_001);
check(pacer.finish(50_001, invalid) == 0, "Invalid ratios cannot cause unbounded waits");
}
pacer.begin(60_001); pacer.finish(70_001, 0.5);
pacer.begin(2_000_070_001L);
check(pacer.finish(2_000_080_001L, 0.5) == 0, "Idle workers discard stale delay debt");
pacer.interrupted(2_000_080_001L);
System.out.println("Worker pacing tests passed");
}
}
@@ -1,64 +0,0 @@
package dev.horizonlab.budget;
import com.seibel.distanthorizons.core.util.threading.RateLimitedThreadPoolExecutor;
import java.lang.management.ManagementFactory;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
public class WorkerPacingBenchmark {
static class PrecisePool extends RateLimitedThreadPoolExecutor {
final ThreadLocal<dev.horizonlab.budget.WorkerPacer> pacing =
ThreadLocal.withInitial(dev.horizonlab.budget.WorkerPacer::new);
PrecisePool(java.util.concurrent.ThreadFactory factory) { super(1, factory, new LinkedBlockingQueue<>()); }
@Override protected void beforeExecute(Thread thread, Runnable job) { pacing.get().begin(System.nanoTime()); }
@Override protected void afterExecute(Runnable job, Throwable error) {
var state = pacing.get();
long wait = state.finish(System.nanoTime(), runTimeRatioConfig.get());
if (wait > 0) {
try { TimeUnit.NANOSECONDS.sleep(wait); state.pauseCompleted(System.nanoTime()); }
catch (InterruptedException interrupted) { state.interrupted(System.nanoTime()); }
}
}
}
public static void main(String[] args) throws Exception {
Class.forName("com.seibel.distanthorizons.core.config.eventHandlers.presets.ThreadPresetConfigEventHandler");
AtomicReference<Thread> worker = new AtomicReference<>();
java.util.concurrent.ThreadFactory factory = runnable -> {
Thread thread = new Thread(runnable, "Horizon probe worker"); worker.set(thread); return thread;
};
boolean precise = args.length > 3 && args[3].equals("precise");
var pool = precise ? new PrecisePool(factory) : new RateLimitedThreadPoolExecutor(1, factory, new LinkedBlockingQueue<>());
double ratio = Double.parseDouble(args[0]);
pool.runTimeRatioConfig.setApiValue(ratio, "Horizon probe");
pool.prestartAllCoreThreads();
CountDownLatch warmed = new CountDownLatch(500);
for (int i = 0; i < 500; i++) pool.execute(warmed::countDown);
warmed.await();
var bean = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
long id = worker.get().threadId();
long initialCpu = bean.getThreadCpuTime(id);
long initialAllocation = bean.getThreadAllocatedBytes(id);
int tasks = args.length > 2 ? Integer.parseInt(args[2]) : 10_000;
long workNs = args.length > 1 ? Long.parseLong(args[1]) : 10_000;
CountDownLatch done = new CountDownLatch(tasks);
Runnable job = () -> {
long end = System.nanoTime() + workNs;
while (System.nanoTime() < end) Thread.onSpinWait();
done.countDown();
};
long started = System.nanoTime();
for (int i = 0; i < tasks; i++) pool.execute(job);
done.await();
long elapsed = System.nanoTime() - started;
long endCpu = bean.getThreadCpuTime(id);
long endAllocation = bean.getThreadAllocatedBytes(id);
long cpu = initialCpu < 0 || endCpu < initialCpu ? -1 : endCpu - initialCpu;
long allocated = initialAllocation < 0 || endAllocation < initialAllocation ? -1 : endAllocation - initialAllocation;
pool.shutdown(); pool.awaitTermination(10, TimeUnit.SECONDS);
String cpuResult = cpu < 0 ? "unavailable" : String.format(java.util.Locale.ROOT, "%.3f", cpu / (double) elapsed);
System.out.printf(java.util.Locale.ROOT,
"%s requested ratio %.3f; %d tasks of %d ns; worker CPU/wall %s; %.3f ms; worker allocation %s bytes%n",
precise ? "precise" : "upstream", ratio, tasks, workNs, cpuResult, elapsed / 1e6,
allocated < 0 ? "unavailable" : Long.toString(allocated));
}
}
@@ -1,56 +0,0 @@
package dev.horizonlab.client;
import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfig;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiMultiThreadingConfig;
import java.lang.reflect.Proxy;
/** Exercise the actual adapter against the upstream API contracts, without a game process. */
public final class DistantTerrainBudgetTest {
private static Double override;
private static double userValue = 0.5;
@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type, java.lang.reflect.InvocationHandler handler) {
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler);
}
private static void check(boolean value, String reason) { if (!value) throw new AssertionError(reason); }
public static void main(String[] args) {
IDhApiConfigValue<?> value = proxy(IDhApiConfigValue.class, (p, method, values) -> switch (method.getName()) {
case "getTrueValue" -> userValue;
case "getValue" -> override == null ? userValue : override;
case "getApiValue" -> override;
case "getCanBeOverrodeByApi" -> true;
case "setValue" -> { override = (Double) values[0]; yield true; }
case "clearValue" -> { override = null; yield true; }
default -> throw new AssertionError("Unexpected API call: " + method);
});
var threading = proxy(IDhApiMultiThreadingConfig.class, (p, method, values) -> value);
DhApi.Delayed.configs = proxy(IDhApiConfig.class, (p, method, values) -> threading);
DistantTerrainBudget budget = new DistantTerrainBudget();
budget.observe(1, 0, 16.667);
budget.observe(3_000_000_001L, 40_000_000, 16.667);
check(Math.abs(override - 0.325) < 1e-9, "Actual adapter applies the policy through the API");
budget.suspend();
check(override == null && userValue == 0.5, "Suspending releases only our temporary override");
budget.observe(4_000_000_001L, 0, 16.667);
budget.observe(7_000_000_001L, 40_000_000, 16.667);
check(Math.abs(override - 0.325) < 1e-9, "Resuming resets stale frame observations and policy state");
userValue = 0.1;
budget.observe(8_000_000_001L, 40_000_000, 16.667);
budget.observe(11_000_000_001L, 40_000_000, 16.667);
check(override <= 0.1, "A changed user ceiling is honored");
override = 0.7;
budget.observe(12_000_000_001L, 40_000_000, 16.667);
budget.suspend();
check(override == 0.7 && budget.status().startsWith("yielded"), "Never clear another mod's changed override");
budget = new DistantTerrainBudget();
budget.observe(13_000_000_001L, 40_000_000, 16.667);
check(override == 0.7 && budget.status().startsWith("yielded"), "Never acquire an existing override");
System.out.println("Distant terrain API adapter tests passed");
}
}
@@ -1,59 +0,0 @@
package dev.horizonlab.client;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfig;
import com.seibel.distanthorizons.api.interfaces.config.IDhApiConfigValue;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiAmbientOcclusionConfig;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiGraphicsConfig;
import com.seibel.distanthorizons.api.interfaces.config.client.IDhApiMultiThreadingConfig;
import com.seibel.distanthorizons.api.enums.config.*;
import com.seibel.distanthorizons.api.enums.rendering.*;
import java.lang.reflect.Proxy;
import java.util.Map;
public final class DistantTerrainSettingsTest {
@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type, java.lang.reflect.InvocationHandler handler) {
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler);
}
private static IDhApiConfigValue<?> effective(Object value) {
return proxy(IDhApiConfigValue.class, (p, method, args) -> {
if (method.getName().equals("getValue")) return value;
throw new AssertionError("Capture must read effective overrides, never mutate settings: " + method);
});
}
private static void check(boolean condition) { if (!condition) throw new AssertionError(); }
public static void main(String[] args) {
Map<String, Object> settings = Map.ofEntries(
Map.entry("renderingEnabled", true), Map.entry("renderingMode", EDhApiRendererMode.DEFAULT),
Map.entry("chunkRenderDistance", 256), Map.entry("horizontalQuality", EDhApiHorizontalQuality.MEDIUM),
Map.entry("verticalQuality", EDhApiVerticalQuality.HIGH), Map.entry("maxHorizontalResolution", EDhApiMaxHorizontalResolution.BLOCK),
Map.entry("transparency", EDhApiTransparency.COMPLETE), Map.entry("lodShading", EDhApiLodShading.AUTO),
Map.entry("lodOnlyMode", false), Map.entry("useCameraPositionForQualityDropOff", true));
var ao = proxy(IDhApiAmbientOcclusionConfig.class, (p, method, values) -> effective(true));
var graphics = proxy(IDhApiGraphicsConfig.class, (p, method, values) ->
method.getName().equals("ambientOcclusion") ? ao : effective(settings.get(method.getName())));
var threading = proxy(IDhApiMultiThreadingConfig.class, (p, method, values) ->
effective(method.getName().equals("threadCount") ? (Object) 2 : 0.05));
var configs = proxy(IDhApiConfig.class, (p, method, values) ->
method.getName().equals("graphics") ? graphics : threading);
Map<String, Object> result = DistantTerrainSettings.read(configs);
check(result.get("dh_capture_status").equals("available"));
check(result.get("dh_lod_radius_chunks").equals(256));
check(result.get("dh_horizontal_quality").equals("MEDIUM"));
check(result.get("dh_vertical_quality").equals("HIGH"));
check(result.get("dh_runtime_ratio").equals(0.05));
check(result.get("dh_thread_count").equals(2));
check(DistantTerrainSettings.read(null).equals(Map.of("dh_capture_status", "unavailable")));
var missing = proxy(IDhApiConfig.class, (p, method, values) -> { throw new UnsupportedOperationException(); });
check(DistantTerrainSettings.read(missing).equals(Map.of("dh_capture_status", "unavailable")));
var changedApi = proxy(IDhApiConfig.class, (p, method, values) -> { throw new NoSuchMethodError(); });
check(DistantTerrainSettings.read(changedApi).equals(Map.of("dh_capture_status", "unavailable")));
check(ShaderSettings.read(false).equals(Map.of("iris_capture_status", "not_installed", "iris_shaders_enabled", false)));
// Test classpath deliberately omits Iris; it must report unavailable rather than assume disabled.
check(ShaderSettings.read(true).equals(Map.of("iris_capture_status", "unavailable")));
System.out.println("Effective terrain settings and absent/unsupported shader APIs checked.");
}
}
@@ -1,100 +0,0 @@
package dev.horizonlab.dispatch;
import dev.horizonlab.mixin.HorizonMixinPlugin;
import dev.tr7zw.entityculling.EntityCullingModBase;
import dev.tr7zw.entityculling.access.Cullable;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.function.Function;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
/** Private fixture validates applied redirects and live handler registration on the publisher's instance. */
public final class CullingEmptyIntegration implements ClientModInitializer {
private static volatile boolean result;
private static final class CountingSet<E> extends HashSet<E> {
int iterators;
@Override public Iterator<E> iterator() { iterators++; return super.iterator(); }
}
@Override public void onInitializeClient() {
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
try { exercise(); }
catch (ReflectiveOperationException failure) { throw new AssertionError("Culling integration failed", failure); }
if (Boolean.getBoolean("horizon.fixture.exit")) System.exit(0);
});
}
private static void exercise() throws ReflectiveOperationException {
var owner = EntityCullingModBase.instance;
if (owner == null) throw new AssertionError("Publisher culling mod was not initialized");
Field entity = EntityCullingModBase.class.getDeclaredField("dynamicEntityWhitelist");
Field block = EntityCullingModBase.class.getDeclaredField("dynamicBlockEntityWhitelist");
entity.setAccessible(true); block.setAccessible(true);
Object oldEntity = entity.get(owner), oldBlock = block.get(owner);
boolean enabled = HorizonMixinPlugin.cullingEmptyEnabled();
var entities = new CountingSet<Function<net.minecraft.world.entity.Entity, Boolean>>();
var blocks = new CountingSet<Function<BlockEntity, Boolean>>();
var chest = new ChestBlockEntity(BlockPos.ZERO, Blocks.CHEST.defaultBlockState());
var subject = (Cullable) (Object) chest;
try {
entity.set(owner, entities); block.set(owner, blocks);
if (owner.isEntityDynamicWhitelisted(null) || owner.isBlockEntityDynamicWhitelisted(subject))
throw new AssertionError("Empty whitelist unexpectedly matched");
int expectedEmpty = enabled ? 0 : 1;
if (entities.iterators != expectedEmpty || blocks.iterators != expectedEmpty)
throw new AssertionError("Empty whitelist iterator hook did not apply");
var called = new ArrayList<Integer>();
Function<BlockEntity, Boolean> first = value -> { called.add(1); return false; };
Function<BlockEntity, Boolean> second = value -> { called.add(2); return value == chest; };
owner.addDynamicBlockEntityWhitelist(first);
owner.addDynamicBlockEntityWhitelist(second);
owner.addDynamicBlockEntityWhitelist(first); // Duplicate registration remains a set operation.
var expected = new ArrayList<Integer>();
for (var handler : blocks) { expected.add(handler == first ? 1 : 2); if (handler == second) break; }
int before = blocks.iterators;
if (!owner.isBlockEntityDynamicWhitelisted(subject) || !called.equals(expected) || blocks.iterators != before + 1)
throw new AssertionError("Live registration, callback order or short circuit changed");
int callbackCount = called.size();
if (owner.isBlockEntityDynamicWhitelisted(null) || called.size() != callbackCount)
throw new AssertionError("Null unexpectedly matched or invoked a callback");
owner.addDynamicEntityWhitelist(value -> { throw new AssertionError("Wrong entity type invoked handler"); });
before = entities.iterators;
if (owner.isEntityDynamicWhitelisted(subject) || entities.iterators != before + 1)
throw new AssertionError("Nonempty entity iterator or type guard changed");
blocks.clear(); entities.clear(); before = blocks.iterators;
if (owner.isBlockEntityDynamicWhitelisted(subject) || blocks.iterators != before + expectedEmpty)
throw new AssertionError("Empty state after clear was cached incorrectly");
// Normal HashSets for the allocation observation; instrumentation is not in this loop.
entity.set(owner, new HashSet<>()); block.set(owner, new HashSet<>());
loop(owner, 200000);
var bean = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
bean.setThreadAllocatedMemoryEnabled(true); bean.setThreadCpuTimeEnabled(true);
long id = Thread.currentThread().threadId(), bytes = bean.getThreadAllocatedBytes(id);
long cpu = bean.getCurrentThreadCpuTime(), wall = System.nanoTime();
loop(owner, 2000000);
long wallDelta = System.nanoTime() - wall, cpuDelta = bean.getCurrentThreadCpuTime() - cpu;
long byteDelta = bean.getThreadAllocatedBytes(id) - bytes;
System.out.println("HORIZON_CULLING_EMPTY_RESULT {\"enabled\":" + enabled
+ ",\"empty_iterator_calls\":" + expectedEmpty + ",\"live_registration_checked\":true"
+ ",\"block_callback_order_checked\":true,\"type_guard_checked\":true"
+ ",\"allocated_bytes\":" + byteDelta + ",\"cpu_ns\":" + cpuDelta + ",\"wall_ns\":" + wallDelta
+ ",\"measured_calls\":4000000,\"result\":" + result + "}");
} finally { entity.set(owner, oldEntity); block.set(owner, oldBlock); }
}
private static void loop(EntityCullingModBase owner, int iterations) {
boolean found = false;
for (int index = 0; index < iterations; index++) {
found |= owner.isEntityDynamicWhitelisted(null);
found |= owner.isBlockEntityDynamicWhitelisted(null);
}
result = found;
}
}
@@ -1,148 +0,0 @@
package dev.horizonlab.dispatch;
import com.logisticscraft.occlusionculling.DataProvider;
import com.logisticscraft.occlusionculling.OcclusionCullingInstance;
import com.logisticscraft.occlusionculling.util.Vec3d;
import dev.horizonlab.mixin.HorizonMixinPlugin;
import dev.horizonlab.dispatch.fixture.CullingRayAccess;
import java.lang.management.ManagementFactory;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.Random;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
/** Private applied-hook fixture; baseline and candidate run the same publisher geometry. */
public final class CullingRayIntegration implements ClientModInitializer {
private static volatile long checksum;
private static final class Provider implements DataProvider {
final MessageDigest digest;
final byte[] coordinates = new byte[12];
final int pattern;
int queries;
Provider(int pattern) throws Exception { this.pattern = pattern; digest = MessageDigest.getInstance("SHA-256"); }
private void number(int offset, int value) {
for (int byteIndex = 0; byteIndex < 4; byteIndex++) coordinates[offset + byteIndex] = (byte) (value >>> (24 - 8 * byteIndex));
}
@Override public boolean prepareChunk(int x, int z) {
number(0, x); number(4, z); number(8, 0x1122); digest.update(coordinates); return true;
}
@Override public boolean isOpaqueFullCube(int x, int y, int z) {
number(0, x); number(4, y); number(8, z); digest.update(coordinates); queries++;
return switch (pattern) {
case 0 -> false;
case 1 -> true;
case 2 -> x == 0;
case 3 -> y == 0;
case 4 -> z == 0;
case 5 -> ((x + y + z) & 1) == 0;
case 6 -> x % 3 == 0 && z % 3 == 0;
default -> Math.floorMod(x * 73856093 ^ y * 19349663 ^ z * 83492791, 7) < 2;
};
}
}
@Override public void onInitializeClient() {
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
try { exercise(); }
catch (Exception failure) { throw new AssertionError("Culling ray fixture failed", failure); }
if (Boolean.getBoolean("horizon.fixture.exit")) System.exit(0);
});
}
private static void bit(MessageDigest digest, boolean value) { digest.update((byte) (value ? 1 : 0)); }
private static Vec3d scratch(OcclusionCullingInstance owner, String fieldName) throws Exception {
var field = owner.getClass().getDeclaredField(fieldName); field.setAccessible(true); return (Vec3d) field.get(owner);
}
private static void same(Vec3d actual, Vec3d expected) {
if (Double.doubleToLongBits(actual.x) != Double.doubleToLongBits(expected.x)
|| Double.doubleToLongBits(actual.y) != Double.doubleToLongBits(expected.y)
|| Double.doubleToLongBits(actual.z) != Double.doubleToLongBits(expected.z))
throw new AssertionError("Scratch reset or upstream arithmetic changed: actual=" + actual.x + "," + actual.y + "," + actual.z
+ " expected=" + expected.x + "," + expected.y + "," + expected.z);
}
private static void exercise() throws Exception {
boolean enabled = HorizonMixinPlugin.cullingRayScratchEnabled();
var visibility = MessageDigest.getInstance("SHA-256");
var queries = MessageDigest.getInstance("SHA-256");
var intersections = MessageDigest.getInstance("SHA-256");
var random = new Random(0x4a19);
int boxes = 0, reads = 0;
for (int pattern = 0; pattern < 8; pattern++) {
var firstProvider = new Provider(pattern); var secondProvider = new Provider(pattern);
var first = new OcclusionCullingInstance(16, firstProvider);
var second = new OcclusionCullingInstance(16, secondProvider);
if (enabled) {
var direction = scratch(first, "horizon$rayDirection");
var inverse = scratch(first, "horizon$inverseDirection");
if (direction == inverse || direction == scratch(second, "horizon$rayDirection")
|| inverse == scratch(second, "horizon$inverseDirection")) throw new AssertionError("Scratch state was shared");
}
Vec3d camera = new Vec3d(-2.5, 0.5, 2.5), otherCamera = new Vec3d(2.5, -0.5, -2.5);
for (int index = 0; index < 800; index++) {
if (index % 17 == 0) { first.resetCache(); second.resetCache(); }
double x = random.nextInt(15) - 7 + (index % 3 == 0 ? 0 : random.nextDouble());
double y = random.nextInt(15) - 7 + (index % 5 == 0 ? 0 : random.nextDouble());
double z = random.nextInt(15) - 7 + (index % 7 == 0 ? 0 : random.nextDouble());
var min = new Vec3d(x, y, z);
var max = new Vec3d(x + (index % 11 == 0 ? 0 : random.nextDouble() * 2),
y + (index % 13 == 0 ? 0 : random.nextDouble() * 2), z + (index % 19 == 0 ? 0 : random.nextDouble() * 2));
bit(visibility, first.isAABBVisible(min, max, camera));
bit(visibility, second.isAABBVisible(min, max, otherCamera));
boxes += 2;
}
queries.update(firstProvider.digest.digest()); queries.update(secondProvider.digest.digest());
reads += firstProvider.queries + secondProvider.queries;
}
var owner = new OcclusionCullingInstance(16, new Provider(1));
var access = (CullingRayAccess) owner;
var camera = new Vec3d(-2, -2, -2);
double[] values = {0.0, -0.0, 1, -1, Double.MIN_VALUE, -Double.MIN_VALUE, 1e-300, -1e-300,
1e300, -1e300, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN};
int rays = 0;
for (double x : values) for (double y : values) for (double z : values) {
var input = new Vec3d(x, y, z);
bit(intersections, access.horizon$intersects(new int[]{0, 0, 0}, camera, input));
same(input, new Vec3d(x, y, z));
if (enabled) same(scratch(owner, "horizon$inverseDirection"), new Vec3d(1, 1, 1).div(input));
rays++;
}
var points = new Vec3d[64];
for (int index = 0; index < points.length; index++) points[index] = new Vec3d(4 + index * 0.001, 4, 4);
java.util.Arrays.fill(access.horizon$lastBlock(), 0);
java.util.Arrays.fill(access.horizon$cameraBlock(), -2);
var directionObject = enabled ? scratch(owner, "horizon$rayDirection") : null;
var inverseObject = enabled ? scratch(owner, "horizon$inverseDirection") : null;
for (var point : points) {
access.horizon$checks(true);
bit(intersections, access.horizon$visible(camera, new Vec3d[]{point}, 1));
if (enabled) {
var expected = new Vec3d(camera.x - point.x, camera.y - point.y, camera.z - point.z).normalize();
same(directionObject, expected); same(inverseObject, new Vec3d(1, 1, 1).div(expected));
}
}
loop(access, camera, points, 200000);
var bean = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
bean.setThreadAllocatedMemoryEnabled(true); bean.setThreadCpuTimeEnabled(true);
long id = Thread.currentThread().threadId(), bytes = bean.getThreadAllocatedBytes(id);
long cpu = bean.getCurrentThreadCpuTime(), wall = System.nanoTime();
loop(access, camera, points, 2000000);
long wallDelta = System.nanoTime() - wall, cpuDelta = bean.getCurrentThreadCpuTime() - cpu;
long byteDelta = bean.getThreadAllocatedBytes(id) - bytes;
if (enabled && (scratch(owner, "horizon$rayDirection") != directionObject
|| scratch(owner, "horizon$inverseDirection") != inverseObject)) throw new AssertionError("Scratch object was replaced");
System.out.println("HORIZON_CULLING_RAYS_RESULT {\"enabled\":" + enabled + ",\"public_boxes\":" + boxes
+ ",\"provider_reads\":" + reads + ",\"edge_rays\":" + rays + ",\"visibility_digest\":\""
+ HexFormat.of().formatHex(visibility.digest()) + "\",\"query_digest\":\""
+ HexFormat.of().formatHex(queries.digest()) + "\",\"intersection_digest\":\""
+ HexFormat.of().formatHex(intersections.digest()) + "\",\"allocated_bytes\":" + byteDelta
+ ",\"cpu_ns\":" + cpuDelta + ",\"wall_ns\":" + wallDelta + ",\"measured_checks\":2000000"
+ ",\"checksum\":" + checksum + ",\"input_unchanged\":true,\"scratch_ownership_checked\":true}");
}
private static void loop(CullingRayAccess access, Vec3d camera, Vec3d[] points, int count) {
var one = new Vec3d[1]; long sum = 0;
for (int index = 0; index < count; index++) {
one[0] = points[index & 63]; access.horizon$checks(true);
if (access.horizon$visible(camera, one, 1)) sum++;
}
checksum = sum;
}
}
@@ -1,108 +0,0 @@
package dev.horizonlab.dispatch;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.RateLimitedThreadPoolExecutor;
import dev.horizonlab.mixin.HorizonMixinPlugin;
import java.lang.reflect.Field;
import java.lang.management.ManagementFactory;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
/** Private fixture mod, excluded from the production JAR. Run inside Fabric to test applied hooks. */
public final class DispatchIntegration implements ClientModInitializer {
@Override public void onInitializeClient() {
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
try { exercise(HorizonMixinPlugin.dispatchRetryEnabled()); }
catch (Exception failure) { throw new AssertionError("Dispatch integration failed", failure); }
if (Boolean.getBoolean("horizon.fixture.exit")) System.exit(0);
});
}
private static void exercise(boolean enabled) throws Exception {
PriorityTaskPicker picker = new PriorityTaskPicker();
var blocked = picker.createExecutor("Horizon fixture blocked", 1);
var independent = picker.createExecutor("Horizon fixture independent", 2);
Field poolField = blocked.getClass().getDeclaredField("threadPoolExecutor");
poolField.setAccessible(true);
((RateLimitedThreadPoolExecutor) poolField.get(blocked)).shutdownNow();
var physical = new RateLimitedThreadPoolExecutor(1, runnable -> {
Thread thread = new Thread(runnable, "Horizon fixture physical");
thread.setDaemon(true);
return thread;
}, new ArrayBlockingQueue<Runnable>(1));
AtomicInteger rejections = new AtomicInteger();
var abort = new ThreadPoolExecutor.AbortPolicy();
physical.setRejectedExecutionHandler((task, pool) -> {
rejections.incrementAndGet();
abort.rejectedExecution(task, pool);
});
poolField.set(blocked, physical);
CountDownLatch entered = new CountDownLatch(1), release = new CountDownLatch(1);
CountDownLatch submitted = new CountDownLatch(1), completed = new CountDownLatch(2);
AtomicInteger firstRuns = new AtomicInteger(), secondRuns = new AtomicInteger();
AtomicLong callerBytes = new AtomicLong(), callerCpu = new AtomicLong(), callerWall = new AtomicLong();
AtomicReference<Throwable> callerFailure = new AtomicReference<>();
var threads = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
threads.setThreadAllocatedMemoryEnabled(true);
threads.setThreadCpuTimeEnabled(true);
try {
// Physical work is deliberately outside the picker: reproduce a busy executor with free logical slots.
physical.execute(() -> {
entered.countDown();
try { release.await(10, TimeUnit.SECONDS); }
catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); }
});
if (!entered.await(5, TimeUnit.SECONDS)) throw new AssertionError("Physical worker did not start");
physical.execute(() -> {}); // Fill its only physical queue slot.
Thread caller = new Thread(() -> {
long id = Thread.currentThread().threadId();
long bytes = threads.getThreadAllocatedBytes(id), cpu = threads.getCurrentThreadCpuTime(), wall = System.nanoTime();
try { blocked.execute(() -> { firstRuns.incrementAndGet(); completed.countDown(); }); }
catch (Throwable failure) { callerFailure.set(failure); }
finally {
callerWall.set(System.nanoTime() - wall);
callerBytes.set(threads.getThreadAllocatedBytes(id) - bytes);
callerCpu.set(threads.getCurrentThreadCpuTime() - cpu);
submitted.countDown();
}
}, "Horizon fixture caller");
caller.setDaemon(true);
caller.start();
boolean returned = submitted.await(enabled ? 2000 : 200, TimeUnit.MILLISECONDS);
if (returned != enabled) throw new AssertionError("Unexpected rejection lock behavior: hook=" + enabled);
independent.execute(() -> { secondRuns.incrementAndGet(); completed.countDown(); });
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
if (enabled) {
while (secondRuns.get() == 0 && System.nanoTime() < deadline) Thread.sleep(1);
if (secondRuns.get() != 1 || firstRuns.get() != 0) throw new AssertionError("Blocked executor prevented independent work");
} else if (secondRuns.get() != 0) throw new AssertionError("Upstream did not reproduce dispatcher blocking");
release.countDown();
if (!completed.await(5, TimeUnit.SECONDS)) throw new AssertionError("Requeued task was lost");
caller.join(5000);
if (caller.isAlive() || callerFailure.get() != null) throw new AssertionError("Submission failed", callerFailure.get());
Field occupiedField = PriorityTaskPicker.class.getDeclaredField("occupiedThreadsRef");
occupiedField.setAccessible(true);
AtomicInteger occupied = (AtomicInteger) occupiedField.get(picker);
deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (occupied.get() != 0 && System.nanoTime() < deadline) Thread.sleep(1);
if (occupied.get() != 0 || firstRuns.get() != 1 || secondRuns.get() != 1
|| blocked.getQueueSize() != 0 || independent.getQueueSize() != 0)
throw new AssertionError("Task count, queue, or occupied slots corrupted");
System.out.println("HORIZON_DISPATCH_RESULT {\"enabled\":" + enabled
+ ",\"submission_allocated_bytes\":" + callerBytes.get()
+ ",\"submission_cpu_ns\":" + callerCpu.get() + ",\"submission_wall_ns\":" + callerWall.get()
+ ",\"rejections\":" + rejections.get() + ",\"tasks_completed\":2,\"occupied_slots\":0}");
} finally {
release.countDown();
picker.shutdownNow();
if (!physical.awaitTermination(5, TimeUnit.SECONDS)) throw new AssertionError("Fixture worker did not shut down");
}
}
}
@@ -1,121 +0,0 @@
package dev.horizonlab.dispatch;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import dev.horizonlab.mixin.HorizonMixinPlugin;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import java.lang.management.ManagementFactory;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.Random;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
/** Private applied-hook fixture, using generated column data and the actual public lookup. */
public final class PositionIntegration implements ClientModInitializer {
private static volatile long checksum;
@Override public void onInitializeClient() {
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
try { exercise(); }
catch (Exception failure) { throw new AssertionError("Terrain position fixture failed", failure); }
if (Boolean.getBoolean("horizon.fixture.exit")) System.exit(0);
});
}
private static FullDataSourceV2 source(byte level, int x, int z) throws Exception {
var owner = FullDataSourceV2.createEmpty(DhSectionPos.encode(level, x, z));
for (int cx = 0; cx < 64; cx++) for (int cz = 0; cz < 64; cz++) {
var column = new LongArrayList();
for (int layer = 2; layer >= 0; layer--) {
long point = FullDataPointUtil.encode(1 + ((cx * 7 + cz * 11 + layer) & 255), 32,
layer * 32, (byte) 15, (byte) (layer * 3));
if (FullDataPointUtil.getHeight(point) != 32 || FullDataPointUtil.getBottomY(point) != layer * 32)
throw new AssertionError("Generated publisher data has wrong height");
column.add(point);
}
owner.dataPoints[FullDataSourceV2.relativePosToIndex(cx, cz)] = column;
}
return owner;
}
private static long dataFingerprint(FullDataSourceV2 owner) {
long hash = owner.getPos();
for (var column : owner.dataPoints) {
hash = hash * 31 + column.size();
for (int index = 0; index < column.size(); index++) hash = hash * 31 + column.getLong(index);
}
return hash;
}
private static void exercise() throws Exception {
boolean enabled = HorizonMixinPlugin.terrainPositionEnabled();
boolean applied = java.util.Arrays.stream(FullDataSourceV2.class.getDeclaredMethods())
.anyMatch(method -> method.getName().contains("horizon$relativePosition"));
if (applied != enabled) throw new AssertionError("Lookup redirect does not match requested state");
var digest = MessageDigest.getInstance("SHA-256");
byte[] bytes = new byte[8];
var random = new Random(0x49a91);
int queries = 0, nonzero = 0, owners = 0;
long fingerprints = 0;
for (byte level : new byte[] {6, 7, 8, 10, 14, 20}) {
for (int[] origin : new int[][] {{-1, -1}, {-7, 9}, {0, 0}, {13, -21}, {11, -13}}) {
var owner = source(level, origin[0], origin[1]);
long before = dataFingerprint(owner);
int minX = DhSectionPos.getMinCornerBlockX(owner.getPos());
int minZ = DhSectionPos.getMinCornerBlockZ(owner.getPos());
int width = DhSectionPos.getDetailLevelWidthInBlocks(level);
for (int index = 0; index < 16384; index++) {
int x = minX + random.nextInt(width), z = minZ + random.nextInt(width);
if ((index & 1023) == 0) x = minX - 1;
if ((index & 1023) == 1) z = minZ + width;
int y = random.nextInt(144) - 80;
long value = owner.getDataPointAtBlockPos(x, y, z, -64);
if (value != 0) nonzero++;
for (int at = 0; at < 8; at++) bytes[at] = (byte) (value >>> (56 - 8 * at));
digest.update(bytes); queries++;
}
if (before != dataFingerprint(owner)) throw new AssertionError("Lookup mutated source data");
fingerprints = fingerprints * 31 + before; owners++;
}
}
if (nonzero < 10000) throw new AssertionError("Fixture did not exercise populated columns");
var owner = source((byte) 10, -13, -7);
int[] xs = new int[256], zs = new int[256];
int minX = DhSectionPos.getMinCornerBlockX(owner.getPos()), minZ = DhSectionPos.getMinCornerBlockZ(owner.getPos());
for (int index = 0; index < 256; index++) { xs[index] = minX + index * 3; zs[index] = minZ + (index * 37 & 1023); }
loop(owner, xs, zs, 10000000);
Thread.sleep(1000);
var cpu = ManagementFactory.getThreadMXBean();
var allocations = (com.sun.management.ThreadMXBean) cpu;
if (allocations.isThreadAllocatedMemorySupported() && !allocations.isThreadAllocatedMemoryEnabled())
allocations.setThreadAllocatedMemoryEnabled(true);
long tid = Thread.currentThread().threadId();
long wall = 0, cpuTime = 0, allocated = 0;
StringBuilder observations = new StringBuilder("[");
for (int repeat = 0; repeat < 3; repeat++) {
loop(owner, xs, zs, 4000000);
Thread.sleep(200);
long allocationStart = allocations.getThreadAllocatedBytes(tid), cpuStart = cpu.getCurrentThreadCpuTime(), start = System.nanoTime();
loop(owner, xs, zs, 2000000);
long observedWall = System.nanoTime() - start, observedCpu = cpu.getCurrentThreadCpuTime() - cpuStart;
long observedAllocation = allocations.getThreadAllocatedBytes(tid) - allocationStart;
wall += observedWall; cpuTime += observedCpu; allocated += observedAllocation;
if (repeat != 0) observations.append(',');
observations.append("{\"cpu_ns\":").append(observedCpu).append(",\"wall_ns\":").append(observedWall)
.append(",\"allocated_bytes\":").append(observedAllocation).append('}');
}
observations.append(']');
System.out.println("HORIZON_POSITION_RESULT {\"enabled\":" + enabled + ",\"applied_hook_checked\":true,\"owners\":" + owners
+ ",\"queries\":" + queries + ",\"nonzero\":" + nonzero + ",\"lookup_digest\":\"" + HexFormat.of().formatHex(digest.digest())
+ "\",\"data_fingerprint\":" + fingerprints + ",\"data_unchanged\":true,\"measured_checks\":6000000,\"checksum\":" + checksum
+ ",\"allocated_bytes\":" + allocated + ",\"cpu_ns\":" + cpuTime + ",\"wall_ns\":" + wall + ",\"observations\":" + observations + "}");
}
private static void loop(FullDataSourceV2 owner, int[] xs, int[] zs, int count) {
long sum = 0;
for (int index = 0; index < count; index++) sum += owner.getDataPointAtBlockPos(xs[index & 255], -48, zs[index & 255], -64);
checksum = sum;
}
}
@@ -1,99 +0,0 @@
package dev.horizonlab.dispatch;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodQuadBuilder;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.BufferQuad;
import com.seibel.distanthorizons.core.enums.EDhDirection;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Random;
import java.security.MessageDigest;
import java.util.HexFormat;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
public final class QuadReuseIntegration implements ClientModInitializer {
static volatile long sink;
static Field opaque,transparent,pool;
public void onInitializeClient(){ClientLifecycleEvents.CLIENT_STARTED.register(client->{try{exercise();}catch(Exception e){throw new AssertionError("Applied quad reuse fixture failed",e);}System.exit(0);});}
static Field field(String name)throws Exception{var f=LodQuadBuilder.class.getDeclaredField(name);f.setAccessible(true);return f;}
static void add(LodQuadBuilder b,EDhDirection face,int x,int y,int z,int width,int color,int tile,int material,int sky,int block){
if(face==EDhDirection.UP)b.addQuadUp((short)x,(short)y,(short)z,(short)width,color,(short)tile,(byte)material,(byte)sky,(byte)block);
else if(face==EDhDirection.DOWN)b.addQuadDown((short)x,(short)y,(short)z,(short)width,color,(short)tile,(byte)material,(byte)sky,(byte)block);
else b.addQuadAdj(face,(short)x,(short)y,(short)z,(short)width,(short)1,color,(short)tile,(byte)material,(byte)sky,(byte)block);
}
static long fingerprint(LodQuadBuilder b,MessageDigest digest)throws Exception{
long count=0;var identities=new java.util.IdentityHashMap<BufferQuad,Boolean>();
for(Field field:new Field[]{opaque,transparent}){
digest.update(field.getName().getBytes(java.nio.charset.StandardCharsets.UTF_8));
Object[] lists=(Object[])field.get(b);
for(int face=0;face<lists.length;face++){
var list=(ArrayList<?>)lists[face];
digest.update((byte)face);
for(int shift=24;shift>=0;shift-=8)digest.update((byte)(list.size()>>>shift));
for(Object item:list){
BufferQuad quad=(BufferQuad)item;
if(identities.put(quad,true)!=null)throw new AssertionError("Aliased live quad");
long[] values={quad.x,quad.y,quad.z,quad.widthEastWest,quad.widthNorthSouthOrHeight,quad.color,quad.textureTileId,quad.irisBlockMaterialId,quad.skyLight,quad.blockLight,quad.direction.ordinal(),quad.hasError?1:0,quad.sortKeyEastWest,quad.sortKeyNorthSouth};
for(long value:values)for(int shift=56;shift>=0;shift-=8)digest.update((byte)(value>>>shift));
count++;
}
}
}
return count;
}
static void batch(LodQuadBuilder b,int rows){
for(int y=0;y<rows;y++)for(int x=0;x<64;x++)add(b,EDhDirection.NORTH,x,y,0,1,0xff113355,3,2,15,4);
}
static String threadOwnership() throws Exception {
var executor=java.util.concurrent.Executors.newFixedThreadPool(4);
var owners=java.util.Collections.synchronizedMap(new java.util.IdentityHashMap<BufferQuad,Integer>());
var barrier=new java.util.concurrent.CyclicBarrier(4);
var futures=new ArrayList<java.util.concurrent.Future<String>>();
try {
for(int worker=0;worker<4;worker++){
final int ownerId=worker;
futures.add(executor.submit(()->{
var digest=MessageDigest.getInstance("SHA-256");
var builder=LodQuadBuilder.getBuilder(false,null);
for(int repeat=0;repeat<16;repeat++){
barrier.await(20,java.util.concurrent.TimeUnit.SECONDS);
batch(builder,8);
for(Field field:new Field[]{opaque,transparent})for(Object list:(Object[])field.get(builder))for(Object item:(ArrayList<?>)list){
var quad=(BufferQuad)item;
Integer previous=owners.putIfAbsent(quad,ownerId);
if(previous!=null && previous!=ownerId)throw new AssertionError("Quad shared across builder threads");
}
fingerprint(builder,digest);builder.mergeQuads();fingerprint(builder,digest);builder.close();
}
return HexFormat.of().formatHex(digest.digest());
}));
}
String expected=null;
for(var future:futures){String actual=future.get(30,java.util.concurrent.TimeUnit.SECONDS);if(expected!=null&&!expected.equals(actual))throw new AssertionError("Concurrent builder geometry differed");expected=actual;}
return expected;
}finally{executor.shutdownNow();if(!executor.awaitTermination(10,java.util.concurrent.TimeUnit.SECONDS))throw new AssertionError("Fixture builder workers did not stop");}
}
static void exercise()throws Exception{
boolean enabled=dev.horizonlab.mixin.HorizonMixinPlugin.quadReuseEnabled();boolean applied=java.util.Arrays.stream(LodQuadBuilder.class.getDeclaredMethods()).anyMatch(m->m.getName().contains("horizon$freshQuad"));if(applied!=enabled)throw new AssertionError("Applied quad reuse state mismatch");
opaque=field("opaqueQuads");transparent=field("transparentQuads");pool=field("bufferQuadCacheList");var digest=MessageDigest.getInstance("SHA-256");var rng=new Random(77181);long quads=0;int faces=0;
for(int round=0;round<36;round++){
var b=LodQuadBuilder.getBuilder((round&1)!=0,null);
if(round==20){var poisoned=new BufferQuad();poisoned.hasError=true;((ArrayList<BufferQuad>)pool.get(b)).add(poisoned);}
for(EDhDirection face:EDhDirection.values())for(int y=0;y<8;y++)for(int x=0;x<32;x++){
int color=(round%3==0?0x88000000:0xff000000)|0x112233;
int tile=round%3==1?rng.nextInt(256):3,sky=round%3==2?rng.nextInt(16):15;
add(b,face,x,y,round%3==0?rng.nextInt(16):0,1,color,tile,round%4,sky,round%16);faces++;
}
quads+=fingerprint(b,digest);b.mergeQuads();quads+=fingerprint(b,digest);b.close();
}
String threadDigest=threadOwnership();
var b=LodQuadBuilder.getBuilder(false,null);for(int r=0;r<2000;r++){batch(b,8);b.close();}Thread.sleep(1000);
var cpu=ManagementFactory.getThreadMXBean();var allocation=(com.sun.management.ThreadMXBean)cpu;allocation.setThreadAllocatedMemoryEnabled(true);long id=Thread.currentThread().threadId();long ns=0,bytes=0;StringBuilder observations=new StringBuilder("[");
for(int r=0;r<3;r++){
for(int i=0;i<500;i++){batch(b,8);b.close();}Thread.sleep(200);
long start=cpu.getCurrentThreadCpuTime(),before=allocation.getThreadAllocatedBytes(id);
for(int i=0;i<2000;i++){batch(b,8);sink+=b.getCurrentOpaqueQuadsCount();b.close();}
long time=cpu.getCurrentThreadCpuTime()-start,allocated=allocation.getThreadAllocatedBytes(id)-before;ns+=time;bytes+=allocated;if(r>0)observations.append(',');observations.append("{\"cpu_ns\":").append(time).append(",\"allocated_bytes\":").append(allocated).append('}');
}
observations.append(']');System.out.println("HORIZON_QUAD_RESULT {\"enabled\":"+enabled+",\"applied_hook_checked\":true,\"geometry_digest\":\""+HexFormat.of().formatHex(digest.digest())+"\",\"thread_geometry_digest\":\""+threadDigest+"\",\"thread_workers\":4,\"geometry_quads\":"+quads+",\"input_faces\":"+faces+",\"measured_faces\":3072000,\"checksum\":"+sink+",\"cpu_ns\":"+ns+",\"allocated_bytes\":"+bytes+",\"observations\":"+observations+"}");
}
}
@@ -1,31 +0,0 @@
package dev.horizonlab.dispatch;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodQuadBuilder;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.BufferQuad;
import com.seibel.distanthorizons.core.enums.EDhDirection;
import java.lang.management.ManagementFactory;
import com.sun.management.HotSpotDiagnosticMXBean;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
public final class QuadSizeIntegration implements ClientModInitializer {
static long size(Object value)throws Exception { return ((Long)ClassLoader.getSystemClassLoader().loadClass("dev.horizonlab.metrics.ObjectSizeAgent").getMethod("size",Object.class).invoke(null,value)).longValue(); }
public void onInitializeClient() { ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
try {
var b=LodQuadBuilder.getBuilder(false,null);
for(int x=0;x<4;x++) b.addQuadAdj(EDhDirection.NORTH,(short)x,(short)0,(short)0,(short)1,(short)1,0xff113355,(short)3,(byte)2,(byte)15,(byte)4);
long scratch=0;boolean hook=false;
for(var f:LodQuadBuilder.class.getDeclaredFields())if(f.getName().contains("horizon$discardedQuad")) { hook=true;f.setAccessible(true);Object v=f.get(b);if(v==null)throw new AssertionError("Missing merged scratch");scratch=size(v); }
boolean headers=Boolean.parseBoolean(ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class).getVMOption("UseCompactObjectHeaders").getValue());
var vm=ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
var result=new java.util.LinkedHashMap<String,Object>();
result.put("enabled",hook);result.put("compact_headers",headers);
result.put("builder_shallow_bytes",size(b));result.put("quad_shallow_bytes",size(new BufferQuad()));
result.put("pending_scratch_shallow_bytes",scratch);result.put("geometry_quads",b.getCurrentOpaqueQuadsCount());
result.put("java_vendor",System.getProperty("java.vendor"));result.put("java_runtime_version",System.getProperty("java.runtime.version"));
result.put("object_alignment_bytes",Integer.parseInt(vm.getVMOption("ObjectAlignmentInBytes").getValue()));
result.put("compressed_oops",Boolean.parseBoolean(vm.getVMOption("UseCompressedOops").getValue()));
result.put("compressed_class_pointers",Boolean.parseBoolean(vm.getVMOption("UseCompressedClassPointers").getValue()));
System.out.println("HORIZON_SIZE_RESULT "+new com.google.gson.Gson().toJson(result));
b.close();System.exit(0);
} catch(Exception e) { throw new AssertionError("Private size probe failed",e); }
}); }
}
@@ -1,155 +0,0 @@
package dev.horizonlab.dispatch;
import com.seibel.distanthorizons.core.util.ExceptionUtil;
import dev.horizonlab.mixin.HorizonMixinPlugin;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.BitSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.storage.IOWorker;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
/** Applies the redirect to a real game IOWorker, using controlled cached read futures. */
public final class TerrainReadIntegration implements ClientModInitializer {
private static final class Worker extends IOWorker {
Worker(Path path) { super(new RegionStorageInfo("fixture", Level.OVERWORLD, "horizon-read-fixture"), path, false); }
}
private record Outcome(Boolean result, Throwable error, boolean interrupted) {}
private record Call(Thread thread, CountDownLatch finished, AtomicReference<Outcome> outcome) {}
@Override public void onInitializeClient() {
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
try { exercise(); }
catch (Exception failure) { throw new AssertionError("Terrain read integration failed", failure); }
if (Boolean.getBoolean("horizon.fixture.exit")) System.exit(0);
});
}
@SuppressWarnings("unchecked")
private static void cache(Worker worker, CompletableFuture<BitSet> future) throws Exception {
var field = IOWorker.class.getDeclaredField("regionCacheForBlender");
field.setAccessible(true);
var map = (Long2ObjectLinkedOpenHashMap<CompletableFuture<BitSet>>) field.get(worker);
synchronized (map) { map.clear(); map.put(ChunkPos.pack(0, 0), future); }
}
private static Call start(Worker worker, String name, boolean initiallyInterrupted) {
var result = new AtomicReference<Outcome>();
var finished = new CountDownLatch(1);
Thread thread = new Thread(() -> {
if (initiallyInterrupted) Thread.currentThread().interrupt();
Boolean value = null; Throwable error = null;
try { value = worker.isOldChunkAround(new ChunkPos(0, 0), 0); }
catch (Throwable failure) { error = failure; }
result.set(new Outcome(value, error, Thread.currentThread().isInterrupted()));
finished.countDown();
}, name);
thread.setDaemon(true); // Fixture failures cannot strand the test JVM.
thread.start();
return new Call(thread, finished, result);
}
private static Outcome finish(Call call) throws Exception {
if (!call.finished.await(4, TimeUnit.SECONDS)) throw new AssertionError("Worker did not finish");
call.thread.join(1000);
return call.outcome.get();
}
private static void awaitPending(Call call) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(4);
while (call.thread.getState() != Thread.State.WAITING) {
if (!call.thread.isAlive() || System.nanoTime() > deadline) throw new AssertionError("Read never waited");
Thread.sleep(1);
}
}
private static void pending(Worker worker, String name, boolean cancellable) throws Exception {
var future = new CompletableFuture<BitSet>();
cache(worker, future);
var call = start(worker, name, false);
try {
awaitPending(call);
call.thread.interrupt();
if (cancellable) {
var outcome = finish(call);
if (!(outcome.error instanceof CompletionException) || !ExceptionUtil.isShutdownException(outcome.error)
|| !outcome.interrupted || future.isDone()) throw new AssertionError("Shutdown changed shared future or interrupt");
} else {
if (call.finished.await(100, TimeUnit.MILLISECONDS)) throw new AssertionError("Ordinary join became interruptible");
}
future.complete(new BitSet());
var outcome = finish(call);
if (!cancellable && (!Boolean.FALSE.equals(outcome.result) || outcome.error != null || !outcome.interrupted))
throw new AssertionError("Original join result or interrupt changed");
// A later caller can still consume the shared read after cancellation of only one waiter.
if (worker.isOldChunkAround(new ChunkPos(0, 0), 0)) throw new AssertionError("Shared read was damaged");
} finally { future.complete(new BitSet()); call.thread.join(4000); }
}
private static void exercise() throws Exception {
boolean enabled = HorizonMixinPlugin.terrainReadWaitEnabled();
Path path = Files.createTempDirectory("horizon-read-fixture-");
try (var worker = new Worker(path)) {
pending(worker, "DH-World Gen Thread[fixture]", enabled);
pending(worker, "Server thread fixture", false);
pending(worker, "DH-LOD Builder Thread[fixture]", false);
var waiting = new CompletableFuture<BitSet>();
cache(worker, waiting);
var initialCall = start(worker, "DH-World Gen Thread[fixture]", true);
try {
if (enabled) {
var preInterrupted = finish(initialCall);
if (!(preInterrupted.error instanceof CompletionException) || !preInterrupted.interrupted
|| waiting.isDone() || !ExceptionUtil.isShutdownException(preInterrupted.error))
throw new AssertionError("Pending read ignored an existing shutdown interrupt");
} else {
awaitPending(initialCall);
if (initialCall.finished.await(100, TimeUnit.MILLISECONDS))
throw new AssertionError("Original pending join honored an existing interrupt");
waiting.complete(new BitSet());
var preInterrupted = finish(initialCall);
if (!Boolean.FALSE.equals(preInterrupted.result) || preInterrupted.error != null || !preInterrupted.interrupted)
throw new AssertionError("Original pre-interrupted join changed");
}
} finally { waiting.complete(new BitSet()); initialCall.thread.join(4000); }
BitSet bits = new BitSet(); bits.set(0);
cache(worker, CompletableFuture.completedFuture(bits));
var complete = finish(start(worker, "DH-World Gen Thread[fixture]", true));
if (!Boolean.TRUE.equals(complete.result) || complete.error != null || !complete.interrupted)
throw new AssertionError("Completed read changed result or interrupt");
var failed = new CompletableFuture<BitSet>();
var original = new CompletionException(new IllegalStateException("fixture failure"));
cache(worker, failed);
var call = start(worker, "DH-World Gen Thread[fixture]", false);
awaitPending(call); failed.completeExceptionally(original);
if (finish(call).error != original) throw new AssertionError("Exceptional completion identity changed");
var pendingCancelled = new CompletableFuture<BitSet>();
cache(worker, pendingCancelled);
var cancelCall = start(worker, "DH-World Gen Thread[fixture]", false);
awaitPending(cancelCall); pendingCancelled.cancel(false);
Throwable expectedCancellation = null;
try { pendingCancelled.join(); } catch (java.util.concurrent.CancellationException expected) { expectedCancellation = expected; }
var actualCancellation = finish(cancelCall).error;
if (!(actualCancellation instanceof java.util.concurrent.CancellationException)
|| !java.util.Objects.equals(actualCancellation.getMessage(), expectedCancellation.getMessage())
|| actualCancellation.getCause() != expectedCancellation.getCause())
throw new AssertionError("Pending cancellation changed join's message or cause");
var cancelled = new CompletableFuture<BitSet>(); cancelled.cancel(false);
cache(worker, cancelled);
if (!(finish(start(worker, "DH-World Gen Thread[fixture]", false)).error instanceof java.util.concurrent.CancellationException))
throw new AssertionError("Cancelled completion changed");
System.out.println("HORIZON_TERRAIN_READ_RESULT {\"enabled\":" + enabled
+ ",\"applied_io_worker_checked\":true,\"ordinary_join_checked\":true,\"shared_future_preserved\":true"
+ ",\"exception_identity_checked\":true,\"completed_interrupt_checked\":true,\"cancelled_completion_checked\":true,\"preinterrupted_pending_checked\":true}");
} finally { Files.deleteIfExists(path); }
}
}
@@ -1,87 +0,0 @@
package dev.horizonlab.dispatch;
import com.seibel.distanthorizons.core.render.QuadTree.LodRenderSection;
import com.seibel.distanthorizons.core.render.QuadTree.QuadTreeTickNodeHolder;
import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode;
import dev.horizonlab.mixin.HorizonMixinPlugin;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
/** Private Fabric fixture: semantic transitions and warmed allocation of the actual upstream holder. */
public final class TickDedupIntegration implements ClientModInitializer {
private static volatile long checksum;
@Override public void onInitializeClient() {
ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
exercise();
if (Boolean.getBoolean("horizon.fixture.exit")) System.exit(0);
});
}
private static void exercise() {
ArrayList<QuadNode<LodRenderSection>> nodes = new ArrayList<>();
for (int index = 0; index < 512; index++) nodes.add(new QuadNode<>(index, (byte) 6));
var holder = new QuadTreeTickNodeHolder();
var present = new HashSet<QuadNode<LodRenderSection>>();
var enabled = new HashSet<QuadNode<LodRenderSection>>();
var disabled = new HashSet<QuadNode<LodRenderSection>>();
var deleteChildren = new ArrayList<QuadNode<LodRenderSection>>();
var random = new Random(0x4835);
for (int step = 0; step < 6000; step++) {
var node = step % 13 == 0 ? null : nodes.get(random.nextInt(nodes.size()));
switch (random.nextInt(5)) {
case 0 -> { holder.addEnableNode(node); if (present.add(node)) enabled.add(node); }
case 1 -> { holder.addDisableNode(node); if (present.add(node)) disabled.add(node); }
case 2 -> { holder.addEnableDeleteChildrenNode(node); if (present.add(node)) deleteChildren.add(node); }
case 3 -> {
holder.removeEnableAndDisableNode(node);
enabled.remove(node); deleteChildren.remove(node); present.add(node); disabled.add(node);
}
case 4 -> { holder.clear(); present.clear(); enabled.clear(); disabled.clear(); deleteChildren.clear(); }
}
// Comparing iteration order as well as membership verifies that published collections stay unchanged.
if (!new ArrayList<>(holder.getEnabledNodes()).equals(new ArrayList<>(enabled))
|| !new ArrayList<>(holder.getDisableNodes()).equals(new ArrayList<>(disabled))
|| !holder.getEnableDeleteChildrenNodes().equals(deleteChildren))
throw new AssertionError("Action precedence or iteration order changed at step " + step);
}
var bean = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
bean.setThreadAllocatedMemoryEnabled(true);
bean.setThreadCpuTimeEnabled(true);
measureLoop(holder, nodes, 5000);
long id = Thread.currentThread().threadId(), bytes = bean.getThreadAllocatedBytes(id);
long cpu = bean.getCurrentThreadCpuTime(), wall = System.nanoTime();
measureLoop(holder, nodes, 20000);
long wallDelta = System.nanoTime() - wall, cpuDelta = bean.getCurrentThreadCpuTime() - cpu;
long byteDelta = bean.getThreadAllocatedBytes(id) - bytes;
// clear must release references in the replacement too, not only empty the published collections.
holder.clear();
if (HorizonMixinPlugin.tickDedupEnabled()) {
try {
var field = holder.getClass().getDeclaredField("horizon$presentNodes"); field.setAccessible(true);
if (!((java.util.Set<?>) field.get(holder)).isEmpty()) throw new AssertionError("Retained tick members");
} catch (ReflectiveOperationException failure) { throw new AssertionError("Missing applied membership hook", failure); }
}
System.out.println("HORIZON_TICK_DEDUP_RESULT {\"enabled\":" + HorizonMixinPlugin.tickDedupEnabled()
+ ",\"allocated_bytes\":" + byteDelta + ",\"cpu_ns\":" + cpuDelta + ",\"wall_ns\":" + wallDelta
+ ",\"nodes_per_tick\":512,\"measured_ticks\":20000,\"verified_transitions\":6000,\"checksum\":" + checksum + "}");
}
private static void measureLoop(QuadTreeTickNodeHolder holder, ArrayList<QuadNode<LodRenderSection>> nodes, int repeats) {
long sum = 0;
for (int pass = 0; pass < repeats; pass++) {
holder.clear();
for (int index = 0; index < nodes.size(); index++) {
var node = nodes.get(index);
holder.addEnableDeleteChildrenNode(node);
holder.addEnableDeleteChildrenNode(node);
}
if (holder.getEnableDeleteChildrenNodes().size() != nodes.size()) throw new AssertionError("Duplicate or lost node");
sum += holder.getEnableDeleteChildrenNodes().size();
}
checksum = sum;
}
}
@@ -1,16 +0,0 @@
package dev.horizonlab.dispatch.fixture;
import com.logisticscraft.occlusionculling.OcclusionCullingInstance;
import com.logisticscraft.occlusionculling.util.Vec3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(value = OcclusionCullingInstance.class, remap = false)
public interface CullingRayAccess {
@Invoker("rayIntersection") boolean horizon$intersects(int[] block, Vec3d camera, Vec3d direction);
@Invoker("isVisible") boolean horizon$visible(Vec3d camera, Vec3d[] points, int count);
@Accessor("allowRayChecks") void horizon$checks(boolean value);
@Accessor("lastHitBlock") int[] horizon$lastBlock();
@Accessor("cameraPos") int[] horizon$cameraBlock();
}
@@ -1,58 +0,0 @@
package dev.horizonlab.dispatch.fixture;
import com.google.gson.JsonObject;
import com.mojang.renderpearl.api.device.BackendCreationException;
import com.mojang.renderpearl.api.device.GpuBackend;
import com.mojang.renderpearl.api.device.GpuDebugOptions;
import com.mojang.renderpearl.api.device.GpuDevice;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/** Private diagnostic wrapper. Calls the original operations and rethrows their errors. */
@Mixin(Minecraft.class)
public abstract class GraphicsBackendTraceMixin {
private static void trace(GpuBackend backend, String operation, Throwable failure) {
var result = new JsonObject();
String name = backend.getName();
result.addProperty("backend", name.equals("OpenGL") || name.equals("Vulkan") ? name : "other");
result.addProperty("operation", operation);
result.addProperty("result", failure == null ? "success" : "failure");
if (failure != null) result.addProperty("exception_type", failure.getClass().getName());
if (failure instanceof BackendCreationException backendFailure)
result.addProperty("reason", backendFailure.getReason().name());
System.out.println("HORIZON_GRAPHICS_BACKEND_ATTEMPT " + result);
}
@Redirect(method = "<init>", at = @At(value = "INVOKE",
target = "Lcom/mojang/renderpearl/api/device/GpuBackend;loadLibrary()V"), require = 1, allow = 1)
private void horizon$traceLibrary(GpuBackend backend) throws BackendCreationException {
try {
backend.loadLibrary();
trace(backend, "load_library", null);
} catch (BackendCreationException | RuntimeException | LinkageError | AssertionError failure) {
trace(backend, "load_library", failure);
throw failure;
}
}
@Redirect(method = "<init>", at = @At(value = "INVOKE",
target = "Lcom/mojang/renderpearl/api/device/GpuBackend;createDevice(Lcom/mojang/renderpearl/api/device/GpuDebugOptions;)Lcom/mojang/renderpearl/api/device/GpuDevice;"), require = 1, allow = 1)
private GpuDevice horizon$traceDevice(GpuBackend backend, GpuDebugOptions options) throws BackendCreationException {
try {
GpuDevice device = backend.createDevice(options);
trace(backend, "create_device", null);
var info = device.getDeviceInfo();
var metadata = new JsonObject();
metadata.addProperty("gpu", info.name());
metadata.addProperty("graphics_backend", info.backendName());
metadata.addProperty("graphics_driver", info.driverInfo());
System.out.println("HORIZON_GRAPHICS_DEVICE_CREATED " + metadata);
return device;
} catch (BackendCreationException | RuntimeException | LinkageError | AssertionError failure) {
trace(backend, "create_device", failure);
throw failure;
}
}
}
@@ -1,52 +0,0 @@
package dev.horizonlab.geometry;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import java.util.ArrayList;
import java.util.Random;
/** Differential checks against the actual pinned publisher method, including its unusual rounding. */
public final class RelativeTerrainPositionTest {
private static int cases, failures;
private static void check(long position, byte level) {
cases++;
long expected;
try { expected = DhSectionPos.getDhSectionRelativePositionForDetailLevel(position, level); }
catch (IllegalArgumentException upstream) {
try { RelativeTerrainPosition.resolve(position, level); }
catch (IllegalArgumentException patched) {
if (!upstream.getMessage().equals(patched.getMessage()) || upstream.getClass() != patched.getClass())
throw new AssertionError("Publisher exception changed");
failures++;
return;
}
throw new AssertionError("Invalid level stopped throwing");
}
long actual = RelativeTerrainPosition.resolve(position, level);
if (expected != actual) throw new AssertionError("Relative position changed: " + position + "/" + level);
}
public static void main(String[] args) {
var values = new ArrayList<Integer>();
values.add(Integer.MIN_VALUE); values.add(Integer.MAX_VALUE); values.add(0);
for (int bit = 0; bit <= 27; bit++) {
int boundary = 1 << bit;
for (int offset = -1; offset <= 1; offset++) {
values.add(boundary + offset); values.add(-boundary + offset);
}
}
for (int input : new int[] {0, 1, 6, 30, 31, 32, 63, 64, 95, 96, 126, 127}) {
for (int output = -128; output <= 127; output++) {
for (int index = 0; index < values.size(); index++) {
int x = values.get(index), z = values.get((index * 37 + 13) % values.size());
check(DhSectionPos.encode((byte) input, x, z), (byte) output);
}
}
}
// A direct shift changes this original negative-coordinate result; preserve it exactly.
check(-7336704456267971543L, (byte) 55);
Random random = new Random(0x6f9d1413);
for (int index = 0; index < 1000000; index++) check(random.nextLong(), (byte) random.nextInt(256));
System.out.println("Actual publisher position differential checks passed: cases=" + cases + " exceptions=" + failures);
}
}
@@ -1,155 +0,0 @@
package dev.horizonlab.metrics;
import com.google.gson.JsonObject;
import com.mojang.blaze3d.systems.RenderSystem;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.Map;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Screenshot;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.levelgen.WorldOptions;
import net.minecraft.world.level.levelgen.presets.WorldPresets;
/** Private startup/readback probe. Never packaged in a playable release. */
public final class ClientGraphicsIntegration implements ClientModInitializer {
private long titleStarted;
private int titleTicks;
private boolean requested;
private boolean opening;
private int overlayWaitTicks;
private long lastWaitingState;
private final boolean worldMode = Files.exists(FabricLoader.getInstance().getConfigDir().resolve("client-graphics-world.flag"));
@SuppressWarnings("unchecked")
private static void captureShaders(JsonObject result) throws ReflectiveOperationException, java.io.IOException {
// Reuse the production recorder's read-only Iris API inspection. The
// bridge and enabling flag exist solely in this private test fixture.
var method = Class.forName("dev.horizonlab.client.ShaderSettings").getDeclaredMethod("read", boolean.class);
method.setAccessible(true);
var settings = (Map<String, Object>) method.invoke(null, FabricLoader.getInstance().isModLoaded("iris"));
String status = String.valueOf(settings.get("iris_capture_status"));
boolean enabled = Boolean.TRUE.equals(settings.get("iris_shaders_enabled"));
result.addProperty("iris_capture_status", status);
result.addProperty("iris_shaders_enabled", enabled);
var flag = FabricLoader.getInstance().getConfigDir().resolve("client-graphics-shaders.flag");
boolean required = Files.isRegularFile(flag);
result.addProperty("shader_required", required);
if (required) {
String expected = Files.readString(flag).strip();
boolean match = expected.equals(settings.get("iris_shader_pack"));
result.addProperty("iris_expected_pack_matches", match);
// Do not export arbitrary pack names or paths from runtime APIs.
if (!status.equals("available") || !enabled) throw new AssertionError("Required shader pack is inactive");
if (!match) throw new AssertionError("Unexpected active shader pack");
}
}
@Override public void onInitializeClient() {
System.out.println("HORIZON_CLIENT_GRAPHICS_INITIALIZER_READY");
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (requested) return;
long now = System.nanoTime();
if (now - lastWaitingState >= 5_000_000_000L) {
lastWaitingState = now;
var state = new JsonObject();
state.addProperty("world_loaded", client.level != null);
state.addProperty("player_present", client.player != null);
state.addProperty("overlay_present", client.gui.overlay() != null);
state.addProperty("screen_present", client.gui.screen() != null);
state.addProperty("window_active", client.isWindowActive());
state.addProperty("paused", client.isPaused());
state.addProperty("ready_client_ticks", titleTicks);
System.out.println("HORIZON_CLIENT_GRAPHICS_WAIT_STATE " + state);
}
// TitleScreen can exist beneath the asynchronous loading overlay.
// Count readiness only after it clears, for both title and world mode.
if (client.gui.overlay() != null) {
overlayWaitTicks++;
titleStarted = 0;
titleTicks = 0;
return;
}
if (worldMode) {
if (!opening && client.gui.screen() instanceof TitleScreen screen) {
opening = true;
if (Files.isRegularFile(client.gameDirectory.toPath().resolve("saves/Demo_World/level.dat"))) {
client.createWorldOpenFlows().openWorld("Demo_World", () -> {
throw new AssertionError("Disposable graphics world did not open");
});
} else {
client.createWorldOpenFlows().createFreshLevel("Demo_World", MinecraftServer.DEMO_SETTINGS,
WorldOptions.DEMO_OPTIONS, WorldPresets::createNormalWorldDimensions, screen);
}
return;
}
if (client.gui.screen() != null && client.gui.screen().getTitle().getContents() instanceof TranslatableContents title
&& title.getKey().equals("demo.help.title")) {
client.gui.setScreen(null);
return;
}
if (client.level == null || client.player == null || client.gui.screen() != null
|| client.isPaused() || !client.isWindowActive()) return;
} else if (!(client.gui.screen() instanceof TitleScreen)) return;
if (titleStarted == 0) titleStarted = System.nanoTime();
titleTicks++;
if (titleTicks < 40 || System.nanoTime() - titleStarted < 2_000_000_000L) return;
requested = true;
System.out.println("HORIZON_CLIENT_GRAPHICS_READBACK_REQUESTED");
Screenshot.takeScreenshot(client.gameRenderer.mainRenderTarget(), image -> {
System.out.println("HORIZON_CLIENT_GRAPHICS_READBACK_RECEIVED");
try (image) {
int width = image.getWidth(), height = image.getHeight();
if (width < 64 || height < 64) throw new AssertionError("Incomplete title framebuffer");
var colors = new HashSet<Integer>();
int samples = 0;
for (int y = 0; y < height; y += Math.max(1, height / 64)) {
for (int x = 0; x < width; x += Math.max(1, width / 64)) {
colors.add(image.getPixel(x, y) & 0xFFFFFF);
samples++;
}
}
var inspection = new JsonObject();
inspection.addProperty("framebuffer_width", width);
inspection.addProperty("framebuffer_height", height);
inspection.addProperty("sampled_pixels", samples);
inspection.addProperty("distinct_sampled_rgb", colors.size());
System.out.println("HORIZON_CLIENT_GRAPHICS_READBACK_INSPECTED " + inspection);
if (samples < 256 || colors.size() < 8) throw new AssertionError("Uniform title readback");
var screenshot = client.gameDirectory.toPath().resolve("client-graphics-private.png");
image.writeToFile(screenshot);
var device = RenderSystem.getDevice().getDeviceInfo();
var result = new JsonObject();
result.addProperty("observed_mode", worldMode ? "world" : "title");
result.addProperty("title_screen_observed", !worldMode);
result.addProperty("world_loaded", worldMode && client.level != null);
result.addProperty("ready_client_ticks", titleTicks);
result.addProperty("window_active", client.isWindowActive());
result.addProperty("overlay_present", client.gui.overlay() != null);
result.addProperty("overlay_wait_ticks", overlayWaitTicks);
result.addProperty("heap_max_bytes", Runtime.getRuntime().maxMemory());
captureShaders(result);
result.addProperty("framebuffer_width", width);
result.addProperty("framebuffer_height", height);
result.addProperty("sampled_pixels", samples);
result.addProperty("distinct_sampled_rgb", colors.size());
result.addProperty("gpu", device.name());
result.addProperty("graphics_backend", device.backendName());
result.addProperty("graphics_driver", device.driverInfo());
result.addProperty("private_png_sha256", HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(screenshot))));
System.out.println("HORIZON_CLIENT_GRAPHICS_RESULT " + result);
client.execute(client::stop);
} catch (Exception error) {
throw new AssertionError("Private client graphics readback failed", error);
}
});
});
}
}
@@ -1,102 +0,0 @@
package dev.horizonlab.metrics;
import java.nio.file.Path;
import java.util.Map;
public final class MetricsTest {
public static void main(String[] args) {
var sourceContext = new java.util.LinkedHashMap<String, Object>();
sourceContext.putAll(Map.of("gpu", "GPU", "graphics_backend", "OpenGL", "graphics_driver", "Driver", "private_path", "secret"));
var serverContext = ServerContext.capture("integrated", sourceContext);
sourceContext.put("graphics_backend", "Vulkan");
check(serverContext.get("graphics_backend").equals("OpenGL"), "graphics snapshot does not track later mutations");
check(!serverContext.containsKey("private_path"), "server context only copies graphics allowlist");
check(serverContext.get("client_graphics_capture_status").equals("captured_at_recording_start"), "integrated graphics captured");
check(!ServerContext.capture("dedicated", sourceContext).containsKey("gpu"), "dedicated server does not claim client graphics");
check(ServerContext.capture("integrated", Map.of("gpu", "GPU")).get("client_graphics_capture_status").equals("not_supplied"), "partial graphics snapshot is unavailable");
check(ServerContext.capture("unrecognized", Map.of()).get("server_runtime_kind").equals("unknown"), "legacy server kind unknown");
SampleStats empty = SampleStats.of(new long[0], 0);
check(empty.count() == 0 && empty.meanRate() == 0, "empty recording");
SampleStats fixed = SampleStats.of(new long[]{10_000_000, 10_000_000, 10_000_000}, 3);
check(fixed.meanRate() == 100 && fixed.lowOnePercentRate() == 100, "constant pacing");
long[] data = new long[100];
java.util.Arrays.fill(data, 10_000_000);
data[99] = 100_000_000;
SampleStats spike = SampleStats.of(data, 100);
check(spike.p99Ms() == 10 && spike.maxMs() == 100 && spike.lowOnePercentRate() == 10,
"nearest-rank percentiles and slowest-one-percent mean");
check(spike.over50Ms() == 1 && spike.over100Ms() == 0, "stall boundaries");
Recording recording = new Recording("../../recording label", "frames", Path.of("unused"), 120, Map.of());
for (int i = 0; i < Recording.MAX_SAMPLES + 2; i++) recording.add(10_000_000);
recording.exclude();
recording.players(50);
Recording.Snapshot snapshot = recording.finish();
check(snapshot.samples().length == Recording.MAX_SAMPLES, "bounded buffer");
check(snapshot.metadata().get("dropped_samples").equals(2), "overflow reported");
check(snapshot.metadata().get("excluded_frames").equals(1), "excluded frames reported");
check(snapshot.metadata().get("peak_players").equals(50), "workload size reported");
check((long) snapshot.metadata().get("heap_used_peak_sampled_bytes") >=
(long) snapshot.metadata().get("heap_used_end_bytes"), "heap peak includes the final sample");
check((int) snapshot.metadata().get("resource_samples") >= 2, "start/end resource snapshots");
check(snapshot.metadata().get("garbage_collectors") instanceof java.util.List, "collector configuration captured");
var diagnostic = java.lang.management.ManagementFactory.getPlatformMXBean(com.sun.management.HotSpotDiagnosticMXBean.class);
if (diagnostic != null) {
boolean actual = Boolean.parseBoolean(diagnostic.getVMOption("UseCompactObjectHeaders").getValue());
check(snapshot.metadata().get("compact_object_headers").equals(actual), "header option reflects the actual VM");
}
if (snapshot.metadata().containsKey("process_cpu_seconds")) {
check((double) snapshot.metadata().get("process_cpu_seconds") >= 0, "CPU time is nonnegative");
}
check(!snapshot.label().contains("/"), "safe report filename");
check(!snapshot.metadata().containsKey("process_rss_peak_sampled_bytes"), "unavailable RSS is absent, not zero");
Recording resident = new Recording("memory", "frames", Path.of("unused"), 10, Map.of());
resident.residentMemory(-1);
resident.residentMemory(100);
resident.residentMemory(200);
resident.residentMemory(150);
resident.add(10_000_000);
var residentSnapshot = resident.finish();
resident.residentMemory(999);
resident.add(10_000_000);
check(residentSnapshot.metadata().get("process_rss_last_sampled_bytes").equals(150L), "last RSS is not called end memory");
check(residentSnapshot.metadata().get("process_rss_peak_sampled_bytes").equals(200L), "RSS peak can exceed last sample");
check(residentSnapshot.metadata().get("process_rss_samples").equals(3), "invalid memory samples excluded");
check(resident.finish().samples().length == 1, "late samples do not change a finished recording");
check(!snapshot.metadata().containsKey("tick_state_capture_status"), "frame reports do not pretend to capture server state");
Recording ticks = new Recording("ticks", "ticks", Path.of("unused"), 10, Map.of());
ticks.addTick(10_000_000, 1, 20, false, false, false, true);
ticks.addTick(5_000_000, 1, 40, true, false, false, false);
ticks.addTick(5_000_000, 1, 20, true, true, true, true);
ticks.addTick(10_000_000, 1, 20, false, false, false, true);
var tickSnapshot = ticks.finish();
ticks.addTick(10_000_000, 99, 40, true, false, true, false);
var tickMetadata = tickSnapshot.metadata();
check(tickMetadata.get("tick_state_first").equals(tickMetadata.get("tick_state_last")), "restored boundary states match");
check(tickMetadata.get("tick_state_changed").equals(java.util.List.of("configured_tick_rate", "frozen", "stepping", "sprinting", "runs_game_elements")),
"intermediate changes are retained after restoration");
check(tickMetadata.get("tick_state_samples").equals(4L), "state is sampled with accepted tick durations");
check(tickMetadata.get("tick_state_frozen_samples").equals(2L), "frozen includes stepping ticks");
check(tickMetadata.get("tick_state_suppressed_samples").equals(1L), "frozen and simulation suppression are distinct");
check(tickMetadata.get("tick_state_nonstandard_rate_samples").equals(1L), "custom rate observed between normal boundaries");
check((double) tickMetadata.get("observed_ticks_per_second") == 4 / (double) tickMetadata.get("elapsed_seconds"),
"observed TPS uses wall time rather than inverse execution duration");
check(ticks.finish().metadata().get("tick_state_samples").equals(4L), "late tick observations are ignored");
Recording unsampled = new Recording("unsampled", "ticks", Path.of("unused"), 10, Map.of());
unsampled.add(10_000_000);
check(unsampled.finish().metadata().get("tick_state_capture_status").equals("unavailable"), "missing state is not assumed normal");
Recording invalid = new Recording("invalid", "ticks", Path.of("unused"), 10, Map.of());
invalid.addTick(1_000_000, 1, Float.NaN, false, false, false, true);
invalid.addTick(1_000_000, 1, Float.POSITIVE_INFINITY, false, false, false, true);
invalid.addTick(1_000_000, 1, 0, false, false, false, true);
invalid.addTick(1_000_000, 1, 20, false, false, false, true);
var invalidSnapshot = invalid.finish();
check(invalidSnapshot.samples().length == 4, "invalid manager observations do not abort tick duration capture");
check(invalidSnapshot.metadata().get("tick_state_capture_status").equals("invalid_samples"), "invalid state is not silently assumed normal");
check(invalidSnapshot.metadata().get("tick_state_invalid_samples").equals(3L), "invalid observations counted");
System.out.println("Metrics tests passed.");
}
private static void check(boolean condition, String name) {
if (!condition) throw new AssertionError(name);
}
}
@@ -1,59 +0,0 @@
package dev.horizonlab.metrics;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.sun.management.HotSpotDiagnosticMXBean;
import java.lang.management.ManagementFactory;
import java.util.Locale;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** Actual publisher types in an isolated JVM; allocated bytes are not retained game RAM. */
public final class ObjectHeaderBenchmark {
private static volatile Object[] retained;
private static final int COUNT = 100_000;
private static void measure(int kind, boolean report) {
Object[] values = new Object[COUNT]; // Reference storage excluded from the counter.
var threads = (com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
if (!threads.isThreadAllocatedMemorySupported())
throw new IllegalStateException("Thread allocation accounting unavailable");
threads.setThreadAllocatedMemoryEnabled(true);
long id = Thread.currentThread().threadId();
long start = threads.getThreadAllocatedBytes(id);
for (int i = 0; i < COUNT; i++) values[i] = switch (kind) {
case 0 -> new AABB(i, 0, 0, i + 1, 1, 1);
case 1 -> new Vec3(i, 2, 3);
case 2 -> new DhChunkPos(i, 0);
case 3 -> Long.valueOf(i + 1000L); // Outside the standard boxed-value cache.
default -> new Object();
};
retained = values; // Escaping objects must actually be materialized.
long bytes = threads.getThreadAllocatedBytes(id) - start;
if (bytes <= 0) throw new AssertionError("Missing allocation counter");
for (int i = 0; i < COUNT; i++) {
boolean valid = switch (kind) {
case 0 -> ((AABB) values[i]).minX == i && ((AABB) values[i]).maxX == i + 1;
case 1 -> ((Vec3) values[i]).x == i && ((Vec3) values[i]).y == 2 && ((Vec3) values[i]).z == 3;
case 2 -> values[i].equals(new DhChunkPos(i, 0));
case 3 -> ((Long) values[i]).longValue() == i + 1000L;
default -> values[i] != null && (i == 0 || values[i] != values[i - 1]);
};
if (!valid) throw new AssertionError("Object values changed");
}
if (report) System.out.printf(Locale.ROOT,
"type=%s objects=%d allocated_bytes=%d bytes_per_object=%.3f%n",
new String[]{"AABB", "Vec3", "DhChunkPos", "Long", "Object"}[kind], COUNT, bytes, (double) bytes / COUNT);
}
public static void main(String[] args) {
var hotspot = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);
boolean enabled = Boolean.parseBoolean(hotspot.getVMOption("UseCompactObjectHeaders").getValue());
if (args.length != 1 || enabled != Boolean.parseBoolean(args[0]))
throw new AssertionError("JVM compact header setting does not match the requested mode");
System.out.println("compact_object_headers=" + enabled);
for (int kind = 0; kind < 5; kind++) {
for (int warmup = 0; warmup < 3; warmup++) measure(kind, false);
for (int trial = 0; trial < 3; trial++) measure(kind, true);
}
}
}
@@ -1,7 +0,0 @@
package dev.horizonlab.metrics;
import java.lang.instrument.Instrumentation;
public final class ObjectSizeAgent {
private static Instrumentation instrumentation;
public static void premain(String args, Instrumentation value) { instrumentation=value; }
public static long size(Object value) { return instrumentation.getObjectSize(value); }
}
@@ -1,59 +0,0 @@
package dev.horizonlab.metrics;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/** Real native reads on each CI platform; also checks idle and failure behavior. */
public final class ResidentMemoryTest {
public static void main(String[] args) throws Exception {
ResidentMemory reader = new ResidentMemory();
AtomicInteger calls = new AtomicInteger();
CountDownLatch threeReads = new CountDownLatch(3);
Thread caller = Thread.currentThread();
ProcessMemoryMonitor.configure(() -> {
if (Thread.currentThread() == caller) throw new AssertionError("Memory read on recording thread");
long rss = reader.getAsLong();
if (rss <= 0) throw new AssertionError("Native resident memory unavailable on this runner");
calls.incrementAndGet();
threeReads.countDown();
return rss;
});
try {
Thread.sleep(50);
if (calls.get() != 0) throw new AssertionError("Reader ran without a recording");
Recording frames = new Recording("rss", "frames", Path.of("unused"), 10, Map.of());
Recording ticks = new Recording("rss", "ticks", Path.of("unused"), 10, Map.of());
// The third read starts after the second dispatch completed, so both recordings have data.
if (!threeReads.await(10, TimeUnit.SECONDS)) throw new AssertionError("Background reads did not complete");
var frameReport = frames.finish();
var tickReport = ticks.finish();
for (var report : new Recording.Snapshot[]{frameReport, tickReport}) {
var metadata = report.metadata();
long last = (long) metadata.get("process_rss_last_sampled_bytes");
long peak = (long) metadata.get("process_rss_peak_sampled_bytes");
if (last <= 0 || peak < last) throw new AssertionError("Invalid resident memory report");
System.out.println(report.kind() + " RSS last=" + last + " peak=" + peak + " samples=" + metadata.get("process_rss_samples"));
}
int idleCalls = calls.get();
Thread.sleep(1100);
if (calls.get() != idleCalls) throw new AssertionError("Reader kept polling after recordings finished");
CountDownLatch failedRead = new CountDownLatch(1);
ProcessMemoryMonitor.configure(() -> {
failedRead.countDown();
throw new IllegalStateException("Unsupported native reader");
});
Recording unavailable = new Recording("unsupported", "frames", Path.of("unused"), 10, Map.of());
unavailable.add(1_000_000);
if (!failedRead.await(10, TimeUnit.SECONDS)) throw new AssertionError("Unavailable reader was not attempted");
var unavailableReport = unavailable.finish();
if (unavailableReport.samples().length != 1 || unavailableReport.metadata().containsKey("process_rss_peak_sampled_bytes"))
throw new AssertionError("Unavailable memory disrupted recording or was reported as zero");
System.out.println("Background resident-memory checks passed.");
} finally {
ProcessMemoryMonitor.configure(null);
}
}
}
@@ -1,69 +0,0 @@
package dev.horizonlab.metrics;
import com.google.gson.JsonParser;
import com.mojang.blaze3d.systems.RenderSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.levelgen.WorldOptions;
import net.minecraft.world.level.levelgen.presets.WorldPresets;
/** Private metadata fixture; ordinary disposable demo, no simulation changes. */
public final class ServerGraphicsIntegration implements ClientModInitializer {
private boolean opening, finished;
private long ready, started;
@Override public void onInitializeClient() {
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (finished || client.gui.overlay() != null) return;
if (!opening && client.gui.screen() instanceof TitleScreen screen) {
opening = true;
client.createWorldOpenFlows().createFreshLevel("Demo_World", MinecraftServer.DEMO_SETTINGS,
WorldOptions.DEMO_OPTIONS, WorldPresets::createNormalWorldDimensions, screen);
return;
}
if (client.gui.screen() != null && client.gui.screen().getTitle().getContents() instanceof TranslatableContents title
&& title.getKey().equals("demo.help.title")) { client.gui.setScreen(null); return; }
if (client.level == null || client.player == null || client.gui.screen() != null || client.isPaused()
|| !client.isWindowActive()) { ready = 0; return; }
long now = System.nanoTime();
if (ready == 0) ready = now;
if (started == 0 && now - ready >= 2_000_000_000L) {
try {
var method = Class.forName("dev.horizonlab.client.HorizonClient").getDeclaredMethod("start", String.class, int.class);
method.setAccessible(true);
if (!method.invoke(null, "server-graphics-fixture", 10).equals(1)) throw new AssertionError("Recording failed to start");
started = now;
} catch (ReflectiveOperationException error) { throw new AssertionError("Private recorder start failed", error); }
}
if (started == 0 || now - started < 14_000_000_000L) return;
Path output = client.gameDirectory.toPath().resolve("horizon-benchmarks");
try (var paths = Files.list(output)) {
var reports = paths.filter(p -> p.toString().endsWith(".json") && p.getFileName().toString().startsWith("server-graphics-fixture-")).toList();
if (reports.size() != 2) throw new AssertionError("Expected completed frame and tick reports");
var device = RenderSystem.getDevice().getDeviceInfo();
var result = new com.google.gson.JsonObject();
for (Path path : reports) {
var report = JsonParser.parseString(Files.readString(path)).getAsJsonObject();
if (!report.get("gpu").getAsString().equals(device.name())
|| !report.get("graphics_backend").getAsString().equals(device.backendName())
|| !report.get("graphics_driver").getAsString().equals(device.driverInfo()))
throw new AssertionError("Report graphics differ from actual device");
if (report.get("kind").getAsString().equals("ticks")) {
if (!report.get("server_runtime_kind").getAsString().equals("integrated")
|| !report.get("client_graphics_capture_status").getAsString().equals("captured_at_recording_start"))
throw new AssertionError("Integrated graphics status missing");
for (String key : new String[]{"server_runtime_kind", "client_graphics_capture_status", "gpu", "graphics_backend", "graphics_driver"}) result.add(key, report.get(key));
}
}
finished = true;
System.out.println("HORIZON_SERVER_GRAPHICS_RESULT " + result);
client.stop();
} catch (Exception error) { throw new AssertionError("Private server graphics fixture failed", error); }
});
}
}
@@ -1,70 +0,0 @@
package dev.horizonlab.metrics;
import com.google.gson.JsonParser;
import dev.horizonlab.HorizonLab;
import java.nio.file.Files;
import java.nio.file.Path;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
/** Private fixture. Run only on a disposable demo world; never distribute this mod. */
public final class TickStateIntegration implements ClientModInitializer {
private int phase;
private long started;
@Override public void onInitializeClient() {
ClientTickEvents.END_CLIENT_TICK.register(client -> {
var server = client.getSingleplayerServer();
if (server == null || client.level == null || client.isPaused() || client.gui.screen() != null) return;
Path output = client.gameDirectory.toPath().resolve("horizon-benchmarks");
if (phase == 0) {
phase++;
started = System.nanoTime();
server.execute(() -> {
var manager = server.tickRateManager();
if (manager.tickrate() != 20 || manager.isFrozen() || manager.isSprinting())
throw new AssertionError("Fixture requires normal initial tick-manager state");
if (!HorizonLab.startTicks("tick-state-fixture", 10, output, "private-fixture"))
throw new AssertionError("Fixture recording was not started");
});
}
double elapsed = (System.nanoTime() - started) / 1e9;
if (phase == 1 && elapsed >= 2) {
phase++;
server.execute(() -> { server.tickRateManager().setTickRate(40); server.tickRateManager().setFrozen(true); });
} else if (phase == 2 && elapsed >= 4) {
phase++;
server.execute(() -> {
if (!server.tickRateManager().stepGameIfPaused(10)) throw new AssertionError("Fixture step failed");
});
} else if (phase == 3 && elapsed >= 6) {
phase++;
server.execute(() -> { server.tickRateManager().setFrozen(false); server.tickRateManager().setTickRate(20); });
} else if (phase == 4 && elapsed >= 8) {
phase++;
server.execute(() -> server.tickRateManager().requestGameToSprint(5));
} else if (phase == 5 && elapsed >= 12) {
try (var paths = Files.list(output)) {
var reports = paths.filter(p -> p.getFileName().toString().startsWith("tick-state-fixture-ticks-")
&& p.toString().endsWith(".json")).toList();
if (reports.size() != 1) throw new AssertionError("Expected one completed fixture report");
var report = JsonParser.parseString(Files.readString(reports.getFirst())).getAsJsonObject();
if (!report.get("tick_state_first").equals(report.get("tick_state_last")))
throw new AssertionError("Fixture did not restore its boundary state");
if (report.get("tick_state_samples").getAsLong() != report.getAsJsonObject("stats").get("count").getAsLong())
throw new AssertionError("Applied tick-state and duration counts differ");
for (String field : new String[]{"frozen", "stepping", "sprinting", "suppressed", "nonstandard_rate"})
if (report.get("tick_state_" + field + "_samples").getAsLong() < 1)
throw new AssertionError("Applied fixture did not observe " + field);
var result = new com.google.gson.JsonObject();
for (var entry : report.entrySet())
if (entry.getKey().startsWith("tick_state_") || entry.getKey().equals("observed_ticks_per_second"))
result.add(entry.getKey(), entry.getValue());
System.out.println("HORIZON_TICK_STATE_RESULT " + result);
phase++;
client.stop();
} catch (Exception error) { throw new AssertionError("Private applied tick-state fixture failed", error); }
}
});
}
}
-32
View File
@@ -1,32 +0,0 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"
PACK="$PWD/horizon-m2-balanced-@[email protected]"
if [[ ! -f "$PACK" ]]; then
printf 'Keep this script beside the .mrpack files from the extracted starter ZIP.\n'
exit 1
fi
APP=""
for candidate in "/Applications/Prism Launcher.app" "$HOME/Applications/Prism Launcher.app"; do
if [[ -d "$candidate" ]]; then APP="$candidate"; break; fi
done
if [[ -z "$APP" ]]; then
printf 'Installing official Prism Launcher 11.1.1 into ~/Applications...\n'
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
curl --fail --location --retry 3 --output "$TEMP_DIR/prism.zip" \
'https://github.com/PrismLauncher/PrismLauncher/releases/download/11.1.1/PrismLauncher-macOS-11.1.1.zip'
printf '%s %s\n' '15126a7df57acb4f37fe581dad68cb03c7bb60b70eb82345f8bb3246235600c6' "$TEMP_DIR/prism.zip" | shasum -a 256 -c -
ditto -x -k "$TEMP_DIR/prism.zip" "$TEMP_DIR/unpacked"
mkdir -p "$HOME/Applications"
APP="$HOME/Applications/Prism Launcher.app"
if [[ ! -d "$TEMP_DIR/unpacked/Prism Launcher.app" ]]; then
printf 'Unexpected official launcher archive layout. Install Prism from https://prismlauncher.org/download/macOS/\n'
exit 1
fi
mv "$TEMP_DIR/unpacked/Prism Launcher.app" "$APP"
fi
printf '\nIn Prism: sign into Microsoft yourself, allow automatic Java 25 download, import the pack, and launch.\n'
printf 'For this 16 GB Mac set maximum memory to 6144 MiB; start at 1280x800 or 1920x1200.\n'
printf 'Read START-HERE.txt for the baseline comparison and graphics profiles.\n\n'
open -a "$APP" --args --import "$PACK"
-195
View File
@@ -1,195 +0,0 @@
HORIZON @PACK_VERSION@ — playable optimization experiments
YOUR MAC
1. Extract this ZIP. Double-click Mac-Start.command. If macOS asks, use Open
from the file's context menu. It installs the official Prism Launcher in
~/Applications if needed and opens the M2 balanced pack import.
Alternatively install Prism yourself: https://prismlauncher.org/download/macOS/
then Add Instance > Import > horizon-m2-balanced-@[email protected].
2. Sign into your Microsoft account in Prism yourself. No credentials are
shared with this project. You need an owned Minecraft Java license.
3. Enable Prism automatic Java downloads, using Java 25 ARM64 on Apple Silicon.
Instance > Edit > Settings > Java: check the Memory override box, then set
maximum memory 6144 MiB and minimum 512 MiB.
If auto Java fails, install a native ARM64 Java 25 JDK and select it there.
4. Launch. Start with a new test world or a COPY of a world from Minecraft 26.3.
This release is 26.3; do not load an older-version world for a fair comparison.
Choose 1280x800 or 1920x1200 before testing; the Retina screen's full native
resolution is a substantially larger GPU workload.
WINDOWS / LINUX
Install Prism from https://prismlauncher.org/download/ and import the same packs.
Use Java 25 matching your CPU architecture. No Rust compilation is required.
PROFILES
- m2-balanced: 12 chunks of full-detail terrain plus 128 chunks of distant LOD.
Sodium, Lithium, FerriteCore, ImmediatelyFast, EntityCulling, Distant Horizons.
Recommended first test for your 16 GB M2 Pro. OpenGL, shaders not installed.
- core-performance: 12 full-detail chunks, no distant LOD or shaders, with Sodium,
Lithium, FerriteCore, ImmediatelyFast and EntityCulling. After a first balanced smoke test,
compare this against baseline to test the core mods at the same view and
simulation distances.
Use the same heap, resolution, world copy, camera route and Java flags in both.
All original experimental controllers are disabled. This is a full-detail
comparison profile; it does not add balanced's 128-chunk distant horizon.
- baseline: original game renderer and simulation with Fabric API and the same
Horizon timing instrument. 12 full-detail chunks, no performance mods or LOD.
Use the SAME 6144 MiB maximum heap as m2-balanced for a fair comparison.
- adaptive: same render distances, worker count and mods as m2-balanced, with
experimental foreground-first terrain work scheduling. /horizon adaptive off
disables it; /horizon adaptive on enables it. /horizon status shows its budget.
It reduces DH background duty cycle when frames fall below a 60 FPS target or
heap usage exceeds 85%, then recovers gradually. Distant terrain can take longer
to appear. It does not reduce your view distance or change normal simulation.
This is a heuristic, not proof that background work caused a slow frame.
- worker-pacing: same balanced configuration, with an experimental precise worker
timer for pinned DH 3.3.4. It accumulates tiny sleep durations instead of rounding
each one down to milliseconds, and avoids per-task timestamp boxing. This can
substantially change background CPU and distant-generation completion times.
- adaptive-pacing: combines the adaptive controller with that precise worker timer.
Compare against balanced on your Mac; no actual M2 graphics result is established.
Disable the timer in config/horizon-worker-pacing.properties (enabled=false),
then restart. /horizon adaptive off changes only the adaptive controller.
- queue-wait: same balanced settings, with a short wait instead of continuous
retries while a DH update is still inside its existing 250 ms debounce period.
Incoming work wakes the queue worker. OS timers can overshoot the requested
1 ms maximum wait; evaluate terrain update latency as well as CPU/FPS.
Version 0.9 also lets incoming work wake an empty queue's former 50 ms sleep.
Cache maintenance keeps its 50 ms cadence; the original debounce stays intact.
- adaptive-queue: combines queue-wait with adaptive terrain and precise pacing.
For an isolated comparison use queue-wait versus m2-balanced. For the combined
controls use adaptive-queue versus adaptive-pacing. Disable the queue hook in
config/horizon-queue-wait.properties (enabled=false), then restart.
- dispatch-retry: same balanced settings, with bounded retries when DH rejects
a task submission. The original task is requeued; other executors can progress.
This is a separate experiment: adaptive, worker-pacing and queue-wait are off.
Set enabled=false in config/horizon-dispatch-retry.properties and restart
to disable it. A synthetic applied-hook test checks exact task completion.
- tick-dedup: same balanced settings, with compact reusable storage for private
terrain tick membership checks. Action order and visual settings stay the same.
Set enabled=false in config/horizon-tick-dedup.properties and restart to disable.
Compare against m2-balanced for an isolated test; no Mac FPS gain is measured yet.
- culling-empty: same balanced settings, with a shared empty iterator for empty
dynamic entity/block-entity whitelists. Registered callbacks stay upstream.
Disable in config/horizon-culling-empty.properties (enabled=false), then restart.
Compare against m2-balanced; no whole-game or Mac FPS gain is measured yet.
- quad-reuse: same balanced settings, with reuse of one discarded terrain quad
when DH's existing pool would otherwise allocate a fresh object. Generated
opaque/translucent geometry and concurrent builders match upstream. Caller-loop
allocations fall in the merging-heavy fixture; CPU results vary with warm-up.
It is off in combined profiles. Disable enabled in
config/horizon-quad-reuse.properties and restart for a comparison.
- terrain-position: same balanced settings, with equivalent position arithmetic
in the full-data lookup. Original rounding and fallback errors are retained.
Disable in config/horizon-terrain-position.properties (enabled=false), then
restart. Compare against m2-balanced; no game or Mac FPS gain is established.
- culling-rays: same balanced settings, with two per-instance scratch vectors for
temporary visibility-ray calculations. Upstream arithmetic and decisions stay
in place. Disable in config/horizon-culling-rays.properties (enabled=false),
then restart. Compare against m2-balanced; game/Mac gains remain unmeasured.
- foreground-first: combines adaptive terrain, precise pacing, wakeable queue
waits, bounded rejected dispatch retries and (since 0.13) tick membership reuse.
Uses OpenGL and a 120 FPS
adaptive goal. This is not an FPS cap; terrain may take longer to appear.
- foreground-vulkan: the same combined controls with Vulkan preference and
OpenGL fallback. On Apple Silicon the game bundles a MoltenVK library.
Test the isolated vulkan pack first, then compare both combined profiles
using equal worlds, heap, resolution and camera routes. Check the actual
backend in the benchmark viewer; a fallback is not a Vulkan result.
- low-memory: 8 full-detail chunks, no LOD. Set maximum memory 3072 MiB.
For a matched comparison set both instances to the same render distance.
- far-distance: 12 full-detail plus 256 LOD chunks. Start with 8192 MiB ONLY on
machines with enough physical RAM; this is not the recommended 16 GB Mac pack.
- cinematic: M2 pack plus Iris and Complementary Reimagined. Shaders start OFF.
Video Settings > Shader Packs lets you enable them. They cost GPU time; Mac
driver limitations may cause visual issues. Try after the balanced pack works.
- vulkan: experimental Vulkan preference with the game's OpenGL fallback,
no Iris. Test independently against OpenGL; faster is hardware-dependent.
All profiles retain simulation distance 12 and normal 20 TPS. These are initial
budgets, not promises of identical speed on every CPU/GPU. More distant LOD uses
simplified geometry; blocks nearby retain full detail. Building a fresh LOD cache
costs CPU/disk/memory. Wait for it to settle before judging steady-state FPS.
Background distant generation does not replace your normal gameplay simulation.
MEASURE ON YOUR M2 PRO
1. Import baseline too. Close every other Minecraft instance. Plug in the Mac,
disable Low Power Mode, and use the same window size, FPS cap, VSync setting,
render distance, simulation distance, world COPY, and camera route in each.
2. Warm each world for 2–3 minutes; allow distant generation to settle. Test new
chunk exploration separately from already-generated terrain. Do not compare
baseline's warmed world against an optimized world's first-time generation.
3. Press F8 in gameplay to start a 120-second recording; press again to stop.
On Mac you may need Fn+F8 if media keys take priority. You can rebind it in
Controls. Alternatively run /horizon bench baseline 120, close chat and play
your route. In balanced use /horizon bench m2 120. /horizon stop ends early.
/horizon status shows the profile. F8 also works when chat is restricted.
4. Open Prism's instance Minecraft folder > horizon-benchmarks. Each run writes
frames JSON/CSV and integrated-server ticks JSON/CSV. They include average FPS,
slowest-1%-average FPS, p95/p99 frame durations, stalls, sampled peak JVM heap,
process CPU time, process resident memory (RSS) when available, and GC data.
RSS reads run once per second on a shared background thread during recordings.
They include resident native pages; this is not a complete GPU/unified-memory
measurement. Last RSS is the last sample, not necessarily the finish time.
Heap is sampled once per second during gameplay;
shorter peaks can be missed. CPU measurement spans the whole recording.
Menus, pauses and unfocused frames are excluded. Tick execution time is not
the observed TPS. JVM heap is not whole-process memory or unified GPU memory.
5. Repeat at least three times, alternating baseline/optimized. For daily play
turn VSync back on or cap FPS to your display; uncapped FPS spends more power.
COMPARE WITHOUT PYTHON
Open Compare-Benchmarks.html from this ZIP in Safari, Chrome, Firefox or Edge.
Choose one baseline JSON and one candidate JSON from horizon-benchmarks. You can
select each JSON together with its matching CSV to see duration charts. Frame and
tick reports are compared separately. Files stay on your device; the viewer needs
no server, account, installation or network access. Check its settings/route/warm-up
warnings before drawing conclusions. Save comparison exports a summary JSON.
The viewer also shows Java implementation and loaded mod versions. Expand the
mod list to inspect it. Upgrades, additions and removals are flagged as experiment
variables; older reports with missing software identity show an unknown warning.
Tick reports also show sampled configured rate, freeze, step and sprint state.
Restored changes remain flagged; old or invalid state is unknown. Observed TPS
counts measured invocations, including frozen ticks, over recording wall time.
State is sampled after server invocations; it is not a completed-work or pause
trace. Compare the accompanying tick reports when judging integrated-server load.
DH changes clouds, chunk fade and transparency to blend its distant renderer;
assess visual differences as well as speed. For collector experiments test
Java 25's -XX:+UseZGC against its default G1 in both profiles; ZGC trades some
CPU/memory for shorter pauses and is not an automatic win on a 16 GB machine.
OPTIONAL JAVA MEMORY EXPERIMENT
With Java 25 HotSpot, add -XX:+UseCompactObjectHeaders to the instance's Java JVM
arguments and restart. Retain your other arguments. Packs do not enable this flag
automatically. For a separate off/on test use duplicate balanced instances with
-XX:-UseCompactObjectHeaders and -XX:+UseCompactObjectHeaders respectively.
Use one flag at a time and keep the collector, heap and workload identical.
Reports and the offline viewer show the actual compact-header setting and GC.
Some object layouts shrink; others do not. Whole-game RAM/FPS gains need testing.
Version 0.8 explicitly selects the custom graphics preset so Minecraft's startup
preset does not override the pack's render distance. Check the recorded settings.
This build is a playable Java optimization stack with original measurement code.
It is NOT a completed Rust rewrite, a new custom renderer, or a measured M2 speedup.
It has no gameplay feature additions and is intended to preserve vanilla rules;
exhaustive vanilla equivalence has not been proven. Start on world copies.
Only original project binaries are embedded. Mods, the game and assets download
from their publishers with pinned hashes. No decompiled Minecraft is distributed.
Source, build commands and measured validation:
https://github.com/ludovic111/Horizon
TERRAIN / SHADER COMPARISONS
Version 0.12 records actual DH radius/quality, worker count/duty cycle, Iris state
and field of view at capture boundaries. If visual settings changed, the viewer
flags the run. Older reports without these fields cannot establish quality
equivalence. Snapshots cannot detect a change that was restored before the end,
and do not prove that all requested distant terrain finished rendering.
Since 0.14, profiles with Distant Horizons request an interruptible blending-read
wait for its generation workers during shutdown. Ordinary Minecraft callers and
chunk saves keep their original behavior. To disable, set enabled=false in
config/horizon-terrain-read-wait.properties and restart. This is a scoped shutdown
correctness fix, not a claim of greater FPS or faster terrain generation.
-18
View File
@@ -1,18 +0,0 @@
# Only explicit overrides; DH fills in other defaults on first launch.
[common.multiThreading]
numberOfThreads = 2
threadRunTimeRatio = "0.5"
threadPriority = 3
[common.worldGenerator]
limitIfServerUnhealthy = true
pauseIfMovingQuickly = true
[client.advanced.graphics.quality]
lodChunkRenderDistanceRadius = 128
horizontalQuality = "MEDIUM"
verticalQuality = "MEDIUM"
[client.advanced.autoUpdater]
enableAutoUpdater = false
enableSilentUpdates = false
-17
View File
@@ -1,17 +0,0 @@
[common.multiThreading]
numberOfThreads = 4
threadRunTimeRatio = "0.5"
threadPriority = 3
[common.worldGenerator]
limitIfServerUnhealthy = true
pauseIfMovingQuickly = true
[client.advanced.graphics.quality]
lodChunkRenderDistanceRadius = 256
horizontalQuality = "MEDIUM"
verticalQuality = "MEDIUM"
[client.advanced.autoUpdater]
enableAutoUpdater = false
enableSilentUpdates = false
-18
View File
@@ -1,18 +0,0 @@
# Only explicit overrides; DH fills in other defaults on first launch.
[common.multiThreading]
numberOfThreads = 2
threadRunTimeRatio = "0.5"
threadPriority = 3
[common.worldGenerator]
limitIfServerUnhealthy = true
pauseIfMovingQuickly = true
[client.advanced.graphics.quality]
lodChunkRenderDistanceRadius = 128
horizontalQuality = "MEDIUM"
verticalQuality = "MEDIUM"
[client.advanced.autoUpdater]
enableAutoUpdater = false
enableSilentUpdates = false
-18
View File
@@ -1,18 +0,0 @@
# Only explicit overrides; DH fills in other defaults on first launch.
[common.multiThreading]
numberOfThreads = 2
threadRunTimeRatio = "0.5"
threadPriority = 3
[common.worldGenerator]
limitIfServerUnhealthy = true
pauseIfMovingQuickly = true
[client.advanced.graphics.quality]
lodChunkRenderDistanceRadius = 128
horizontalQuality = "MEDIUM"
verticalQuality = "MEDIUM"
[client.advanced.autoUpdater]
enableAutoUpdater = false
enableSilentUpdates = false
-323
View File
@@ -1,323 +0,0 @@
{
"format": 1,
"minecraft": "26.3",
"java_major": 25,
"fabric_loader": "0.19.5",
"pack_version": "0.21.0",
"options_data_version": 5023,
"projects": {
"sodium": {
"slug": "sodium",
"project_id": "AANobbMI",
"version_id": "bAZQdGpg",
"version": "mc26.3-0.9.2-fabric",
"game_versions": [
"26.3"
],
"loaders": [
"fabric"
],
"license": "LicenseRef-Polyform-Shield-1.0.0",
"source": "https://github.com/CaffeineMC/sodium",
"url": "https://cdn.modrinth.com/data/AANobbMI/versions/bAZQdGpg/sodium-fabric-0.9.2%2Bmc26.3.jar",
"filename": "sodium-fabric-0.9.2+mc26.3.jar",
"size": 1908068,
"hashes": {
"sha1": "9acfe851e36f4fb27d7c5baa224e8bd4e0334283",
"sha512": "f3f260f204b8ce5e8777c2c73f537956755ec61f3817b0b78b5e5568e8aa40f14891f146475075109747665c7c88a45ca0ca9a8df3c68d79c58ecdaa8d8c4b93"
},
"dependencies": [],
"client_side": "required",
"server_side": "unsupported"
},
"lithium": {
"slug": "lithium",
"project_id": "gvQqBUqZ",
"version_id": "xS0Q8LSi",
"version": "mc26.3-0.26.2-fabric",
"game_versions": [
"26.3"
],
"loaders": [
"fabric",
"quilt"
],
"license": "LGPL-3.0-only",
"source": "https://github.com/caffeinemc/lithium-fabric",
"url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/xS0Q8LSi/lithium-fabric-0.26.2%2Bmc26.3.jar",
"filename": "lithium-fabric-0.26.2+mc26.3.jar",
"size": 914543,
"hashes": {
"sha512": "4d7fee66132eedc71feab9390b92c95d7058edbdad0fecfac1d836a2950b97a7ca463afbede61c7ef361ce65e1f927ab9e89dde5bf2e0e0ce486afb5c5dbee40",
"sha1": "dd5a4ec2c68d7601d8bce9076f789c55a5f8bef3"
},
"dependencies": [],
"client_side": "optional",
"server_side": "optional"
},
"iris": {
"slug": "iris",
"project_id": "YL57xq9U",
"version_id": "vTN4NRGW",
"version": "1.11.7+26.3-fabric",
"game_versions": [
"26.3"
],
"loaders": [
"fabric"
],
"license": "LGPL-3.0-only",
"source": "https://github.com/IrisShaders/Iris",
"url": "https://cdn.modrinth.com/data/YL57xq9U/versions/vTN4NRGW/iris-fabric-1.11.7%2Bmc26.3.jar",
"filename": "iris-fabric-1.11.7+mc26.3.jar",
"size": 2850938,
"hashes": {
"sha1": "fa03640ba1e4ef2f7ae3619c6373a06e46d0ba2c",
"sha512": "d8d3312f77fc52a48e4a6cd836fda33669bdebcb4de0b8c0d30adc808b287330a533e0baef355cc1ce6336a26851d9775369a802a4666b7b7a6266af20088f42"
},
"dependencies": [
{
"version_id": "bAZQdGpg",
"project_id": "AANobbMI",
"file_name": null,
"dependency_type": "required"
}
],
"client_side": "required",
"server_side": "unsupported"
},
"distanthorizons": {
"slug": "distanthorizons",
"project_id": "uCdwusMi",
"version_id": "TGgEbP9A",
"version": "3.3.4-26.3",
"game_versions": [
"26.3"
],
"loaders": [
"fabric",
"neoforge"
],
"license": "LGPL-3.0-only",
"source": "https://gitlab.com/distant-horizons-team/distant-horizons",
"url": "https://cdn.modrinth.com/data/uCdwusMi/versions/TGgEbP9A/DistantHorizons-3.3.4-26.3-fabric-neoforge.jar",
"filename": "DistantHorizons-3.3.4-26.3-fabric-neoforge.jar",
"size": 28476894,
"hashes": {
"sha512": "0b90b1b08d99974bfc3c8e86b2ceb252e2f91715ed66b925004c28335fb44e1bddbbb6b8043e4d4d53c87ed247789cd1788b1ba4e3f4394efe154e0c59cff236",
"sha1": "310c0363068dfae18e91f799ed671a591b6ad49a"
},
"dependencies": [],
"client_side": "optional",
"server_side": "optional"
},
"ferrite-core": {
"slug": "ferrite-core",
"project_id": "uXXizFIs",
"version_id": "d5ddUdiB",
"version": "9.0.0-fabric",
"game_versions": [
"26.1",
"26.1.1",
"26.1.2",
"26.2",
"26.3"
],
"loaders": [
"fabric"
],
"license": "MIT",
"source": "https://github.com/malte0811/FerriteCore",
"url": "https://cdn.modrinth.com/data/uXXizFIs/versions/d5ddUdiB/ferritecore-9.0.0-fabric.jar",
"filename": "ferritecore-9.0.0-fabric.jar",
"size": 72677,
"hashes": {
"sha1": "eac76ff0f3753422c61b2c44487d0d195d88d4bc",
"sha512": "d81fa97e11784c19d42f89c2f433831d007603dd7193cee45fa177e4a6a9c52b384b198586e04a0f7f63cd996fed713322578bde9a8db57e1188854ae5cbe584"
},
"dependencies": [],
"client_side": "optional",
"server_side": "optional"
},
"fabric-api": {
"slug": "fabric-api",
"project_id": "P7dR8mSH",
"version_id": "bNnaTiuM",
"version": "0.161.0+26.3",
"game_versions": [
"26.3"
],
"loaders": [
"fabric"
],
"license": "Apache-2.0",
"source": "https://github.com/FabricMC/fabric",
"url": "https://cdn.modrinth.com/data/P7dR8mSH/versions/bNnaTiuM/fabric-api-0.161.0%2B26.3.jar",
"filename": "fabric-api-0.161.0+26.3.jar",
"size": 2620967,
"hashes": {
"sha1": "53f9ee02c3702734d90520aa8c9719bf0e1c8d4f",
"sha512": "ed6b2586d6fde11fde8472f5a527c51e99b67026e46f94d4bfd85e7e28ce5ee299173ee16ad576ceb51f39f98d30a811086a6deb1a86a524859cc16e12da109d"
},
"dependencies": [],
"client_side": "optional",
"server_side": "optional"
},
"entityculling": {
"slug": "entityculling",
"project_id": "NNAgCjsB",
"version_id": "F4loCvYt",
"version": "1.11.2",
"game_versions": [
"26.3"
],
"loaders": [
"fabric"
],
"license": "LicenseRef-tr7zw-Protective-License",
"source": "https://github.com/tr7zw/EntityCulling",
"url": "https://cdn.modrinth.com/data/NNAgCjsB/versions/F4loCvYt/entityculling-fabric-1.11.2-mc26.3.jar",
"filename": "entityculling-fabric-1.11.2-mc26.3.jar",
"size": 1508273,
"hashes": {
"sha1": "f2196e803fe77cfbb62c4a2acb64803d98b4f61c",
"sha512": "c5d8947cb022f8baed4bf17e38ca598c56b8049871e7e47fb462bcecc9bc01e7eb75290494199fcb1aa73f5e37f93a296211ba78c9dd2a5bff30d5eed497ebcc"
},
"dependencies": [
{
"version_id": null,
"project_id": "P7dR8mSH",
"file_name": null,
"dependency_type": "required"
}
],
"client_side": "required",
"server_side": "unsupported"
},
"immediatelyfast": {
"slug": "immediatelyfast",
"project_id": "5ZwdcRci",
"version_id": "3MP9UR23",
"version": "1.17.1+26.3-fabric",
"game_versions": [
"26.3"
],
"loaders": [
"fabric",
"quilt"
],
"license": "LGPL-3.0-or-later",
"source": "https://github.com/RaphiMC/ImmediatelyFast",
"url": "https://cdn.modrinth.com/data/5ZwdcRci/versions/3MP9UR23/ImmediatelyFast-Fabric-1.17.1%2B26.3.jar",
"filename": "ImmediatelyFast-Fabric-1.17.1+26.3.jar",
"size": 99586,
"hashes": {
"sha512": "8e139589fd995bce3d485a34d92a07243964212f7ed2e4fc2f6de5b9a74d0f59b03252e7ce40c64e006cf3e21bed5de27b830d33ef048b286e041f6178ad8428",
"sha1": "f565bbeb201f2305e81761429d3f1f47e1c3a321"
},
"dependencies": [],
"client_side": "required",
"server_side": "unsupported"
},
"complementary-reimagined": {
"slug": "complementary-reimagined",
"project_id": "HVnmMxH1",
"version_id": "Bqen1mJX",
"version": "r5.9.3",
"game_versions": [
"1.7.10",
"1.8",
"1.8.1",
"1.8.2",
"1.8.3",
"1.8.4",
"1.8.5",
"1.8.6",
"1.8.7",
"1.8.8",
"1.8.9",
"1.9",
"1.9.1",
"1.9.2",
"1.9.3",
"1.9.4",
"1.10",
"1.10.1",
"1.10.2",
"1.11",
"1.11.1",
"1.11.2",
"1.12",
"1.12.1",
"1.12.2",
"1.13",
"1.13.1",
"1.13.2",
"1.14",
"1.14.1",
"1.14.2",
"1.14.3",
"1.14.4",
"1.15",
"1.15.1",
"1.15.2",
"1.16",
"1.16.1",
"1.16.2",
"1.16.3",
"1.16.4",
"1.16.5",
"1.17",
"1.17.1",
"1.18",
"1.18.1",
"1.18.2",
"1.19",
"1.19.1",
"1.19.2",
"1.19.3",
"1.19.4",
"1.20",
"1.20.1",
"1.20.2",
"1.20.3",
"1.20.4",
"1.20.5",
"1.20.6",
"1.21",
"1.21.1",
"1.21.2",
"1.21.3",
"1.21.4",
"1.21.5",
"1.21.6",
"1.21.7",
"1.21.8",
"1.21.9",
"1.21.10",
"1.21.11",
"26.1",
"26.1.1",
"26.1.2",
"26.2",
"26.3"
],
"loaders": [
"iris",
"optifine"
],
"license": "LicenseRef-Custom",
"source": "https://github.com/ComplementaryDevelopment/ComplementaryReimagined",
"url": "https://cdn.modrinth.com/data/HVnmMxH1/versions/Bqen1mJX/ComplementaryReimagined_r5.9.3.zip",
"filename": "ComplementaryReimagined_r5.9.3.zip",
"size": 553397,
"hashes": {
"sha1": "838139b54cddb56b2e83cd260d8efd960ac536d6",
"sha512": "45304b1d7862afdb2177b7e6226fc9c6737911d17a4ad46cc9c519ca4deb935f05072cd337cd8b19a15bb8062b57b54eceec7bdee4cd572d94858e2946d37d85"
},
"dependencies": [],
"client_side": "required",
"server_side": "unsupported"
}
}
}
-119
View File
@@ -1,119 +0,0 @@
import json
import subprocess
import unittest
from tools.test_dispatch import failure_summary
from tools.test_client_graphics import graphics_failure_summary, validate_shader_report
from tools.test_culling_rays import FixtureTimeout
class FixtureDiagnosticTests(unittest.TestCase):
def test_timeout_preserves_selected_stages_without_private_content(self):
output = '\n'.join([
'private-token /private/world/path',
'HORIZON_CLIENT_GRAPHICS_INITIALIZER_READY',
'HORIZON_CLIENT_GRAPHICS_WAIT_STATE {"world_loaded":true,"player_present":true,"overlay_present":false,"screen_present":true,"window_active":false,"paused":true,"ready_client_ticks":0,"ignored":"private-token"}',
'HORIZON_CLIENT_GRAPHICS_WAIT_STATE {"world_loaded":"private-token"}',
])
error = FixtureTimeout(subprocess.CompletedProcess(['private-command'], -15, output), 'private-log')
self.assertIsInstance(error, RuntimeError)
result = graphics_failure_summary(error.run, 0)
self.assertEqual(result['exit_code'], -15)
self.assertTrue(result['client_initializer_reached'])
self.assertFalse(result['readback_requested'])
self.assertEqual(result['last_waiting_state'], {'world_loaded': True, 'player_present': True,
'overlay_present': False, 'screen_present': True, 'window_active': False, 'paused': True,
'ready_client_ticks': 0})
self.assertNotIn('private', json.dumps(result))
def test_required_shaders_cannot_pass_with_disabled_unknown_or_different_pack(self):
report = {'shader_required': True, 'iris_capture_status': 'available',
'iris_shaders_enabled': True, 'iris_expected_pack_matches': True}
validate_shader_report(report, True)
for field, bad in [('shader_required', False), ('iris_capture_status', 'unavailable'),
('iris_shaders_enabled', False), ('iris_shaders_enabled', 'true'),
('iris_expected_pack_matches', False)]:
with self.subTest(field=field, bad=bad), self.assertRaises(AssertionError):
validate_shader_report({**report, field: bad}, True)
with self.assertRaises(AssertionError):
validate_shader_report({}, True)
validate_shader_report({}, False)
def test_graphics_diagnostics_separate_demo_auth_and_exclude_methods_messages(self):
output = '\n'.join([
'com.mojang.authlib.exceptions.InvalidCredentialsException: private-token',
' at com.mojang.authlib.minecraft.client.MinecraftClient.buildHttpException(File.java:1)',
'java.lang.AssertionError: /private/world/path',
'Caused by: java.lang.RuntimeException: private-message VK_ERROR_FEATURE_NOT_PRESENT',
'HORIZON_CLIENT_GRAPHICS_INITIALIZER_READY',
'HORIZON_CLIENT_GRAPHICS_READBACK_REQUESTED',
])
result = graphics_failure_summary(subprocess.CompletedProcess([], 1, output), 0)
self.assertEqual(result['java_exception_types'], ['java.lang.AssertionError', 'java.lang.RuntimeException'])
self.assertTrue(result['expected_demo_authentication_errors_observed'])
self.assertTrue(result['client_initializer_reached'])
self.assertTrue(result['readback_requested'])
self.assertEqual(result['vulkan_error_codes'], ['VK_ERROR_FEATURE_NOT_PRESENT'])
self.assertNotIn('private', json.dumps(result))
self.assertNotIn('buildHttpException', json.dumps(result))
def test_only_exit_count_and_exception_types_leave_private_log(self):
output = '\n'.join([
'Exception in thread "private-thread-name" java.lang.NoClassDefFoundError: private-token',
'Caused by: java.lang.ClassNotFoundException: /private/world/path',
'Caused by: java.lang.ClassNotFoundException: other-private-message',
' at private.World.method(World.java:42)',
'session-token=private-token',
])
run = subprocess.CompletedProcess(['private-command'], 1, output)
report = failure_summary(run, 0)
self.assertEqual(report, {'exit_code': 1, 'result_count': 0,
'java_exception_types': ['java.lang.NoClassDefFoundError', 'java.lang.ClassNotFoundException']})
self.assertNotIn('private', json.dumps(report))
def test_graphics_stack_locations_are_selected_and_bounded(self):
frames = [' at knot//com.mojang.renderpearl.backend.vulkan.Buffer.method%d(PrivateFile.java:42)' % index
for index in range(12)]
output = '\n'.join([
'message: at com.mojang.blaze3d.Bad.message(Private.java:1)',
' at com.mojang.authlib.Auth.privateToken(PrivateFile.java:2)',
' at net.minecraft.client.Screenshot.takeScreenshot(/private/file.java:3)',
*frames, *frames,
])
result = graphics_failure_summary(subprocess.CompletedProcess([], 1, output), 0)
methods = result['observed_graphics_stack_methods']
self.assertEqual(len(methods), 8)
self.assertEqual(methods[0], 'net.minecraft.client.Screenshot.takeScreenshot')
self.assertEqual(methods[1], 'com.mojang.renderpearl.backend.vulkan.Buffer.method0')
self.assertNotIn('private', json.dumps(result))
self.assertNotIn('Private', json.dumps(result))
self.assertNotIn('Bad', json.dumps(result))
def test_owned_probe_failure_is_separate_from_logged_vulkan_codes(self):
output = '\n'.join([
'logged separately: VK_ERROR_FEATURE_NOT_PRESENT private-message',
'java.lang.AssertionError: Uniform title readback',
'HORIZON_CLIENT_GRAPHICS_READBACK_RECEIVED',
'HORIZON_CLIENT_GRAPHICS_READBACK_INSPECTED {"framebuffer_width":1280,"framebuffer_height":646,"sampled_pixels":4160,"distinct_sampled_rgb":1,"ignored":"private-token"}',
'HORIZON_CLIENT_GRAPHICS_READBACK_INSPECTED {"framebuffer_width":"private-path"}',
'HORIZON_CLIENT_GRAPHICS_READBACK_INSPECTED private-malformed',
])
result = graphics_failure_summary(subprocess.CompletedProcess([], 1, output), 0)
self.assertEqual(result['probe_failure_categories'], ['uniform_readback'])
self.assertTrue(result['readback_callback_received'])
self.assertEqual(result['readback_inspections'][0]['distinct_sampled_rgb'], 1)
self.assertEqual(len(result['readback_inspections']), 1)
self.assertEqual(result['vulkan_error_codes'], ['VK_ERROR_FEATURE_NOT_PRESENT'])
self.assertNotIn('private', json.dumps(result))
def test_type_list_and_names_are_bounded(self):
output = '\n'.join('Caused by: sample.Failure%d: private-message' % index for index in range(20))
output = 'Caused by: sample.' + 'A' * 201 + ': private-message\n' + output
report = failure_summary(subprocess.CompletedProcess([], 2, output), 3)
self.assertEqual(len(report['java_exception_types']), 8)
self.assertEqual(report['java_exception_types'][0], 'sample.Failure0')
self.assertEqual(report['result_count'], 3)
def test_unknown_failure_still_reports_exit_and_count(self):
report = failure_summary(subprocess.CompletedProcess([], -15, 'private unrelated message'), 0)
self.assertEqual(report, {'exit_code': -15, 'result_count': 0, 'java_exception_types': []})
-447
View File
@@ -1,447 +0,0 @@
import copy
import json
from pathlib import Path
import tempfile
import unittest
from unittest import mock
import zipfile
from tools import build_pack, compare_benchmarks, runtime
class PackTests(unittest.TestCase):
def test_rebrand_upgrade_retires_legacy_and_current_names(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
mods = root / "instance/mods"
mods.mkdir(parents=True)
previous = [mods / "horizon-lab-0.20.0.jar", mods / "horizon-0.21.0.jar"]
for old in previous:
with zipfile.ZipFile(old, "w") as archive:
archive.writestr("fabric.mod.json", json.dumps({"id": "horizonlab"}))
new = root / "horizon-0.22.0.jar"
new.write_bytes(b"new original fixture")
runtime.install_original_mod(new, mods)
self.assertTrue(all(not old.exists() for old in previous))
self.assertEqual(list(mods.iterdir()), [mods / new.name])
self.assertEqual(len(list((mods.parent / ".horizon-mod-backups").glob("*.jar"))), 2)
def test_original_mod_upgrade_preserves_backup_and_other_mods(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
mods = root / "instance/mods"
mods.mkdir(parents=True)
old = mods / "horizon-0.2.0.jar"
with zipfile.ZipFile(old, "w") as archive:
archive.writestr("fabric.mod.json", json.dumps({"id": "horizonlab"}))
old_bytes = old.read_bytes()
other = mods / "other.jar"
other.write_bytes(b"another mod")
new = root / "horizon-0.3.0.jar"
new.write_bytes(b"new original fixture")
runtime.install_original_mod(new, mods)
self.assertFalse(old.exists())
self.assertEqual((mods / new.name).read_bytes(), new.read_bytes())
self.assertEqual(other.read_bytes(), b"another mod")
backups = list((mods.parent / ".horizon-mod-backups").glob("*.jar"))
self.assertEqual(len(backups), 1)
self.assertEqual(backups[0].read_bytes(), old_bytes)
def test_original_mod_upgrade_refuses_unrelated_named_jar(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
with zipfile.ZipFile(root / "horizon-0.1.0.jar", "w") as archive:
archive.writestr("fabric.mod.json", json.dumps({"id": "someoneelse"}))
new = root / "horizon-0.3.0.jar"
new.write_bytes(b"new original fixture")
with self.assertRaisesRegex(ValueError, "another mod"):
runtime.install_original_mod(new, root)
def test_every_profile_has_correct_game_and_exact_dependency_closure(self):
lock = build_pack.read_lock()
for profile in build_pack.PROFILES.values():
build_pack.validate_selection(lock, profile["mods"])
def test_core_performance_matches_baseline_view_and_simulation(self):
baseline = build_pack.PROFILES["baseline"]
candidate = build_pack.PROFILES["core-performance"]
self.assertEqual(candidate["mods"], build_pack.CORE)
for key in ["render", "lod", "heap"]:
self.assertEqual(candidate[key], baseline[key])
files = build_pack.overrides("core-performance", b"original fixture")
before = build_pack.overrides("baseline", b"original fixture")
for name, value in before.items():
if name != "config/horizon-profile.txt":
self.assertEqual(files[name], value, name)
self.assertNotIn("config/DistantHorizons.toml", files)
self.assertNotIn("config/iris.properties", files)
self.assertNotIn("distanthorizons", candidate["mods"])
self.assertNotIn("iris", candidate["mods"])
def test_missing_exact_iris_sodium_dependency_is_rejected(self):
with self.assertRaisesRegex(ValueError, "Missing pinned dependency"):
build_pack.validate_selection(build_pack.read_lock(), ["iris"])
def test_dispatch_experiment_does_not_enable_other_controllers(self):
for name, profile in build_pack.PROFILES.items():
files = build_pack.overrides(name, b"original fixture")
dispatch = files["config/horizon-dispatch-retry.properties"].decode()
self.assertEqual(dispatch, "enabled=true\n" if name in {"dispatch-retry", "foreground-first", "foreground-vulkan"} else "enabled=false\n")
if name == "dispatch-retry":
for setting in ["worker-pacing", "queue-wait"]:
self.assertEqual(files[f"config/horizon-{setting}.properties"], b"enabled=false\n")
self.assertTrue(files["config/horizon-adaptive.properties"].startswith(b"enabled=false\n"))
self.assertEqual(files["config/DistantHorizons.toml"],
build_pack.overrides("m2-balanced", b"original fixture")["config/DistantHorizons.toml"])
def test_combined_profiles_enable_all_controls_without_changing_simulation(self):
for name, backend in [("foreground-first", "opengl"), ("foreground-vulkan", "vulkan")]:
files = build_pack.overrides(name, b"original fixture")
self.assertEqual(files["config/horizon-adaptive.properties"], b"enabled=true\ntargetFps=120\n")
for key in ["worker-pacing", "queue-wait", "dispatch-retry", "tick-dedup"]:
self.assertEqual(files[f"config/horizon-{key}.properties"], b"enabled=true\n")
options = dict(line.split(":", 1) for line in files["options.txt"].decode().splitlines())
self.assertEqual(options["simulationDistance"], "12")
self.assertEqual(options["renderDistance"], "12")
self.assertEqual(options["maxFps"], "260")
self.assertEqual(json.loads(options["preferredGraphicsBackend"]), backend)
self.assertNotIn("config/iris.properties", files)
def test_tick_membership_experiment_preserves_visual_and_worker_settings(self):
original = build_pack.overrides("m2-balanced", b"fixture")
for name in build_pack.PROFILES:
files = build_pack.overrides(name, b"fixture")
self.assertEqual(files["config/horizon-tick-dedup.properties"],
b"enabled=true\n" if name in {"tick-dedup", "foreground-first", "foreground-vulkan"} else b"enabled=false\n")
candidate = build_pack.overrides("tick-dedup", b"fixture")
for filename in ["options.txt", "config/DistantHorizons.toml", "config/horizon-adaptive.properties",
"config/horizon-worker-pacing.properties", "config/horizon-queue-wait.properties",
"config/horizon-dispatch-retry.properties"]:
self.assertEqual(original[filename], candidate[filename])
def test_empty_culling_experiment_preserves_balanced_settings(self):
original = build_pack.overrides("m2-balanced", b"fixture")
for name in build_pack.PROFILES:
files = build_pack.overrides(name, b"fixture")
self.assertEqual(files["config/horizon-culling-empty.properties"],
b"enabled=true\n" if name == "culling-empty" else b"enabled=false\n")
candidate = build_pack.overrides("culling-empty", b"fixture")
for filename in ["options.txt", "config/DistantHorizons.toml", "config/horizon-adaptive.properties",
"config/horizon-worker-pacing.properties", "config/horizon-queue-wait.properties",
"config/horizon-dispatch-retry.properties", "config/horizon-tick-dedup.properties"]:
self.assertEqual(original[filename], candidate[filename])
def test_ray_scratch_experiment_preserves_balanced_settings(self):
baseline = build_pack.overrides("m2-balanced", b"fixture")
candidate = build_pack.overrides("culling-rays", b"fixture")
for name in build_pack.PROFILES:
self.assertEqual(build_pack.overrides(name, b"fixture")["config/horizon-culling-rays.properties"],
b"enabled=true\n" if name == "culling-rays" else b"enabled=false\n")
for filename in ["options.txt", "config/DistantHorizons.toml", "config/horizon-adaptive.properties",
"config/horizon-worker-pacing.properties", "config/horizon-queue-wait.properties",
"config/horizon-dispatch-retry.properties", "config/horizon-tick-dedup.properties",
"config/horizon-culling-empty.properties", "config/horizon-terrain-read-wait.properties"]:
self.assertEqual(baseline[filename], candidate[filename])
def test_position_experiment_preserves_balanced_settings(self):
baseline = build_pack.overrides("m2-balanced", b"fixture")
candidate = build_pack.overrides("terrain-position", b"fixture")
for name in build_pack.PROFILES:
self.assertEqual(build_pack.overrides(name, b"fixture")["config/horizon-terrain-position.properties"],
b"enabled=true\n" if name == "terrain-position" else b"enabled=false\n")
for filename in ["options.txt", "config/DistantHorizons.toml", "config/horizon-adaptive.properties",
"config/horizon-worker-pacing.properties", "config/horizon-queue-wait.properties",
"config/horizon-dispatch-retry.properties", "config/horizon-tick-dedup.properties",
"config/horizon-culling-empty.properties", "config/horizon-culling-rays.properties", "config/horizon-terrain-read-wait.properties"]:
self.assertEqual(baseline[filename], candidate[filename])
def test_quad_reuse_profile_is_isolated_at_equal_visual_quality(self):
baseline = build_pack.overrides("m2-balanced", b"fixture")
candidate = build_pack.overrides("quad-reuse", b"fixture")
for name in build_pack.PROFILES:
self.assertEqual(build_pack.overrides(name, b"fixture")["config/horizon-quad-reuse.properties"],
b"enabled=true\n" if name == "quad-reuse" else b"enabled=false\n")
for filename, content in baseline.items():
if filename not in {"config/horizon-profile.txt", "config/horizon-quad-reuse.properties"}:
self.assertEqual(content, candidate[filename], filename)
def test_terrain_read_shutdown_fix_only_requested_with_distant_terrain(self):
for name, profile in build_pack.PROFILES.items():
files = build_pack.overrides(name, b"fixture")
self.assertEqual(files["config/horizon-terrain-read-wait.properties"],
b"enabled=true\n" if profile["lod"] else b"enabled=false\n")
def test_wrong_game_or_download_path_is_rejected(self):
for field, value in [("game_versions", ["1.21"]), ("filename", "../bad.jar"), ("url", "http://unsafe.invalid")]:
lock = copy.deepcopy(build_pack.read_lock())
lock["projects"]["sodium"][field] = value
with self.assertRaises(ValueError):
build_pack.validate_selection(lock, ["sodium"])
def test_portable_pack_contains_only_original_embedded_mod_and_download_references(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
(root / "packs").mkdir()
(root / "packs/mods.lock.json").write_text(json.dumps(build_pack.read_lock()))
(root / "packs/config").mkdir()
(root / "packs/config/m2-balanced.toml").write_text("[client.advanced.graphics.quality]\nlodChunkRenderDistanceRadius = 128\n")
original = root / "original.jar"
original.write_bytes(b"original fixture")
with mock.patch.object(build_pack, "ROOT", root):
pack = build_pack.build("m2-balanced", original)
with zipfile.ZipFile(pack) as archive:
names = archive.namelist()
self.assertEqual([n for n in names if n.endswith(".jar")],
[f"overrides/mods/horizon-{build_pack.read_lock()['pack_version']}.jar"])
index = json.loads(archive.read("modrinth.index.json"))
self.assertEqual(index["dependencies"]["minecraft"], "26.3")
options = dict(line.split(":", 1) for line in archive.read("overrides/options.txt").decode().splitlines())
# A missing/currently wrong version invokes the game's graphics API reset migration.
self.assertEqual(options["version"], "5023")
self.assertEqual(json.loads(options["preferredGraphicsBackend"]), "opengl")
# The default Fancy startup preset otherwise overwrites the requested view distance.
self.assertEqual(json.loads(options["graphicsPreset"]), "custom")
for entry in index["files"]:
self.assertEqual(set(entry["hashes"]), {"sha1", "sha512"})
self.assertTrue(entry["downloads"][0].startswith("https://cdn.modrinth.com/"))
def test_native_library_rules_choose_only_matching_operating_system(self):
library = {"rules": [{"action": "allow", "os": {"name": "osx", "arch": "aarch64|arm64"}}]}
self.assertTrue(runtime.library_allowed(library, "osx", "aarch64"))
self.assertFalse(runtime.library_allowed(library, "linux", "aarch64"))
self.assertFalse(runtime.library_allowed(library, "osx", "x86_64"))
def test_conditional_client_jvm_arguments_follow_official_platform_rules(self):
windows = "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"
manifest = {"arguments": {"jvm": [
{"rules": [{"action": "allow", "os": {"name": "osx"}}], "value": ["-XstartOnFirstThread"]},
{"rules": [{"action": "allow", "os": {"name": "windows"}}], "value": windows},
{"rules": [{"action": "allow", "os": {"arch": "x86"}}], "value": "-Xss1M"},
"-cp", "${classpath}", "--enable-native-access=ALL-UNNAMED",
]}}
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "osx", "aarch64"), ["-XstartOnFirstThread"])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "osx", "x86_64"), ["-XstartOnFirstThread"])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "windows", "AMD64"), [windows])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "windows", "arm64"), [windows])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "linux", "x86_64"), [])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "windows", "x86"), [windows, "-Xss1M"])
# Client platform flags must not leak into the shared server JVM policy.
self.assertEqual(runtime.jvm_options("3G", True, "g1"),
["-Xms512M", "-Xmx3G", "-XX:+UseCompactObjectHeaders", "-XX:+UseG1GC"])
def test_conditional_client_jvm_arguments_observe_rule_order_and_reject_placeholders(self):
argument = {"rules": [{"action": "allow", "os": {"name": "osx"}},
{"action": "disallow", "os": {"arch": "x86_64"}}], "value": "-XstartOnFirstThread"}
manifest = {"arguments": {"jvm": [argument]}}
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "osx", "aarch64"), ["-XstartOnFirstThread"])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "osx", "x86_64"), [])
self.assertEqual(runtime.conditional_client_jvm_options({}, "linux", "x86_64"), [])
argument["value"] = "${unsupported}"
with self.assertRaisesRegex(ValueError, "Unsupported"):
runtime.conditional_client_jvm_options(manifest, "osx", "aarch64")
def test_demo_command_includes_mac_first_thread_flag_before_main_class(self):
manifest = {"id": "26.3", "assetIndex": {"id": "fixture"}, "arguments": {"jvm": [
{"rules": [{"action": "allow", "os": {"name": "osx"}}], "value": "-XstartOnFirstThread"}]}}
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
with mock.patch.object(runtime, "prepare_client", return_value=(manifest, [root / "game.jar"], root / "natives")), \
mock.patch.object(runtime.platform, "system", return_value="Darwin"), \
mock.patch.object(runtime.subprocess, "run") as run, \
mock.patch("sys.argv", ["runtime.py", "demo", "--instance", str(root / "instance")]):
runtime.main()
command = run.call_args.args[0]
self.assertEqual(command.count("-XstartOnFirstThread"), 1)
self.assertLess(command.index("-XstartOnFirstThread"),
command.index("net.fabricmc.loader.impl.launch.knot.KnotClient"))
self.assertEqual(command.count("-cp"), 1)
self.assertTrue(run.call_args.kwargs["check"])
def test_native_architecture_is_selected_even_with_os_only_rules(self):
intel = {"name": "org.lwjgl:lwjgl:3.4.3:natives-macos"}
arm = {"name": "org.lwjgl:lwjgl:3.4.3:natives-macos-arm64"}
self.assertTrue(runtime.native_library_matches(arm, "aarch64"))
self.assertFalse(runtime.native_library_matches(intel, "aarch64"))
self.assertTrue(runtime.native_library_matches(intel, "x86_64"))
self.assertFalse(runtime.native_library_matches(arm, "AMD64"))
class ComparisonTests(unittest.TestCase):
def report(self, kind="frames"):
return {"schema": 1, "kind": kind, "elapsed_seconds": 120, "framebuffer_width": 1920,
"stats": {"count": 1000, "meanRate": 100, "lowOnePercentRate": 50,
"meanMs": 10, "p95Ms": 15, "p99Ms": 20, "maxMs": 30}}
def test_integrated_tick_graphics_and_server_kind(self):
baseline = self.tick_report()
baseline.update(server_runtime_kind="integrated", client_graphics_capture_status="captured_at_recording_start",
gpu="GPU", graphics_backend="OpenGL", graphics_driver="Driver")
candidate = copy.deepcopy(baseline)
candidate["graphics_backend"] = "Vulkan"
result = compare_benchmarks.compare(baseline, candidate)
self.assertTrue(any("graphics_backend" in item for item in result["mismatches"]))
self.assertFalse(any("were not captured" in item for item in compare_benchmarks.server_context_warnings(baseline, candidate)))
candidate["server_runtime_kind"] = "dedicated"
self.assertTrue(any("server_runtime_kind" in item for item in compare_benchmarks.compare(baseline, candidate)["mismatches"]))
def test_legacy_and_missing_integrated_graphics_are_unknown(self):
legacy = self.tick_report()
self.assertTrue(any("runtime kind was not captured" in item for item in compare_benchmarks.server_context_warnings(legacy, legacy)))
legacy.update(server_runtime_kind="integrated", client_graphics_capture_status="captured_at_recording_start", gpu="GPU", graphics_backend="OpenGL")
self.assertTrue(any("client graphics were not captured" in item for item in compare_benchmarks.server_context_warnings(legacy, legacy)))
dedicated = {"server_runtime_kind": "dedicated", "client_graphics_capture_status": "not_supplied"}
self.assertFalse(any("were not captured" in item for item in compare_benchmarks.server_context_warnings(dedicated, dedicated)))
def test_rate_improvement_and_setting_mismatch(self):
baseline = self.report()
candidate = copy.deepcopy(baseline)
candidate["stats"]["meanRate"] = 150
candidate["framebuffer_width"] = 1280
result = compare_benchmarks.compare(baseline, candidate)
self.assertEqual(result["metrics"][0]["change_percent"], 50)
self.assertIn("framebuffer_width", result["mismatches"][0])
def tick_report(self):
report = self.report("ticks")
state = dict(configured_tick_rate=20, frozen=False, stepping=False, sprinting=False, runs_game_elements=True)
report.update(tick_state_capture_status="sampled_server_ticks", tick_state_samples=1000,
tick_state_first=state, tick_state_last=copy.deepcopy(state), tick_state_changed=[],
observed_ticks_per_second=1000 / 120)
report.update({"tick_state_" + field + "_samples": 0
for field in ["frozen", "stepping", "sprinting", "suppressed", "nonstandard_rate"]})
return report
def test_restored_tick_changes_still_disqualify_unchanged_workload(self):
baseline = self.tick_report()
candidate = copy.deepcopy(baseline)
candidate.update(tick_state_changed=["configured_tick_rate", "frozen"],
tick_state_frozen_samples=50, tick_state_nonstandard_rate_samples=50)
self.assertEqual(baseline["tick_state_first"], candidate["tick_state_last"])
result = compare_benchmarks.compare(baseline, candidate)
self.assertIn("candidate tick-manager state changed during recording", result["mismatches"])
self.assertTrue(any("frozen, nonstandard_rate" in warning for warning in result["warnings"]))
observed = next(row for row in result["metrics"] if row["metric"] == "observed_ticks_per_second")
self.assertAlmostEqual(observed["baseline"], 8.333333333)
self.assertTrue(any("execution capacity is not TPS" in warning for warning in result["warnings"]))
def test_equal_tick_state_ignores_map_order_and_warns_on_custom_rate(self):
baseline = self.tick_report()
for boundary in ["tick_state_first", "tick_state_last"]:
baseline[boundary]["configured_tick_rate"] = 40
baseline["tick_state_nonstandard_rate_samples"] = 1000
candidate = copy.deepcopy(baseline)
candidate["tick_state_first"] = dict(reversed(list(candidate["tick_state_first"].items())))
result = compare_benchmarks.compare(baseline, candidate)
self.assertEqual(result["mismatches"], [])
self.assertTrue(any("nonstandard_rate" in warning for warning in result["warnings"]))
candidate["tick_state_last"]["frozen"] = True
self.assertTrue(any("tick_state_last.frozen" in mismatch
for mismatch in compare_benchmarks.compare(baseline, candidate)["mismatches"]))
def test_legacy_and_malformed_tick_state_are_unknown_not_normal(self):
baseline = self.tick_report()
for field, bad in [("tick_state_first", None), ("tick_state_samples", True),
("tick_state_samples", -1), ("tick_state_changed", ["invented"]),
("tick_state_frozen_samples", 1001)]:
candidate = copy.deepcopy(baseline)
candidate[field] = bad
self.assertFalse(compare_benchmarks.tick_state_available(candidate))
result = compare_benchmarks.compare(baseline, candidate)
self.assertTrue(any("candidate tick-manager state was not captured" in warning for warning in result["warnings"]))
self.assertFalse(compare_benchmarks.tick_state_available(self.report("ticks")))
def test_no_samples_and_mixed_kinds_are_rejected(self):
empty = self.report()
empty["stats"]["count"] = 0
for other in [empty, self.report("ticks")]:
with self.assertRaises(ValueError):
compare_benchmarks.compare(self.report(), other)
def test_position_hook_change_is_an_explicit_experiment(self):
baseline = self.report()
baseline["terrain_position_arithmetic"] = False
candidate = copy.deepcopy(baseline)
candidate["terrain_position_arithmetic"] = True
result = compare_benchmarks.compare(baseline, candidate)
self.assertEqual(result["mismatches"], [])
self.assertTrue(any("terrain_position_arithmetic differs" in warning for warning in result["warnings"]))
def test_quad_reuse_flag_warns_without_implying_changed_quality(self):
baseline = self.report()
baseline["terrain_quad_reuse"] = False
candidate = copy.deepcopy(baseline)
candidate["terrain_quad_reuse"] = True
result = compare_benchmarks.compare(baseline, candidate)
self.assertEqual(result["mismatches"], [])
self.assertTrue(any("terrain_quad_reuse differs" in warning for warning in result["warnings"]))
def test_mod_comparison_detects_upgrades_and_ignores_order(self):
baseline = self.report()
baseline.update(mods=["[email protected]", "[email protected]", "[email protected]"],
java_vendor="Eclipse Adoptium", java_vm_name="OpenJDK 64-Bit Server VM",
java_runtime_version="25.0.4.1+1")
candidate = copy.deepcopy(baseline)
candidate["mods"] = list(reversed(candidate["mods"]))
self.assertFalse(compare_benchmarks.software_warnings(baseline, candidate))
candidate["mods"].remove("[email protected]")
candidate["mods"].extend(["[email protected]", "[email protected]"])
result = compare_benchmarks.compare(baseline, candidate)
self.assertEqual(result["mismatches"], [])
self.assertIn("Only baseline: [email protected].", result["warnings"])
self.assertIn("Only candidate: [email protected], [email protected].", result["warnings"])
for field in ["java_vendor", "java_vm_name", "java_runtime_version"]:
candidate[field] = "Different implementation"
self.assertTrue(any(f"Software {field} differs" in warning
for warning in compare_benchmarks.software_warnings(baseline, candidate)))
def test_legacy_or_invalid_software_identity_stays_unknown(self):
baseline = self.report("ticks")
for entries in [None, "[email protected]", [3], [""]]:
candidate = copy.deepcopy(baseline)
candidate["mods"] = entries
result = compare_benchmarks.compare(baseline, candidate)
self.assertTrue(any("Loaded mod versions were not captured" in warning for warning in result["warnings"]))
self.assertFalse(any("Loaded mod versions differ." in warning for warning in result["warnings"]))
def test_excluded_frames_disclose_that_process_counters_cover_inactive_time(self):
baseline = self.report()
candidate = copy.deepcopy(baseline)
candidate["excluded_frames"] = 28
result = compare_benchmarks.compare(baseline, candidate)
self.assertEqual(result["mismatches"], [])
self.assertTrue(any("candidate excluded 28" in warning and "Process CPU still spans" in warning
for warning in result["warnings"]))
self.assertFalse(any("baseline excluded" in warning for warning in result["warnings"]))
def test_jvm_experiment_warns_without_rejecting_an_intentional_change(self):
baseline = self.report()
baseline.update(compact_object_headers=False, garbage_collectors=["G1 Young Generation"])
candidate = copy.deepcopy(baseline)
candidate["compact_object_headers"] = True
result = compare_benchmarks.compare(baseline, candidate)
self.assertEqual(result["mismatches"], [])
self.assertTrue(any("compact_object_headers differs" in warning for warning in result["warnings"]))
del candidate["compact_object_headers"]
result = compare_benchmarks.compare(baseline, candidate)
self.assertTrue(any("not captured" in warning for warning in result["warnings"]))
def test_terrain_quality_and_shader_changes_cannot_look_like_equal_workloads(self):
baseline = self.report()
baseline.update(dh_capture_status="available", dh_lod_radius_chunks=128, dh_horizontal_quality="MEDIUM",
iris_capture_status="available", iris_shaders_enabled=False, dh_thread_count=2, world_difficulty="NORMAL")
candidate = copy.deepcopy(baseline)
candidate.update(dh_lod_radius_chunks=256, iris_shaders_enabled=True, iris_shader_pack="Example pack",
dh_thread_count=1, world_difficulty="PEACEFUL", visual_settings_changed=["render_distance"])
result = compare_benchmarks.compare(baseline, candidate)
self.assertTrue(any("dh_lod_radius_chunks" in field for field in result["mismatches"]))
self.assertTrue(any("iris_shaders_enabled" in field for field in result["mismatches"]))
self.assertTrue(any("world_difficulty" in field for field in result["mismatches"]))
self.assertTrue(any("dh_thread_count differs" in warning for warning in result["warnings"]))
self.assertTrue(any("changed between capture boundaries" in warning for warning in result["warnings"]))
legacy = self.report()
result = compare_benchmarks.compare(legacy, copy.deepcopy(legacy))
self.assertTrue(any("terrain configuration was not captured" in warning for warning in result["warnings"]))
if __name__ == "__main__":
unittest.main()
+48
View File
@@ -0,0 +1,48 @@
"""Platform JVM rules used by the Java comparison benchmarks."""
import unittest
from tools import runtime
class RuntimeTests(unittest.TestCase):
def test_native_library_rules_choose_only_matching_operating_system(self):
library = {"rules": [{"action": "allow", "os": {"name": "osx", "arch": "aarch64|arm64"}}]}
self.assertTrue(runtime.library_allowed(library, "osx", "aarch64"))
self.assertFalse(runtime.library_allowed(library, "linux", "aarch64"))
self.assertFalse(runtime.library_allowed(library, "osx", "x86_64"))
def test_conditional_client_jvm_arguments_follow_official_platform_rules(self):
windows = "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"
manifest = {"arguments": {"jvm": [
{"rules": [{"action": "allow", "os": {"name": "osx"}}], "value": ["-XstartOnFirstThread"]},
{"rules": [{"action": "allow", "os": {"name": "windows"}}], "value": windows},
{"rules": [{"action": "allow", "os": {"arch": "x86"}}], "value": "-Xss1M"},
"-cp", "${classpath}", "--enable-native-access=ALL-UNNAMED",
]}}
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "osx", "aarch64"), ["-XstartOnFirstThread"])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "osx", "x86_64"), ["-XstartOnFirstThread"])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "windows", "AMD64"), [windows])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "windows", "arm64"), [windows])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "linux", "x86_64"), [])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "windows", "x86"), [windows, "-Xss1M"])
def test_conditional_client_jvm_arguments_observe_rule_order_and_reject_placeholders(self):
argument = {"rules": [{"action": "allow", "os": {"name": "osx"}},
{"action": "disallow", "os": {"arch": "x86_64"}}], "value": "-XstartOnFirstThread"}
manifest = {"arguments": {"jvm": [argument]}}
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "osx", "aarch64"), ["-XstartOnFirstThread"])
self.assertEqual(runtime.conditional_client_jvm_options(manifest, "osx", "x86_64"), [])
self.assertEqual(runtime.conditional_client_jvm_options({}, "linux", "x86_64"), [])
argument["value"] = "${unsupported}"
with self.assertRaisesRegex(ValueError, "Unsupported"):
runtime.conditional_client_jvm_options(manifest, "osx", "aarch64")
def test_native_architecture_is_selected_even_with_os_only_rules(self):
intel = {"name": "org.lwjgl:lwjgl:3.4.3:natives-macos"}
arm = {"name": "org.lwjgl:lwjgl:3.4.3:natives-macos-arm64"}
self.assertTrue(runtime.native_library_matches(arm, "aarch64"))
self.assertFalse(runtime.native_library_matches(intel, "aarch64"))
self.assertTrue(runtime.native_library_matches(intel, "x86_64"))
self.assertFalse(runtime.native_library_matches(arm, "AMD64"))
-299
View File
@@ -1,299 +0,0 @@
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'; base-uri 'none'; form-action 'none'">
<title>Horizon — Compare runs</title>
<style>
:root{color-scheme:light;--ink:#192920;--muted:#65726a;--line:#d8ded8;--paper:#f6f7f3;--base:#247854;--candidate:#bc642f}
*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{max-width:1100px;margin:auto;padding:40px 28px 70px}header{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:26px}.brand{display:flex;align-items:center;gap:11px;font-weight:700}.mark{width:30px;height:30px;background:var(--base);box-shadow:inset 0 -10px #195a3c,inset -10px 0 #2a8c62}.badge{border:1px solid var(--line);padding:4px 10px;border-radius:20px;color:var(--muted);font-size:12px}h1{font-size:clamp(30px,5vw,44px);letter-spacing:-1.3px;line-height:1.15;margin:0 0 10px}h2{font-size:20px;letter-spacing:-.3px;margin:0}p{margin:8px 0;color:var(--muted)}.intro{max-width:750px;margin-bottom:26px}.uploads{display:grid;grid-template-columns:1fr 1fr;gap:18px}.card{background:white;border:1px solid var(--line);border-radius:12px;padding:22px;min-width:0}.card-top{display:flex;gap:9px;align-items:center;margin-bottom:6px}.dot{width:9px;height:9px;border-radius:50%;background:var(--base)}.candidate .dot{background:var(--candidate)}input[type=file]{width:100%;font:inherit;font-size:13px;color:var(--muted);margin-top:15px}input::file-selector-button,button{font:inherit;font-size:13px;background:white;color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:9px 13px;cursor:pointer;margin-right:10px}button:disabled{opacity:.45;cursor:default}input:focus-visible,button:focus-visible{outline:3px solid #80b998;outline-offset:3px}.status{font-size:13px;overflow-wrap:anywhere;margin-top:14px;min-height:20px}.error{color:#a23624}.details{font-size:12px;color:var(--muted);margin:12px 0 0;display:grid;grid-template-columns:100px 1fr;gap:4px 12px;overflow-wrap:anywhere}.details dt,.details dd{margin:0}.details dt{color:var(--ink)}.empty{border:1px dashed var(--line);border-radius:12px;padding:34px;text-align:center;margin-top:22px}.section{margin-top:24px}.section-top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px}.notice{overflow-wrap:anywhere;background:#fff8e8;border:1px solid #e7d9af;border-radius:9px;padding:14px 18px;margin-bottom:18px;font-size:13px}.notice p{color:#685c3f;margin:3px 0}.notice ul{padding-left:18px;margin:8px 0}.metrics{overflow:auto;border:1px solid var(--line);border-radius:10px;background:white}table{width:100%;border-collapse:collapse;white-space:nowrap;text-align:right;font-variant-numeric:tabular-nums}th,td{padding:12px 18px;border-bottom:1px solid #edf0ea}th{font-size:12px;font-weight:600;color:var(--muted)}th:first-child,td:first-child{text-align:left}tr:last-child td{border-bottom:0}td:first-child{font-weight:500}td:last-child{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:13px}.chart{padding:20px;background:white;border:1px solid var(--line);border-radius:10px}.chart svg{display:block;width:100%;height:auto}.chart-tools{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.chart-tools label{font-size:13px;color:var(--muted)}.chart-info{min-height:22px;font-size:13px;color:var(--muted);font-variant-numeric:tabular-nums}.footnote{font-size:12px;margin-top:15px;max-width:850px}.hidden{display:none!important}footer{margin-top:30px;border-top:1px solid var(--line);padding-top:16px;color:var(--muted);font-size:12px}.legend{display:flex;gap:18px;font-size:12px}.legend span{display:flex;gap:6px;align-items:center}@media(max-width:650px){main{padding:24px 16px 40px}.uploads{grid-template-columns:1fr}.card{padding:18px}header{align-items:flex-start}.section-top{align-items:flex-start}th,td{padding:10px 12px}.badge{white-space:nowrap;font-size:11px}.chart{padding:12px}}
</style>
<main>
<header><div class="brand"><span class="mark" aria-hidden="true"></span>Horizon</div><span class="badge">Offline · files stay on your device</span></header>
<h1>Compare your runs.</h1>
<p class="intro">Choose two Horizon benchmark JSON files from your instance’s <strong>horizon-benchmarks</strong> folder. Add each matching CSV to see frame spikes. Compare frames with frames, or server ticks with server ticks.</p>
<div class="uploads">
<section class="card" aria-labelledby="baselineTitle"><div class="card-top"><span class="dot" aria-hidden="true"></span><h2 id="baselineTitle">Baseline</h2></div><p>Vanilla or your previous configuration.</p><input id="baselineFiles" type="file" multiple accept=".json,.csv" aria-label="Choose baseline JSON and optional CSV"><div id="baselineStatus" class="status" role="status">No report selected.</div><dl id="baselineDetails" class="details"></dl></section>
<section class="card candidate" aria-labelledby="candidateTitle"><div class="card-top"><span class="dot" aria-hidden="true"></span><h2 id="candidateTitle">Candidate</h2></div><p>The configuration you want to evaluate.</p><input id="candidateFiles" type="file" multiple accept=".json,.csv" aria-label="Choose candidate JSON and optional CSV"><div id="candidateStatus" class="status" role="status">No report selected.</div><dl id="candidateDetails" class="details"></dl></section>
</div>
<div id="empty" class="empty"><strong>Your measurements go here.</strong><p>Record with F8 in the game. Keep the world, route, graphics settings and warm-up consistent.</p></div>
<p id="comparisonError" class="error hidden" role="alert"></p>
<section id="results" class="section hidden">
<div class="section-top"><h2 id="resultsTitle">Frame comparison</h2><button id="export" type="button">Save comparison</button></div>
<div id="notices" class="notice"></div>
<div class="metrics"><table aria-label="Benchmark comparison"><thead><tr><th>Measurement</th><th>Baseline</th><th>Candidate</th><th>Change</th></tr></thead><tbody id="metricRows"></tbody></table></div>
<p class="footnote">Changes are candidate versus baseline. Higher FPS is better; lower frame/tick duration, memory and CPU use is usually better. Check workload differences before attributing any change to an optimization.</p>
<section id="chartSection" class="section hidden"><div class="chart"><div class="chart-tools"><h2 id="chartTitle">Frame pacing</h2><label><input id="logScale" type="checkbox" checked> Log scale</label></div><div class="legend"><span><i class="dot"></i>Baseline</span><span><i class="dot" style="background:var(--candidate)"></i>Candidate</span></div><svg id="timeline" viewBox="0 0 960 300" role="img" aria-label="Recorded duration timeline"></svg><div id="chartInfo" class="chart-info">Move over the chart to inspect a time bucket.</div><p id="chartNote" class="footnote"></p></div></section>
<p id="csvHint" class="footnote"></p>
</section>
<footer>No installation, account or network access is needed. Repeat at least three matched runs and test fresh terrain separately from a warm cache. Heap/nonheap exclude native allocations. Process RSS is not a complete GPU/unified-memory measurement.</footer>
</main>
<script>
'use strict';
const SETTINGS=['minecraft','kind','os','os_version','architecture','gpu','graphics_backend','graphics_driver','server_runtime_kind','client_graphics_capture_status','java_version','heap_max_bytes','framebuffer_width','framebuffer_height','render_distance','simulation_distance','vsync','fps_limit','graphics_preset','ambient_occlusion','improved_transparency','mipmap_levels','entity_distance_scaling','entity_shadows','particles','clouds','cloud_range','fov','world_difficulty','dh_capture_status','dh_rendering_enabled','dh_renderer_mode','dh_lod_radius_chunks','dh_horizontal_quality','dh_vertical_quality','dh_max_horizontal_resolution','dh_transparency','dh_lod_shading','dh_lod_only_mode','dh_camera_quality_dropoff','dh_ambient_occlusion','iris_capture_status','iris_shaders_enabled','iris_shader_pack'];
const state={baseline:null,candidate:null},generation={baseline:0,candidate:0};
let comparison=null,chartData=null;
const $=id=>document.getElementById(id);
const number=value=>typeof value==='number'&&Number.isFinite(value);
const text=(tag,value)=>{const element=document.createElement(tag);element.textContent=String(value);return element;};
const format=value=>number(value)?new Intl.NumberFormat(undefined,{maximumFractionDigits:2}).format(value):'—';
function validateReport(report){
if(!report||report.schema!==1||!['frames','ticks'].includes(report.kind))throw Error('Choose a Horizon frame or server-tick JSON report.');
if(!report.stats||!Number.isInteger(report.stats.count)||report.stats.count<1||report.stats.count>300000)throw Error('This report has no valid sample count. Record focused gameplay with F8 first.');
for(const key of ['meanRate','lowOnePercentRate','meanMs','p50Ms','p95Ms','p99Ms','maxMs']){
if(!number(report.stats[key])||report.stats[key]<0)throw Error('This report has missing or invalid timing measurements.');
}
if(!number(report.elapsed_seconds)||report.elapsed_seconds<=0)throw Error('This report has an invalid recording duration.');
return report;
}
function parseCsv(source,count){
const lines=source.trim().split(/\r?\n/);
if(lines.shift()!=='sample,duration_ns')throw Error('CSV header does not match a Horizon recording.');
if(lines.length!==count||lines.length>300000)throw Error('CSV sample count does not match this JSON report.');
return lines.map((line,index)=>{
const fields=line.split(','),sample=Number(fields[0]),duration=Number(fields[1]);
if(fields.length!==2||sample!==index||!Number.isSafeInteger(duration)||duration<=0)throw Error('CSV contains an invalid sample.');
return duration/1e6;
});
}
function softwareWarnings(a,b){
const warnings=[];
for(const key of ['java_vendor','java_vm_name','java_runtime_version']){
if(![a,b].every(r=>typeof r[key]==='string'&&r[key].length))warnings.push(key+' was not captured in both reports; Java implementation equivalence is unknown.');
else if(a[key]!==b[key])warnings.push('Software '+key+' differs. Treat it as an explicit experiment variable.');
}
const mods=[a,b].map(r=>Array.isArray(r.mods)&&r.mods.every(entry=>typeof entry==='string'&&entry.length)?new Set(r.mods):null);
if(mods.some(entries=>entries===null))warnings.push('Loaded mod versions were not captured in both reports; software equivalence is unknown.');
else{
const only=[...mods[0]].filter(entry=>!mods[1].has(entry)).sort(),other=[...mods[1]].filter(entry=>!mods[0].has(entry)).sort();
if(only.length||other.length){
warnings.push('Loaded mod versions differ. Treat added, removed or upgraded mods as explicit experiment variables.');
for(const [label,entries] of [['baseline',only],['candidate',other]])if(entries.length)warnings.push('Only '+label+': '+entries.slice(0,12).join(', ')+(entries.length>12?'; '+(entries.length-12)+' more':'')+'.');
}
}
return warnings;
}
const TICK_STATE_FIELDS=['configured_tick_rate','frozen','stepping','sprinting','runs_game_elements'];
function tickStateAvailable(r){
if(r.tick_state_capture_status!=='sampled_server_ticks'||!Number.isSafeInteger(r.tick_state_samples)||r.tick_state_samples<=0)return false;
for(const boundary of ['tick_state_first','tick_state_last']){
const s=r[boundary];
if(!s||!number(s.configured_tick_rate)||s.configured_tick_rate<1||TICK_STATE_FIELDS.slice(1).some(key=>typeof s[key]!=='boolean'))return false;
}
if(!Array.isArray(r.tick_state_changed)||r.tick_state_changed.some(key=>!TICK_STATE_FIELDS.includes(key)))return false;
return ['frozen','stepping','sprinting','suppressed','nonstandard_rate'].every(key=>{
const v=r['tick_state_'+key+'_samples'];return Number.isSafeInteger(v)&&v>=0&&v<=r.tick_state_samples;
});
}
function compareTickStates(a,b){
const differences=[],warnings=[],available=[tickStateAvailable(a),tickStateAvailable(b)];
for(const [index,label,r] of [[0,'Baseline',a],[1,'Candidate',b]]){
if(!available[index]){warnings.push(label+' tick-manager state was not captured or is invalid; simulation workload equivalence is unknown.');continue;}
if(r.tick_state_changed.length){
differences.push({setting:label+' tick-manager state changed during recording',baseline:label==='Baseline'?r.tick_state_changed:null,candidate:label==='Candidate'?r.tick_state_changed:null});
warnings.push(label+' sampled tick-manager changes: '+r.tick_state_changed.join(', ')+'.');
}
const abnormal=['frozen','stepping','sprinting','suppressed','nonstandard_rate'].filter(key=>r['tick_state_'+key+'_samples']>0);
if(abnormal.length)warnings.push(label+' includes tick-manager samples with '+abnormal.join(', ')+'; interpret execution times as this workload.');
}
if(available.every(Boolean))for(const boundary of ['tick_state_first','tick_state_last'])for(const key of TICK_STATE_FIELDS){
if(a[boundary][key]!==b[boundary][key])differences.push({setting:boundary+'.'+key,baseline:a[boundary][key],candidate:b[boundary][key]});
}
warnings.push('Tick-manager state is sampled after measured server invocations. Changes entirely between samples, paused intervals and completed world work are not verified.');
return{differences,warnings};
}
function serverContextWarnings(a,b){
const warnings=[];
for(const [label,r] of [['Baseline',a],['Candidate',b]]){
if(!['integrated','dedicated'].includes(r.server_runtime_kind))warnings.push(label+' server runtime kind was not captured; integrated/dedicated equivalence is unknown.');
if(r.server_runtime_kind==='integrated'&&!(r.client_graphics_capture_status==='captured_at_recording_start'&&['gpu','graphics_backend','graphics_driver'].every(key=>typeof r[key]==='string'&&r[key].trim().length)))warnings.push(label+' integrated-server client graphics were not captured; shared rendering workload is unknown.');
}
warnings.push('Integrated-server graphics context is a recording-start snapshot; later graphics changes and full client workload equivalence are not verified.');
return warnings;
}
function compareReports(a,b){
if(a.kind!==b.kind)throw Error('Compare frame runs with frame runs, or tick runs with tick runs.');
const differences=SETTINGS.filter(key=>JSON.stringify(a[key])!==JSON.stringify(b[key])).map(key=>({setting:key,baseline:a[key]??null,candidate:b[key]??null}));
if(a.kind==='ticks'&&a.peak_players!==b.peak_players)differences.push({setting:'peak_players',baseline:a.peak_players??null,candidate:b.peak_players??null});
const warnings=['World, camera route, shaders and terrain-cache warm-up still need to match. These are not verified by the captured settings.'];
warnings.push(...softwareWarnings(a,b));
if(a.kind==='ticks'){
warnings.push(...serverContextWarnings(a,b));
const tick=compareTickStates(a,b);differences.push(...tick.differences);warnings.push(...tick.warnings);
}
for(const key of ['compact_object_headers','garbage_collectors']){
if(!(key in a)||!(key in b))warnings.push(key+' was not captured in both reports; JVM configuration equivalence is unknown.');
else if(JSON.stringify(a[key])!==JSON.stringify(b[key]))warnings.push('JVM '+key+' differs. Treat it as an explicit experiment variable.');
}
if(a.kind==='frames'){
for(const key of ['adaptive_budget_enabled','adaptive_target_fps','precise_worker_pacing','debounce_queue_wait','wakeable_idle_wait','dispatch_retry_backoff','tick_membership_dedup','empty_culling_whitelist','culling_ray_scratch','terrain_position_arithmetic','terrain_quad_reuse','interruptible_terrain_read','dh_thread_count','dh_runtime_ratio']){
if(key in a&&key in b&&JSON.stringify(a[key])!==JSON.stringify(b[key]))warnings.push('Experiment '+key+' differs.');
}
for(const [label,r] of [['Baseline',a],['Candidate',b]]){
if(!['available','not_installed'].includes(r.dh_capture_status))warnings.push(label+' distant-terrain configuration was not captured; quality equivalence is unknown.');
if(!['available','not_installed'].includes(r.iris_capture_status))warnings.push(label+' Iris state was not captured; shader equivalence is unknown.');
if(Array.isArray(r.visual_settings_changed)&&r.visual_settings_changed.length){
differences.push({setting:label+' visual settings changed during recording',baseline:label==='Baseline'?r.visual_settings_changed:null,candidate:label==='Candidate'?r.visual_settings_changed:null});
warnings.push(label+' visual settings changed between capture boundaries: '+r.visual_settings_changed.join(', ')+'.');
}
}
}
if(Math.abs(a.elapsed_seconds-b.elapsed_seconds)>Math.max(2,Math.min(a.elapsed_seconds,b.elapsed_seconds)*.1))warnings.push('Recording durations differ. Match durations when comparing memory peaks and collection totals.');
for(const [label,r] of [['Baseline',a],['Candidate',b]]){
if(r.elapsed_seconds<60)warnings.push(label+' recording is shorter than 60 seconds. Use longer repeated runs.');
if(r.dropped_samples)warnings.push(label+' exceeded the recording buffer; '+r.dropped_samples+' samples were dropped.');
if(r.kind==='frames'&&r.excluded_frames)warnings.push(label+' excluded '+r.excluded_frames+' menu, paused or unfocused frame starts. Process CPU still spans the whole recording.');
}
const rows=[];
const add=(label,key,unit,scale=1,stats=false)=>{
const av=stats?a.stats[key]:a[key],bv=stats?b.stats[key]:b[key];
if(number(av)&&number(bv))rows.push({label,key,unit,baseline:av/scale,candidate:bv/scale,change_percent:av?100*(bv/av-1):null});
};
if(a.kind==='frames'){
add('Average FPS','meanRate','FPS',1,true);add('Slowest 1% average','lowOnePercentRate','FPS',1,true);
}else{
add('Average tick duration','meanMs','ms',1,true);add('Observed tick rate','observed_ticks_per_second','TPS');
warnings.push('Tick durations exclude scheduler sleep. Observed TPS counts measured invocations, including frozen ticks, and includes recording time spent paused; tick execution capacity is not TPS.');
}
add('95th percentile duration','p95Ms','ms',1,true);add('99th percentile duration','p99Ms','ms',1,true);add('Longest duration','maxMs','ms',1,true);
add('Sampled peak process RSS','process_rss_peak_sampled_bytes','MiB',1048576);add('Last sampled process RSS','process_rss_last_sampled_bytes','MiB',1048576);
add('Sampled peak JVM heap','heap_used_peak_sampled_bytes','MiB',1048576);add('JVM heap at end','heap_used_end_bytes','MiB',1048576);
add('Sampled peak nonheap','nonheap_used_peak_sampled_bytes','MiB',1048576);add('Average process CPU','process_cpu_mean_cores','cores');
if(number(a.gc_time_ms)&&number(b.gc_time_ms)){
const av=a.gc_time_ms/a.elapsed_seconds,bv=b.gc_time_ms/b.elapsed_seconds;
rows.push({label:'GC collection time / second',key:'gc_collection_ms_per_second',unit:'ms/s',baseline:av,candidate:bv,change_percent:av?100*(bv/av-1):null});
}
warnings.push('Memory peaks are sampled and can miss short spikes. RSS includes resident JVM and native pages, but is not a complete GPU/unified-memory measurement. Heap/nonheap measure JVM pools only. CPU includes the whole process and integrated server.');
warnings.push('GC collection time comes from collector MXBean counters. It may include concurrent cycles and overlapping collector phases; it is not a measured stop-the-world pause total. Use JFR pause events for that comparison.');
for(const [label,r] of [['Baseline',a],['Candidate',b]])if(number(r.process_rss_last_sample_age_ms)&&r.process_rss_last_sample_age_ms>2500)warnings.push(label+' resident-memory sample is '+format(r.process_rss_last_sample_age_ms/1000)+' seconds old. The background reader may be slow or unavailable.');
return{kind:a.kind,differences,warnings,metrics:rows};
}
function renderDetails(side){
const entry=state[side],target=$(side+'Details');target.replaceChildren();
if(!entry)return;
const r=entry.report,fields=[['Profile',r.profile||r.label||'Unlabeled'],['Recording',format(r.elapsed_seconds)+' s · '+format(r.stats.count)+' samples'],['Platform',[r.os,r.architecture].filter(Boolean).join(' ')||'Not captured']];
if(r.kind==='frames')fields.push(['Graphics',[r.gpu,r.graphics_backend].filter(Boolean).join(' · ')||'Not captured'],['Window',number(r.framebuffer_width)?r.framebuffer_width+' × '+r.framebuffer_height:'Not captured'],['Experiments','Adaptive: '+(r.adaptive_budget_enabled?'on':'off')+' · precise pacing: '+(r.precise_worker_pacing?'on':'off')+' · queue wait: '+(r.debounce_queue_wait?'on':'off')]);
fields.push(['JVM', 'Compact headers: '+(r.compact_object_headers===true?'on':r.compact_object_headers===false?'off':'not captured')+' · GC: '+(Array.isArray(r.garbage_collectors)?r.garbage_collectors.join(', '):'not captured')]);
fields.push(['Java',[r.java_vendor,r.java_vm_name,r.java_runtime_version||r.java_version].filter(Boolean).join(' · ')||'Not captured']);
if(r.kind==='ticks'){
fields.push(['Server runtime',['integrated','dedicated'].includes(r.server_runtime_kind)?r.server_runtime_kind:'Not captured']);
if(r.server_runtime_kind==='integrated')fields.push(['Shared client graphics',r.client_graphics_capture_status==='captured_at_recording_start'?[r.gpu,r.graphics_backend,r.graphics_driver].filter(Boolean).join(' · ')+' (at recording start)':'Not captured']);
if(tickStateAvailable(r)){
const first=r.tick_state_first,last=r.tick_state_last;
fields.push(['Configured ticks',format(first.configured_tick_rate)+' / s at first sample · '+format(last.configured_tick_rate)+' / s at last sample']);
fields.push(['Tick-manager samples',r.tick_state_samples+' · frozen '+r.tick_state_frozen_samples+' · stepping '+r.tick_state_stepping_samples+' · sprinting '+r.tick_state_sprinting_samples+' · suppressed '+r.tick_state_suppressed_samples]);
fields.push(['Tick-manager changes',r.tick_state_changed.length?r.tick_state_changed.join(', '):'None observed']);
}else fields.push(['Tick-manager state','Not captured or invalid']);
}
if(r.kind==='frames'){
fields.push(['Distant terrain',r.dh_capture_status==='not_installed'?'Not installed':r.dh_capture_status==='available'?(r.dh_rendering_enabled?'Enabled':'Disabled')+' · '+r.dh_lod_radius_chunks+' chunk radius · '+r.dh_horizontal_quality+'/'+r.dh_vertical_quality+' quality · '+r.dh_max_horizontal_resolution+' resolution':'Not captured']);
if(number(r.dh_thread_count))fields.push(['Terrain workers',r.dh_thread_count+' · runtime ratio at start '+format(r.dh_runtime_ratio)+(number(r.visual_settings_end?.dh_runtime_ratio)?' / end '+format(r.visual_settings_end.dh_runtime_ratio):'')]);
fields.push(['Iris shaders',r.iris_capture_status==='not_installed'?'Not installed':r.iris_capture_status==='available'?r.iris_shaders_enabled?'Enabled · '+(r.iris_shader_pack||'pack name unavailable'):'Disabled':'Not captured']);
if(number(r.fov))fields.push(['Field of view',format(r.fov)]);
if(r.world_difficulty)fields.push(['World difficulty',r.world_difficulty]);
fields.push(['Frame limit',r.fps_limit_unlimited===true||(r.minecraft==='26.3'&&number(r.fps_limit)&&r.fps_limit>=260)?'Uncapped':number(r.fps_limit)?r.fps_limit+' FPS':'not captured']);
if(r.adaptive_budget_enabled&&number(r.adaptive_target_fps))fields.push(['Adaptive frame goal',r.adaptive_target_fps+' FPS (not a cap)']);
}
fields.push(['Rejected dispatch retry', r.dispatch_retry_backoff===true?'on':r.dispatch_retry_backoff===false?'off':'not captured']);
fields.push(['Wakeable queue idle', r.wakeable_idle_wait===true?'on':r.wakeable_idle_wait===false?'off':'not captured']);
if(r.kind==='frames')fields.push(['Tick membership deduplication',r.tick_membership_dedup===true?'on':r.tick_membership_dedup===false?'off':'not captured']);
if(r.kind==='frames')fields.push(['Terrain quad reuse',r.terrain_quad_reuse===true?'on':r.terrain_quad_reuse===false?'off':'not captured']);
if(r.kind==='frames')fields.push(['Terrain position arithmetic',r.terrain_position_arithmetic===true?'on':r.terrain_position_arithmetic===false?'off':'not captured']);
if(r.kind==='frames')fields.push(['Culling ray scratch',r.culling_ray_scratch===true?'on':r.culling_ray_scratch===false?'off':'not captured']);
if(r.kind==='frames')fields.push(['Empty culling whitelist',r.empty_culling_whitelist===true?'on':r.empty_culling_whitelist===false?'off':'not captured']);
if(r.kind==='frames')fields.push(['Interruptible terrain read',r.interruptible_terrain_read===true?'on':r.interruptible_terrain_read===false?'off':'not captured']);
for(const [label,value]of fields)target.append(text('dt',label),text('dd',value));
if(Array.isArray(r.mods)&&r.mods.every(entry=>typeof entry==='string'&&entry.length)){
const versions=[...new Set(r.mods)].sort(),list=text('details','');
list.append(text('summary',versions.length+' loaded mods'),text('div',versions.join('\n')));
list.lastChild.style.whiteSpace='pre-wrap';
list.lastChild.style.maxHeight='16rem';list.lastChild.style.overflow='auto';
const value=text('dd','');value.append(list);target.append(text('dt','Software'),value);
}else target.append(text('dt','Software'),text('dd','Loaded mod versions not captured'));
}
async function loadFiles(side,files){
const token=++generation[side];state[side]=null;render();
const status=$(side+'Status');status.classList.remove('error');status.textContent='Reading files…';
try{
const list=Array.from(files),jsonFiles=list.filter(f=>f.name.toLowerCase().endsWith('.json')),csvFiles=list.filter(f=>f.name.toLowerCase().endsWith('.csv'));
if(list.length<1||list.length>2||jsonFiles.length!==1||csvFiles.length>1)throw Error('Choose one JSON report and, optionally, its matching CSV.');
if(jsonFiles[0].size>2*1024*1024||csvFiles.some(f=>f.size>16*1024*1024))throw Error('File is too large for a Horizon recording.');
const report=validateReport(JSON.parse(await jsonFiles[0].text()));
let samples=null,csvWarning=null;
if(csvFiles.length){
try{
if(csvFiles[0].name!==report.samples_file)throw Error('CSV filename does not match this report’s samples file.');
samples=parseCsv(await csvFiles[0].text(),report.stats.count);
const mean=samples.reduce((sum,value)=>sum+value,0)/samples.length;
if(Math.abs(mean-report.stats.meanMs)>Math.max(1e-6,report.stats.meanMs*1e-6)){
samples=null;throw Error('CSV durations do not match the timing summary in this JSON.');
}
}catch(error){csvWarning=error.message;}
}
if(token!==generation[side])return;
state[side]={report,samples,csvWarning,filename:jsonFiles[0].name};
status.textContent=jsonFiles[0].name+(samples?' · CSV loaded':'');
}catch(error){
if(token!==generation[side])return;
status.textContent=error instanceof SyntaxError?'This file is not valid JSON.':error.message;status.classList.add('error');
}
render();
}
function render(){
comparison=null;renderDetails('baseline');renderDetails('candidate');
$('results').classList.add('hidden');$('comparisonError').classList.add('hidden');$('empty').classList.toggle('hidden',!!state.baseline&&!!state.candidate);
if(!state.baseline||!state.candidate)return;
try{comparison=compareReports(state.baseline.report,state.candidate.report);}catch(error){$('comparisonError').textContent=error.message;$('comparisonError').classList.remove('hidden');return;}
$('results').classList.remove('hidden');$('resultsTitle').textContent=comparison.kind==='frames'?'Frame comparison':'Server tick comparison';
const notice=$('notices');notice.replaceChildren(text('strong',comparison.differences.length?'Captured settings differ — check the workload.':'Captured settings match. Check the route and warm-up too.'));
if(comparison.differences.length){const list=document.createElement('ul');for(const d of comparison.differences)list.append(text('li',d.setting+': '+JSON.stringify(d.baseline)+' → '+JSON.stringify(d.candidate)));notice.append(list);}
for(const warning of comparison.warnings)notice.append(text('p',warning));
const rows=$('metricRows');rows.replaceChildren();
for(const metric of comparison.metrics){const row=document.createElement('tr');row.append(text('td',metric.label),text('td',format(metric.baseline)+' '+metric.unit),text('td',format(metric.candidate)+' '+metric.unit),text('td',metric.change_percent===null?'—':(metric.change_percent>=0?'+':'')+format(metric.change_percent)+'%'));rows.append(row);}
const hints=[];for(const side of ['baseline','candidate'])if(state[side].csvWarning)hints.push((side==='baseline'?'Baseline: ':'Candidate: ')+state[side].csvWarning);
if(!state.baseline.samples||!state.candidate.samples)hints.push('Add each matching CSV alongside its JSON to draw both duration timelines.');
$('csvHint').textContent=hints.join(' ');drawChart();
}
function bucketSamples(samples,ticks){
const total=ticks?samples.length:samples.reduce((a,b)=>a+b,0)/1000,bucketSeconds=Math.max(total/1000,ticks?1:.001),buckets=[];
let time=0;
for(const duration of samples){time+=ticks?1:duration/1000;const index=Math.floor(time/bucketSeconds),last=buckets[buckets.length-1];if(last&&last.index===index)last.duration=Math.max(last.duration,duration);else buckets.push({index,time,duration});}
return{total,buckets};
}
function svgElement(tag,attributes){const element=document.createElementNS('http://www.w3.org/2000/svg',tag);for(const [key,value]of Object.entries(attributes))element.setAttribute(key,String(value));return element;}
function drawChart(){
const svg=$('timeline');svg.replaceChildren();chartData=null;
$('chartInfo').textContent='Move over the chart to inspect a bucket.';
const available=!!comparison&&!!state.baseline?.samples&&!!state.candidate?.samples;$('chartSection').classList.toggle('hidden',!available);if(!available)return;
const ticks=comparison.kind==='ticks';
$('chartTitle').textContent=ticks?'Tick durations by sample':'Frame pacing';
$('chartNote').textContent='Each point preserves the longest duration in its bucket. '+(ticks?'Ticks are placed by sample number. Scheduler sleep and pauses are not represented as durations.':'Frames use recorded active time; menus and unfocused intervals are omitted. This is not GPU execution time.');
const a=bucketSamples(state.baseline.samples,ticks),b=bucketSamples(state.candidate.samples,ticks),maxTime=Math.max(a.total,b.total,.001),maxMs=Math.max(...a.buckets.map(p=>p.duration),...b.buckets.map(p=>p.duration),1);
const width=Math.max(320,Math.round(svg.getBoundingClientRect().width));svg.setAttribute('viewBox','0 0 '+width+' 300');
const left=64,right=width-18,top=20,bottom=250,log=$('logScale').checked,transform=x=>log?Math.log1p(x):x;
const x=t=>left+t/maxTime*(right-left),y=ms=>bottom-transform(ms)/transform(maxMs)*(bottom-top);
for(const ms of [0,8.33,16.67,33.33,50,100,250,500,1000,2000,5000,10000,30000,60000]){
if(ms>maxMs)continue;
const line=svgElement('line',{x1:left,x2:right,y1:y(ms),y2:y(ms),stroke:'#e5eae3'});svg.append(line);
const label=svgElement('text',{x:left-8,y:y(ms)+4,'text-anchor':'end',fill:'#65726a','font-size':11});label.textContent=format(ms)+' ms';svg.append(label);
}
const divisions=width<500?3:5;
for(let i=0;i<=divisions;i++){const t=maxTime*i/divisions,label=svgElement('text',{x:x(t),y:bottom+24,'text-anchor':i===0?'start':i===divisions?'end':'middle',fill:'#65726a','font-size':11});label.textContent=ticks?'Tick '+format(Math.round(t)):format(t)+' s';svg.append(label);}
for(const [data,color]of [[a,'#247854'],[b,'#bc642f']])svg.append(svgElement('path',{d:data.buckets.map((point,index)=>(index?'L':'M')+x(point.time).toFixed(2)+' '+y(point.duration).toFixed(2)).join(' '),fill:'none',stroke:color,'stroke-width':1.7,'vector-effect':'non-scaling-stroke'}));
const cursor=svgElement('line',{x1:0,x2:0,y1:top,y2:bottom,stroke:'#192920','stroke-dasharray':'3 3',visibility:'hidden'});svg.append(cursor);chartData={a,b,maxTime,left,right,cursor,ticks,width};
}
$('timeline').addEventListener('pointermove',event=>{
if(!chartData)return;const rect=$('timeline').getBoundingClientRect(),px=(event.clientX-rect.left)/rect.width*chartData.width,t=Math.max(0,Math.min(chartData.maxTime,(px-chartData.left)/(chartData.right-chartData.left)*chartData.maxTime));
chartData.cursor.setAttribute('x1',px);chartData.cursor.setAttribute('x2',px);chartData.cursor.setAttribute('visibility',px>=chartData.left&&px<=chartData.right?'visible':'hidden');
const nearest=data=>{if(t>data.total)return'no samples';let point=data.buckets[0];for(const p of data.buckets){if(Math.abs(p.time-t)<Math.abs(point.time-t))point=p;}return format(point.duration)+' ms';};
$('chartInfo').textContent=(chartData.ticks?'Tick '+format(Math.round(t)):format(t)+' s')+' · baseline '+nearest(chartData.a)+' · candidate '+nearest(chartData.b);
});
$('timeline').addEventListener('pointerleave',()=>{if(chartData)chartData.cursor.setAttribute('visibility','hidden');});
$('logScale').addEventListener('change',drawChart);
window.addEventListener('resize',drawChart);
for(const side of ['baseline','candidate'])$(side+'Files').addEventListener('change',event=>loadFiles(side,event.target.files));
$('export').addEventListener('click',()=>{
if(!comparison)return;const summary={horizon_comparison:1,source_files:{baseline:state.baseline.filename,candidate:state.candidate.filename},...comparison},blob=new Blob([JSON.stringify(summary,null,2)+'\n'],{type:'application/json'}),url=URL.createObjectURL(blob),link=document.createElement('a');link.href=url;link.download='horizon-comparison.json';link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
});
</script>
</html>
+10
View File
@@ -0,0 +1,10 @@
# Java comparison dependencies
These locks support `tools/perf_bench.py` (vanilla Fabric and performance-mod comparisons)
and `tools/server_bench.py` (Fabric + Lithium). `tools/runtime.py` downloads and verifies
the pinned Fabric libraries. Game files still come from the user's own Minecraft install.
`mods.lock.json` pins Sodium, Lithium, FerriteCore, ImmediatelyFast and Fabric API with
publisher URLs and hashes. `fabric.lock.json` pins the loader and its libraries. These
are developer benchmark inputs, not distributable Horizon packs. The former Horizon
Java optimization mod and its pack profiles are retired; see `docs/java-packs.md`.
File renamed without changes.
+134
View File
@@ -0,0 +1,134 @@
{
"format": 1,
"minecraft": "26.3",
"java_major": 25,
"fabric_loader": "0.19.5",
"projects": {
"sodium": {
"slug": "sodium",
"project_id": "AANobbMI",
"version_id": "bAZQdGpg",
"version": "mc26.3-0.9.2-fabric",
"game_versions": [
"26.3"
],
"loaders": [
"fabric"
],
"license": "LicenseRef-Polyform-Shield-1.0.0",
"source": "https://github.com/CaffeineMC/sodium",
"url": "https://cdn.modrinth.com/data/AANobbMI/versions/bAZQdGpg/sodium-fabric-0.9.2%2Bmc26.3.jar",
"filename": "sodium-fabric-0.9.2+mc26.3.jar",
"size": 1908068,
"hashes": {
"sha1": "9acfe851e36f4fb27d7c5baa224e8bd4e0334283",
"sha512": "f3f260f204b8ce5e8777c2c73f537956755ec61f3817b0b78b5e5568e8aa40f14891f146475075109747665c7c88a45ca0ca9a8df3c68d79c58ecdaa8d8c4b93"
},
"dependencies": [],
"client_side": "required",
"server_side": "unsupported"
},
"lithium": {
"slug": "lithium",
"project_id": "gvQqBUqZ",
"version_id": "xS0Q8LSi",
"version": "mc26.3-0.26.2-fabric",
"game_versions": [
"26.3"
],
"loaders": [
"fabric",
"quilt"
],
"license": "LGPL-3.0-only",
"source": "https://github.com/caffeinemc/lithium-fabric",
"url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/xS0Q8LSi/lithium-fabric-0.26.2%2Bmc26.3.jar",
"filename": "lithium-fabric-0.26.2+mc26.3.jar",
"size": 914543,
"hashes": {
"sha512": "4d7fee66132eedc71feab9390b92c95d7058edbdad0fecfac1d836a2950b97a7ca463afbede61c7ef361ce65e1f927ab9e89dde5bf2e0e0ce486afb5c5dbee40",
"sha1": "dd5a4ec2c68d7601d8bce9076f789c55a5f8bef3"
},
"dependencies": [],
"client_side": "optional",
"server_side": "optional"
},
"ferrite-core": {
"slug": "ferrite-core",
"project_id": "uXXizFIs",
"version_id": "d5ddUdiB",
"version": "9.0.0-fabric",
"game_versions": [
"26.1",
"26.1.1",
"26.1.2",
"26.2",
"26.3"
],
"loaders": [
"fabric"
],
"license": "MIT",
"source": "https://github.com/malte0811/FerriteCore",
"url": "https://cdn.modrinth.com/data/uXXizFIs/versions/d5ddUdiB/ferritecore-9.0.0-fabric.jar",
"filename": "ferritecore-9.0.0-fabric.jar",
"size": 72677,
"hashes": {
"sha1": "eac76ff0f3753422c61b2c44487d0d195d88d4bc",
"sha512": "d81fa97e11784c19d42f89c2f433831d007603dd7193cee45fa177e4a6a9c52b384b198586e04a0f7f63cd996fed713322578bde9a8db57e1188854ae5cbe584"
},
"dependencies": [],
"client_side": "optional",
"server_side": "optional"
},
"fabric-api": {
"slug": "fabric-api",
"project_id": "P7dR8mSH",
"version_id": "bNnaTiuM",
"version": "0.161.0+26.3",
"game_versions": [
"26.3"
],
"loaders": [
"fabric"
],
"license": "Apache-2.0",
"source": "https://github.com/FabricMC/fabric",
"url": "https://cdn.modrinth.com/data/P7dR8mSH/versions/bNnaTiuM/fabric-api-0.161.0%2B26.3.jar",
"filename": "fabric-api-0.161.0+26.3.jar",
"size": 2620967,
"hashes": {
"sha1": "53f9ee02c3702734d90520aa8c9719bf0e1c8d4f",
"sha512": "ed6b2586d6fde11fde8472f5a527c51e99b67026e46f94d4bfd85e7e28ce5ee299173ee16ad576ceb51f39f98d30a811086a6deb1a86a524859cc16e12da109d"
},
"dependencies": [],
"client_side": "optional",
"server_side": "optional"
},
"immediatelyfast": {
"slug": "immediatelyfast",
"project_id": "5ZwdcRci",
"version_id": "3MP9UR23",
"version": "1.17.1+26.3-fabric",
"game_versions": [
"26.3"
],
"loaders": [
"fabric",
"quilt"
],
"license": "LGPL-3.0-or-later",
"source": "https://github.com/RaphiMC/ImmediatelyFast",
"url": "https://cdn.modrinth.com/data/5ZwdcRci/versions/3MP9UR23/ImmediatelyFast-Fabric-1.17.1%2B26.3.jar",
"filename": "ImmediatelyFast-Fabric-1.17.1+26.3.jar",
"size": 99586,
"hashes": {
"sha512": "8e139589fd995bce3d485a34d92a07243964212f7ed2e4fc2f6de5b9a74d0f59b03252e7ce40c64e006cf3e21bed5de27b830d33ef048b286e041f6178ad8428",
"sha1": "f565bbeb201f2305e81761429d3f1f47e1c3a321"
},
"dependencies": [],
"client_side": "required",
"server_side": "unsupported"
}
}
}
-268
View File
@@ -1,268 +0,0 @@
#!/usr/bin/env python3
"""Compile only original unobfuscated-26.3 mod sources; never bundle game classes."""
import argparse
import json
import os
import platform
from pathlib import Path
import shutil
import subprocess
import zipfile
try:
from . import lab, runtime
except ImportError:
import lab
import runtime
ROOT = lab.ROOT
SOURCE = ROOT / "mods/horizon/src"
BUILD = ROOT / ".local/mod-build"
def test_metrics():
destination = BUILD / "metric-tests"
destination.mkdir(parents=True, exist_ok=True)
sources = [SOURCE / "main/java/dev/horizonlab/metrics" / name
for name in ["SampleStats.java", "Recording.java", "ProcessMemoryMonitor.java", "TickState.java", "ServerContext.java"]]
sources.append(SOURCE / "test/java/dev/horizonlab/metrics/MetricsTest.java")
subprocess.run(["javac", "--release", "25", "-d", str(destination), *map(str, sources)], check=True)
subprocess.run(["java", "-cp", str(destination), "dev.horizonlab.metrics.MetricsTest"], check=True)
subprocess.run(["java", "-XX:+UseCompactObjectHeaders", "-cp", str(destination),
"dev.horizonlab.metrics.MetricsTest"], check=True)
subprocess.run(["java", "-Xms32M", "-Xmx256M", "-XX:+UseZGC", "-XX:+UseCompactObjectHeaders", "-cp", str(destination),
"dev.horizonlab.metrics.MetricsTest"], check=True)
budget_sources = [SOURCE / "main/java/dev/horizonlab/budget/TerrainBudget.java",
SOURCE / "test/java/dev/horizonlab/budget/TerrainBudgetTest.java"]
subprocess.run(["javac", "--release", "25", "-d", str(destination), *map(str, budget_sources)], check=True)
subprocess.run(["java", "-cp", str(destination), "dev.horizonlab.budget.TerrainBudgetTest"], check=True)
worker_sources = [SOURCE / "main/java/dev/horizonlab/budget/WorkerPacer.java",
SOURCE / "test/java/dev/horizonlab/budget/WorkerPacerTest.java"]
subprocess.run(["javac", "--release", "25", "-d", str(destination), *map(str, worker_sources)], check=True)
subprocess.run(["java", "-cp", str(destination), "dev.horizonlab.budget.WorkerPacerTest"], check=True)
debounce_sources = [SOURCE / "main/java/dev/horizonlab/budget/DebounceWait.java",
SOURCE / "test/java/dev/horizonlab/budget/DebounceWaitTest.java"]
subprocess.run(["javac", "--release", "25", "-d", str(destination), *map(str, debounce_sources)], check=True)
subprocess.run(["java", "-cp", str(destination), "dev.horizonlab.budget.DebounceWaitTest"], check=True)
idle_sources = [SOURCE / "main/java/dev/horizonlab/budget/WakeableIdleWait.java",
SOURCE / "test/java/dev/horizonlab/budget/WakeableIdleWaitTest.java"]
subprocess.run(["javac", "--release", "25", "-d", str(destination), *map(str, idle_sources)], check=True)
subprocess.run(["java", "-cp", str(destination), "dev.horizonlab.budget.WakeableIdleWaitTest"], check=True)
dispatch_sources = [SOURCE / "main/java/dev/horizonlab/budget/DispatchRetry.java",
SOURCE / "test/java/dev/horizonlab/budget/DispatchRetryTest.java"]
subprocess.run(["javac", "--release", "25", "-d", str(destination), *map(str, dispatch_sources)], check=True)
subprocess.run(["java", "-cp", str(destination), "dev.horizonlab.budget.DispatchRetryTest"], check=True)
def build():
lock = json.loads((ROOT / "packs/mods.lock.json").read_text())
_, jars, _ = runtime.prepare_client(assets=False)
with zipfile.ZipFile(jars[0]) as game_archive:
game_version = json.loads(game_archive.read("version.json"))
if game_version["world_version"] != lock["options_data_version"]:
raise ValueError("Pack options data version does not match the verified game JAR")
dh = lock["projects"]["distanthorizons"]
jars.append(runtime.fetch(dh["url"], ROOT / ".local/pack-cache" / dh["filename"],
dh["hashes"]["sha512"], "sha512"))
culling = lock["projects"]["entityculling"]
jars.append(runtime.fetch(culling["url"], ROOT / ".local/pack-cache" / culling["filename"],
culling["hashes"]["sha512"], "sha512"))
api = lock["projects"]["fabric-api"]
api_jar = runtime.fetch(api["url"], ROOT / ".local/pack-cache" / api["filename"],
api["hashes"]["sha512"], "sha512")
nested = BUILD / "fabric-api"
nested.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(api_jar) as archive:
for name in archive.namelist():
if name.startswith("META-INF/jars/") and name.endswith(".jar"):
destination = nested / Path(name).name
destination.write_bytes(archive.read(name))
jars.append(destination)
classes = BUILD / "classes"
if classes.exists():
shutil.rmtree(classes)
classes.mkdir(parents=True)
sources = sorted((SOURCE / "main/java").rglob("*.java"))
command = ["--release", "25", "-proc:none", "-encoding", "UTF-8", "-d", str(classes),
"-classpath", os.pathsep.join(map(str, jars)), *map(str, sources)]
argfile = BUILD / "javac.args"
argfile.write_text("\n".join(json.dumps(arg) for arg in command) + "\n")
subprocess.run(["javac", "@" + str(argfile)], cwd=ROOT, check=True)
adapter_tests = BUILD / "adapter-tests"
adapter_tests.mkdir(parents=True, exist_ok=True)
test_classpath = os.pathsep.join(map(str, [classes, *jars]))
subprocess.run(["javac", "--release", "25", "-proc:none", "-classpath", test_classpath,
"-d", str(adapter_tests),
str(SOURCE / "test/java/dev/horizonlab/client/DistantTerrainBudgetTest.java"),
str(SOURCE / "test/java/dev/horizonlab/client/DistantTerrainSettingsTest.java"),
str(SOURCE / "test/java/dev/horizonlab/geometry/RelativeTerrainPositionTest.java"),
str(SOURCE / "test/java/dev/horizonlab/metrics/ResidentMemoryTest.java")], check=True)
subprocess.run(["java", "-cp", os.pathsep.join([str(adapter_tests), test_classpath]),
"dev.horizonlab.client.DistantTerrainBudgetTest"], check=True)
subprocess.run(["java", "-cp", os.pathsep.join([str(adapter_tests), test_classpath]),
"dev.horizonlab.client.DistantTerrainSettingsTest"], check=True)
subprocess.run(["java", "--enable-native-access=ALL-UNNAMED", "-cp", os.pathsep.join([str(adapter_tests), test_classpath]),
"dev.horizonlab.metrics.ResidentMemoryTest"], check=True)
subprocess.run(["java", "-cp", os.pathsep.join([str(adapter_tests), test_classpath]),
"dev.horizonlab.geometry.RelativeTerrainPositionTest"], check=True)
output = ROOT / "dist" / f"horizon-{lock['pack_version']}.jar"
output.parent.mkdir(exist_ok=True)
entries = {path.relative_to(classes).as_posix(): path.read_bytes() for path in classes.rglob("*.class")}
if any(not name.startswith("dev/horizonlab/") for name in entries):
raise ValueError("Only original Horizon classes may be packaged")
resources = SOURCE / "main/resources"
entries.update({path.relative_to(resources).as_posix(): path.read_bytes() for path in resources.rglob("*") if path.is_file()})
entries["LICENSE"] = (ROOT / "LICENSE").read_bytes()
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for name in sorted(entries):
info = zipfile.ZipInfo(name, date_time=(2026, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, entries[name])
print(f"Built {output} ({output.stat().st_size} bytes, sha256 {lab.digest(output, 'sha256')})")
return output
def build_dispatch_fixture():
return build_integration_fixture("dispatch", "dev.horizonlab.dispatch.DispatchIntegration")
def build_tick_dedup_fixture():
return build_integration_fixture("tick-dedup", "dev.horizonlab.dispatch.TickDedupIntegration")
def build_culling_empty_fixture():
return build_integration_fixture("culling-empty", "dev.horizonlab.dispatch.CullingEmptyIntegration", ["entityculling"])
def build_integration_fixture(name, entrypoint, extra_projects=(), fixture_mixins=(), fixture_dependencies=None):
"""Create a private integration-test mod; never include it in release packs."""
arguments = [json.loads(line) for line in (BUILD / "javac.args").read_text().splitlines()]
classpath = arguments[arguments.index("-classpath") + 1] + os.pathsep + str(BUILD / "classes")
lock = json.loads((ROOT / "packs/mods.lock.json").read_text())
for name_project in extra_projects:
project = lock["projects"][name_project]
jar = runtime.fetch(project["url"], ROOT / ".local/pack-cache" / project["filename"],
project["hashes"]["sha512"], "sha512")
classpath += os.pathsep + str(jar)
classes = BUILD / f"{name}-fixture"
if classes.exists():
shutil.rmtree(classes)
classes.mkdir(parents=True)
fixture_sources = [SOURCE / "test/java" / (entrypoint.replace(".", "/") + ".java")]
fixture_sources.extend(SOURCE / "test/java/dev/horizonlab/dispatch/fixture" / (mixin + ".java") for mixin in fixture_mixins)
subprocess.run(["javac", "--release", "25", "-proc:none", "-classpath", classpath,
"-d", str(classes), *map(str, fixture_sources)], check=True)
output = BUILD / f"{name}-fixture.jar"
metadata = {"schemaVersion": 1, "id": "horizon_" + name.replace("-", "_") + "_fixture",
"version": "1.0.0", "environment": "client", "entrypoints": {"client": [entrypoint]},
"depends": {"horizonlab": "*", **({"distanthorizons": "3.3.4"}
if fixture_dependencies is None else fixture_dependencies)}}
if fixture_mixins:
metadata["mixins"] = [name + "-fixture.mixins.json"]
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive:
if fixture_mixins:
archive.writestr(metadata["mixins"][0], json.dumps({"required": True, "package": "dev.horizonlab.dispatch.fixture",
"compatibilityLevel": "JAVA_25", "client": list(fixture_mixins), "injectors": {"defaultRequire": 1}}))
for path in sorted(classes.rglob("*.class")):
archive.write(path, path.relative_to(classes).as_posix())
archive.writestr("fabric.mod.json", json.dumps(metadata))
print(f"Private {name} integration fixture: {output}")
return output
def benchmark_workers():
arguments = [json.loads(line) for line in (BUILD / "javac.args").read_text().splitlines()]
classpath = os.pathsep.join([str(BUILD / "classes"), arguments[arguments.index("-classpath") + 1]])
tests = BUILD / "worker-benchmark"
tests.mkdir(parents=True, exist_ok=True)
subprocess.run(["javac", "--release", "25", "-proc:none", "-cp", classpath,
"-d", str(tests), str(SOURCE / "test/java/dev/horizonlab/budget/WorkerPacingBenchmark.java")], check=True)
output = ROOT / "dist/worker-pacing-benchmark.txt"
with output.open("w", encoding="utf-8") as report:
report.write(f"Java {subprocess.check_output(['java', '--version'], text=True).splitlines()[0]}\n")
report.write(f"Platform {platform.system()} {platform.release()} {platform.machine()}; logical CPUs {os.cpu_count()}\n")
report.flush()
for repeat in range(3):
for ratio in ["0.5", "0.05"]:
for mode in ["upstream", "precise"]:
subprocess.run(["java", "-cp", os.pathsep.join([str(tests), classpath]),
"dev.horizonlab.budget.WorkerPacingBenchmark", ratio, "10000", "50000", mode],
stdout=report, check=True)
print(output.read_text(), end="")
def benchmark_debounce():
arguments = [json.loads(line) for line in (BUILD / "javac.args").read_text().splitlines()]
classpath = os.pathsep.join([str(BUILD / "classes"), arguments[arguments.index("-classpath") + 1]])
tests = BUILD / "debounce-benchmark"
tests.mkdir(parents=True, exist_ok=True)
subprocess.run(["javac", "--release", "25", "-proc:none", "-cp", classpath,
"-d", str(tests), str(SOURCE / "test/java/dev/horizonlab/budget/DebounceQueueBenchmark.java")], check=True)
output = ROOT / "dist/debounce-queue-benchmark.txt"
with output.open("w", encoding="utf-8") as report:
report.write(f"Java {subprocess.check_output(['java', '--version'], text=True).splitlines()[0]}\n")
report.write(f"Platform {platform.system()} {platform.release()} {platform.machine()}; logical CPUs {os.cpu_count()}\n")
report.flush()
subprocess.run(["java", "-cp", os.pathsep.join([str(tests), classpath]),
"dev.horizonlab.budget.DebounceQueueBenchmark"], stdout=report, check=True)
print(output.read_text(), end="")
def benchmark_headers():
arguments = [json.loads(line) for line in (BUILD / "javac.args").read_text().splitlines()]
classpath = arguments[arguments.index("-classpath") + 1]
tests = BUILD / "header-benchmark"
tests.mkdir(parents=True, exist_ok=True)
subprocess.run(["javac", "--release", "25", "-proc:none", "-cp", classpath,
"-d", str(tests), str(SOURCE / "test/java/dev/horizonlab/metrics/ObjectHeaderBenchmark.java")], check=True)
output = ROOT / "dist/object-header-benchmark.txt"
with output.open("w", encoding="utf-8") as report:
report.write(f"Java {subprocess.check_output(['java', '--version'], text=True).splitlines()[0]}\n")
report.write(f"Platform {platform.system()} {platform.release()} {platform.machine()}; logical CPUs {os.cpu_count()}\n")
report.flush()
for repeat in range(3):
for enabled in [False, True]:
report.write(f"fork={repeat} requested_compact_headers={enabled}\n")
report.flush()
subprocess.run(["java", "-Xms128M", "-Xmx512M",
"-XX:" + ("+" if enabled else "-") + "UseCompactObjectHeaders",
"-cp", os.pathsep.join([str(tests), classpath]),
"dev.horizonlab.metrics.ObjectHeaderBenchmark", str(enabled).lower()],
stdout=report, check=True)
print(output.read_text(), end="")
def benchmark_idle():
arguments = [json.loads(line) for line in (BUILD / "javac.args").read_text().splitlines()]
classpath = os.pathsep.join([str(BUILD / "classes"), arguments[arguments.index("-classpath") + 1]])
tests = BUILD / "idle-benchmark"
tests.mkdir(parents=True, exist_ok=True)
subprocess.run(["javac", "--release", "25", "-proc:none", "-cp", classpath,
"-d", str(tests), str(SOURCE / "test/java/dev/horizonlab/budget/IdleQueueBenchmark.java")], check=True)
output = ROOT / "dist/idle-queue-benchmark.txt"
with output.open("w", encoding="utf-8") as report:
report.write(f"Java {subprocess.check_output(['java', '--version'], text=True).splitlines()[0]}\n")
report.write(f"Platform {platform.system()} {platform.release()} {platform.machine()}; logical CPUs {os.cpu_count()}\n")
report.flush()
subprocess.run(["java", "-cp", os.pathsep.join([str(tests), classpath]),
"dev.horizonlab.budget.IdleQueueBenchmark"], stdout=report, check=True)
print(output.read_text(), end="")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--test-only", action="store_true")
parser.add_argument("--benchmark-workers", action="store_true", help="Measure tiny-job executor pacing; not Minecraft FPS")
parser.add_argument("--benchmark-headers", action="store_true", help="Measure object allocation with compact headers off/on; not game RAM")
args = parser.parse_args()
if args.test_only and (args.benchmark_workers or args.benchmark_headers):
parser.error("Benchmarks require the compiled mod; omit --test-only")
test_metrics()
if not args.test_only:
build()
if args.benchmark_workers:
benchmark_workers()
if args.benchmark_headers:
benchmark_headers()
-192
View File
@@ -1,192 +0,0 @@
#!/usr/bin/env python3
"""Build portable pinned Modrinth packs; game code/assets remain launcher downloads."""
import argparse
import json
from pathlib import Path
import shutil
import zipfile
try:
from . import lab, runtime
except ImportError:
import lab
import runtime
ROOT = lab.ROOT
CORE = ["fabric-api", "sodium", "lithium", "ferrite-core", "immediatelyfast", "entityculling"]
PROFILES = {
"baseline": {"title": "Vanilla baseline", "mods": ["fabric-api"], "render": 12, "lod": 0, "heap": 6144},
"core-performance": {"title": "Full-detail performance comparison", "mods": CORE, "render": 12, "lod": 0, "heap": 6144},
"m2-balanced": {"title": "M2 Pro balanced", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144},
"adaptive": {"title": "Adaptive terrain experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "adaptive": True},
"worker-pacing": {"title": "Precise worker pacing experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "pacing": True},
"adaptive-pacing": {"title": "Adaptive precise pacing experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "adaptive": True, "pacing": True},
"queue-wait": {"title": "Debounce queue wait experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "queue_wait": True},
"adaptive-queue": {"title": "Adaptive pacing and queue wait experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "adaptive": True, "pacing": True, "queue_wait": True},
"dispatch-retry": {"title": "Rejected dispatch retry experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "dispatch_retry": True},
"tick-dedup": {"title": "Terrain tick membership experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "tick_dedup": True},
"culling-empty": {"title": "Empty culling whitelist experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "culling_empty": True},
"quad-reuse": {"title": "Terrain quad reuse experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "quad_reuse": True},
"terrain-position": {"title": "Relative terrain position experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "terrain_position": True},
"culling-rays": {"title": "Culling ray scratch experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "culling_rays": True},
"foreground-first": {"title": "Foreground-first combined experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "adaptive": True, "pacing": True, "queue_wait": True, "dispatch_retry": True, "tick_dedup": True, "target_fps": 120},
"foreground-vulkan": {"title": "Foreground-first Vulkan experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "adaptive": True, "pacing": True, "queue_wait": True, "dispatch_retry": True, "tick_dedup": True, "target_fps": 120, "backend": "vulkan"},
"low-memory": {"title": "Low memory", "mods": CORE, "render": 8, "lod": 0, "heap": 3072},
"far-distance": {"title": "Far distance", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 256, "heap": 8192},
"cinematic": {"title": "Cinematic (optional)", "mods": CORE + ["distanthorizons", "iris", "complementary-reimagined"], "render": 12, "lod": 128, "heap": 6144},
"vulkan": {"title": "Vulkan experiment", "mods": CORE + ["distanthorizons"], "render": 12, "lod": 128, "heap": 6144, "backend": "vulkan"},
}
def read_lock():
return json.loads((ROOT / "packs/mods.lock.json").read_text())
def validate_selection(lock, selection):
projects = lock["projects"]
selected_ids = {projects[name]["project_id"] for name in selection}
selected_versions = {projects[name]["version_id"] for name in selection}
for name in selection:
entry = projects[name]
if lock["minecraft"] not in entry["game_versions"]:
raise ValueError(f"Wrong game version: {name}")
if not entry["filename"].endswith(".zip") and "fabric" not in entry["loaders"]:
raise ValueError(f"Wrong loader: {name}")
if Path(entry["filename"]).name != entry["filename"] or "\\" in entry["filename"]:
raise ValueError(f"Unsafe filename: {name}")
if not entry["url"].startswith("https://cdn.modrinth.com/"):
raise ValueError(f"Unexpected download host: {name}")
for dependency in entry["dependencies"]:
if dependency["dependency_type"] != "required":
continue
if dependency.get("version_id"):
if dependency["version_id"] not in selected_versions:
raise ValueError(f"Missing pinned dependency for {name}: {dependency}")
elif dependency.get("project_id") not in selected_ids:
raise ValueError(f"Missing dependency for {name}: {dependency}")
def mod_path(entry):
return ("shaderpacks/" if entry["filename"].endswith(".zip") else "mods/") + entry["filename"]
def options(profile):
# Simulation distance stays at the vanilla default in every profile.
values = {"version": read_lock()["options_data_version"], "graphicsPreset": json.dumps("custom"),
"renderDistance": profile["render"], "simulationDistance": 12, "enableVsync": "false",
"maxFps": 260, "guiScale": 2, "onboardAccessibility": "false",
"preferredGraphicsBackend": json.dumps(profile.get("backend", "opengl"))}
return "".join(f"{key}:{value}\n" for key, value in values.items())
def overrides(profile_name, mod_bytes):
profile = PROFILES[profile_name]
files = {f"mods/horizon-{read_lock()['pack_version']}.jar": mod_bytes,
"config/horizon-profile.txt": (profile_name + "\n").encode(),
"options.txt": options(profile).encode()}
experiment = any(profile.get(key, False) for key in ["adaptive", "pacing", "queue_wait", "dispatch_retry", "tick_dedup", "culling_empty", "culling_rays", "terrain_position", "quad_reuse"])
config = ROOT / "packs/config" / f"{'m2-balanced' if experiment else profile_name}.toml"
files["config/horizon-adaptive.properties"] = (
f"enabled={str(profile.get('adaptive', False)).lower()}\ntargetFps={profile.get('target_fps', 60)}\n").encode()
files["config/horizon-worker-pacing.properties"] = (
f"enabled={str(profile.get('pacing', False)).lower()}\n").encode()
files["config/horizon-queue-wait.properties"] = (
f"enabled={str(profile.get('queue_wait', False)).lower()}\n").encode()
files["config/horizon-dispatch-retry.properties"] = (
f"enabled={str(profile.get('dispatch_retry', False)).lower()}\n").encode()
files["config/horizon-tick-dedup.properties"] = (
f"enabled={str(profile.get('tick_dedup', False)).lower()}\n").encode()
files["config/horizon-culling-empty.properties"] = (
f"enabled={str(profile.get('culling_empty', False)).lower()}\n").encode()
files["config/horizon-culling-rays.properties"] = (
f"enabled={str(profile.get('culling_rays', False)).lower()}\n").encode()
files["config/horizon-quad-reuse.properties"] = (
f"enabled={str(profile.get('quad_reuse', False)).lower()}\n").encode()
files["config/horizon-terrain-position.properties"] = (
f"enabled={str(profile.get('terrain_position', False)).lower()}\n").encode()
files["config/horizon-terrain-read-wait.properties"] = (
f"enabled={str(bool(profile['lod'])).lower()}\n").encode()
if profile["lod"]:
if not config.exists():
raise FileNotFoundError(f"Missing verified DH profile config: {config}")
files["config/DistantHorizons.toml"] = config.read_bytes()
if profile_name == "cinematic":
# Start with shaders disabled: users explicitly select their visual/performance budget.
files["config/iris.properties"] = b"enableShaders=false\nshaderPack=ComplementaryReimagined_r5.9.3.zip\n"
return files
def write_zip(path, entries):
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for name in sorted(entries):
info = zipfile.ZipInfo(name, (2026, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
mode = 0o100755 if name.endswith(".command") else 0o100644
info.external_attr = mode << 16
archive.writestr(info, entries[name])
def build(profile_name, mod_jar):
lock = read_lock()
profile = PROFILES[profile_name]
validate_selection(lock, profile["mods"])
files = []
for name in profile["mods"]:
entry = lock["projects"][name]
files.append({"path": mod_path(entry), "hashes": entry["hashes"], "downloads": [entry["url"]],
"fileSize": entry["size"], "env": {"client": "required", "server": "unsupported"}})
index = {"formatVersion": 1, "game": "minecraft", "versionId": lock["pack_version"],
"name": "Horizon — " + profile["title"],
"summary": "Vanilla mechanics, pinned performance mods, measured frame and tick timings.",
"files": files, "dependencies": {"minecraft": lock["minecraft"], "fabric-loader": lock["fabric_loader"]}}
entries = {"modrinth.index.json": json.dumps(index, indent=2).encode()}
entries.update({"overrides/" + name: data for name, data in overrides(profile_name, mod_jar.read_bytes()).items()})
output = ROOT / "dist" / f"horizon-{profile_name}-{lock['pack_version']}.mrpack"
write_zip(output, entries)
print(f"Built {output.name}: {len(files)} pinned downloads")
return output
def prepare_instance(profile_name, destination, mod_jar):
lock = read_lock()
profile = PROFILES[profile_name]
validate_selection(lock, profile["mods"])
destination.mkdir(parents=True, exist_ok=True)
for name in profile["mods"]:
entry = lock["projects"][name]
source = runtime.fetch(entry["url"], ROOT / ".local/pack-cache" / entry["filename"],
entry["hashes"]["sha512"], "sha512")
target = destination / mod_path(entry)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, target)
runtime.install_original_mod(mod_jar, destination / "mods", f"horizon-{lock['pack_version']}.jar")
for name, data in overrides(profile_name, mod_jar.read_bytes()).items():
if name.startswith("mods/"):
continue
target = destination / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
print(f"Prepared {profile_name} instance: {destination}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--profile", choices=PROFILES)
parser.add_argument("--instance", type=Path, help="Local demo smoke-test directory (no normal account launch)")
parser.add_argument("--mod", type=Path, default=ROOT / "dist" / f"horizon-{read_lock()['pack_version']}.jar")
args = parser.parse_args()
if not args.mod.exists():
parser.error("Build the original mod first: python3 tools/build_mod.py")
if args.instance:
if not args.profile:
parser.error("--instance requires --profile")
prepare_instance(args.profile, args.instance.resolve(), args.mod)
else:
for profile in [args.profile] if args.profile else PROFILES:
build(profile, args.mod)
if __name__ == "__main__":
main()
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env python3
"""Create the downloadable client starter and hashes from original project artifacts. (The Java/Fabric server
starter is gone: horizon-server replaces it, docs/hosting.md.)"""
import hashlib
from pathlib import Path
try:
from . import build_pack
except ImportError:
import build_pack
ROOT = build_pack.ROOT
def main():
lock = build_pack.read_lock()
version = lock["pack_version"]
mod = ROOT / "dist" / f"horizon-{version}.jar"
if not mod.exists():
raise FileNotFoundError("Run tools/build_mod.py first")
packs = [build_pack.build(profile, mod) for profile in build_pack.PROFILES]
client = {pack.name: pack.read_bytes() for pack in packs}
client["START-HERE.txt"] = (ROOT / "packs/START-HERE.txt").read_text().replace("@PACK_VERSION@", version).encode()
client["Mac-Start.command"] = (ROOT / "packs/Mac-Start.command").read_text().replace("@PACK_VERSION@", version).encode()
client["LICENSE"] = (ROOT / "LICENSE").read_bytes()
viewer = (ROOT / "tools/Compare-Benchmarks.html").read_bytes()
client["Compare-Benchmarks.html"] = viewer
(ROOT / "dist/Compare-Benchmarks.html").write_bytes(viewer)
build_pack.write_zip(ROOT / "dist" / f"horizon-mac-starter-{version}.zip", client)
artifacts = sorted(path for path in (ROOT / "dist").glob("*") if version in path.name)
artifacts.append(ROOT / "dist/Compare-Benchmarks.html")
checksums = []
for path in artifacts:
if path.suffix in [".jar", ".mrpack", ".zip", ".html"]:
checksums.append(hashlib.sha256(path.read_bytes()).hexdigest() + " " + path.name)
(ROOT / "dist/SHA256SUMS.txt").write_text("\n".join(checksums) + "\n")
print("Built the client starter and SHA256SUMS.txt")
if __name__ == "__main__":
main()
-195
View File
@@ -1,195 +0,0 @@
#!/usr/bin/env python3
"""Compare two real Horizon JSON reports and flag mismatched workloads."""
import argparse
import json
import math
from pathlib import Path
MATCH_FIELDS = ("minecraft", "kind", "os", "os_version", "architecture", "gpu", "graphics_backend", "graphics_driver",
"server_runtime_kind", "client_graphics_capture_status", "java_version", "heap_max_bytes", "framebuffer_width", "framebuffer_height", "render_distance",
"simulation_distance", "vsync", "fps_limit", "graphics_preset", "ambient_occlusion",
"improved_transparency", "mipmap_levels", "entity_distance_scaling", "entity_shadows", "particles",
"clouds", "cloud_range", "fov", "world_difficulty", "dh_capture_status", "dh_rendering_enabled", "dh_renderer_mode",
"dh_lod_radius_chunks", "dh_horizontal_quality", "dh_vertical_quality", "dh_max_horizontal_resolution",
"dh_transparency", "dh_lod_shading", "dh_lod_only_mode", "dh_camera_quality_dropoff",
"dh_ambient_occlusion", "iris_capture_status", "iris_shaders_enabled", "iris_shader_pack")
TICK_STATE_FIELDS = ("configured_tick_rate", "frozen", "stepping", "sprinting", "runs_game_elements")
def tick_state_available(report):
def number(value):
if not isinstance(value, (int, float)) or isinstance(value, bool):
return False
try:
return math.isfinite(value)
except OverflowError:
return False
if report.get("tick_state_capture_status") != "sampled_server_ticks":
return False
count = report.get("tick_state_samples")
if not number(count) or count <= 0 or count > 9_007_199_254_740_991 or count != int(count):
return False
for boundary in ["tick_state_first", "tick_state_last"]:
state = report.get(boundary)
if not isinstance(state, dict) or not number(state.get("configured_tick_rate")) or state["configured_tick_rate"] < 1:
return False
if any(not isinstance(state.get(field), bool) for field in TICK_STATE_FIELDS[1:]):
return False
changed = report.get("tick_state_changed")
if not isinstance(changed, list) or any(field not in TICK_STATE_FIELDS for field in changed):
return False
return all(number(report.get("tick_state_" + field + "_samples"))
and 0 <= report["tick_state_" + field + "_samples"] <= count
and report["tick_state_" + field + "_samples"] == int(report["tick_state_" + field + "_samples"])
for field in ["frozen", "stepping", "sprinting", "suppressed", "nonstandard_rate"])
def compare_tick_states(baseline, candidate):
mismatches, warnings = [], []
available = [tick_state_available(report) for report in [baseline, candidate]]
for name, report, captured in zip(["baseline", "candidate"], [baseline, candidate], available):
if not captured:
warnings.append(f"{name} tick-manager state was not captured or is invalid; simulation workload equivalence is unknown.")
continue
if report["tick_state_changed"]:
mismatches.append(f"{name} tick-manager state changed during recording")
warnings.append(f"{name} sampled tick-manager changes: " + ", ".join(report["tick_state_changed"]) + ".")
abnormal = [field for field in ["frozen", "stepping", "sprinting", "suppressed", "nonstandard_rate"]
if report["tick_state_" + field + "_samples"] > 0]
if abnormal:
warnings.append(f"{name} includes tick-manager samples with " + ", ".join(abnormal) + "; interpret execution times as this workload.")
if all(available):
for boundary in ["tick_state_first", "tick_state_last"]:
for field in TICK_STATE_FIELDS:
if baseline[boundary][field] != candidate[boundary][field]:
mismatches.append(f"{boundary}.{field}: {baseline[boundary][field]!r} vs {candidate[boundary][field]!r}")
warnings.append("Tick-manager state is sampled after measured server invocations. Changes entirely between samples, paused intervals and completed world work are not verified.")
return mismatches, warnings
def software_warnings(baseline, candidate):
warnings = []
for field in ["java_vendor", "java_vm_name", "java_runtime_version"]:
if not all(isinstance(report.get(field), str) and report[field] for report in [baseline, candidate]):
warnings.append(f"{field} was not captured in both reports; Java implementation equivalence is unknown.")
elif baseline[field] != candidate[field]:
warnings.append(f"Software {field} differs: {baseline[field]!r} vs {candidate[field]!r}. Treat it as an explicit experiment variable.")
mods = []
for report in [baseline, candidate]:
entries = report.get("mods")
mods.append(set(entries) if isinstance(entries, list) and all(isinstance(entry, str) and entry for entry in entries) else None)
if any(entries is None for entries in mods):
warnings.append("Loaded mod versions were not captured in both reports; software equivalence is unknown.")
elif mods[0] != mods[1]:
warnings.append("Loaded mod versions differ. Treat added, removed or upgraded mods as explicit experiment variables.")
for label, entries in [("baseline", mods[0] - mods[1]), ("candidate", mods[1] - mods[0])]:
ordered = sorted(entries)
if ordered:
suffix = f"; {len(ordered) - 12} more" if len(ordered) > 12 else ""
warnings.append(f"Only {label}: " + ", ".join(ordered[:12]) + suffix + ".")
return warnings
def server_context_warnings(baseline, candidate):
warnings = []
for name, report in [("baseline", baseline), ("candidate", candidate)]:
kind = report.get("server_runtime_kind")
if kind not in ["integrated", "dedicated"]:
warnings.append(f"{name} server runtime kind was not captured; integrated/dedicated equivalence is unknown.")
if kind == "integrated":
captured = report.get("client_graphics_capture_status") == "captured_at_recording_start" and all(
isinstance(report.get(key), str) and report[key].strip() for key in ["gpu", "graphics_backend", "graphics_driver"])
if not captured:
warnings.append(f"{name} integrated-server client graphics were not captured; shared rendering workload is unknown.")
warnings.append("Integrated-server graphics context is a recording-start snapshot; later graphics changes and full client workload equivalence are not verified.")
return warnings
def compare(baseline, candidate):
for report in [baseline, candidate]:
if report.get("schema") != 1 or report.get("kind") not in ["frames", "ticks"]:
raise ValueError("Expected a schema-1 Horizon frame/tick report")
if report.get("stats", {}).get("count", 0) < 1:
raise ValueError("Report contains no samples")
if baseline["kind"] != candidate["kind"]:
raise ValueError("Compare frames with frames, or ticks with ticks")
mismatches = [f"{key}: {baseline.get(key)!r} vs {candidate.get(key)!r}" for key in MATCH_FIELDS
if baseline.get(key) != candidate.get(key)]
warnings = ["World, route, warm-up, shader state and LOD cache must also match; these are not verified by this tool."]
warnings.extend(software_warnings(baseline, candidate))
if baseline["kind"] == "ticks":
warnings.extend(server_context_warnings(baseline, candidate))
tick_mismatches, tick_warnings = compare_tick_states(baseline, candidate)
mismatches.extend(tick_mismatches)
warnings.extend(tick_warnings)
warnings.append("Tick durations exclude scheduler sleep. Observed TPS counts measured invocations, including frozen ticks, and includes recording time spent paused; tick execution capacity is not TPS.")
if baseline["kind"] == "frames":
for field in ["adaptive_budget_enabled", "adaptive_target_fps", "precise_worker_pacing", "debounce_queue_wait",
"wakeable_idle_wait", "dispatch_retry_backoff", "tick_membership_dedup", "empty_culling_whitelist", "culling_ray_scratch", "terrain_position_arithmetic", "terrain_quad_reuse", "interruptible_terrain_read", "dh_thread_count", "dh_runtime_ratio"]:
if field in baseline and field in candidate and baseline[field] != candidate[field]:
warnings.append(f"Experiment {field} differs: {baseline[field]!r} vs {candidate[field]!r}.")
for name, report in [("baseline", baseline), ("candidate", candidate)]:
if report.get("dh_capture_status") not in ["available", "not_installed"]:
warnings.append(f"{name} distant-terrain configuration was not captured; quality equivalence is unknown.")
if report.get("iris_capture_status") not in ["available", "not_installed"]:
warnings.append(f"{name} Iris state was not captured; shader equivalence is unknown.")
if report.get("visual_settings_changed"):
mismatches.append(f"{name} visual settings changed during recording")
warnings.append(f"{name} visual settings changed between capture boundaries: {report['visual_settings_changed']!r}.")
for field in ["compact_object_headers", "garbage_collectors"]:
if field not in baseline or field not in candidate:
warnings.append(f"{field} was not captured in both reports; JVM configuration equivalence is unknown.")
elif baseline[field] != candidate[field]:
warnings.append(f"JVM {field} differs: {baseline[field]!r} vs {candidate[field]!r}. Treat this as an explicit experiment variable.")
if abs(baseline.get("elapsed_seconds", 0) - candidate.get("elapsed_seconds", 0)) > max(
2, min(baseline.get("elapsed_seconds", 0), candidate.get("elapsed_seconds", 0)) * 0.1):
warnings.append("Recording durations differ; match durations when comparing memory peaks and collection totals.")
for name, report in [("baseline", baseline), ("candidate", candidate)]:
if report.get("dropped_samples", 0):
warnings.append(f"{name} exceeded the recording buffer: {report['dropped_samples']} dropped samples.")
if report["kind"] == "frames" and report.get("excluded_frames", 0):
warnings.append(f"{name} excluded {report['excluded_frames']} menu, paused or unfocused frame starts. Process CPU still spans the whole recording.")
if report.get("elapsed_seconds", 0) < 60:
warnings.append(f"{name} is shorter than 60 seconds; use longer repeated runs for decisions.")
if baseline["kind"] == "ticks" and baseline.get("peak_players") != candidate.get("peak_players"):
mismatches.append("Peak player count differs")
rows = []
keys = ["meanRate", "lowOnePercentRate", "p95Ms", "p99Ms", "maxMs"] if baseline["kind"] == "frames" else ["meanMs", "p95Ms", "p99Ms", "maxMs"]
for key in keys:
before, after = baseline["stats"][key], candidate["stats"][key]
rows.append({"metric": key, "baseline": before, "candidate": after,
"change_percent": 100 * (after / before - 1) if before else None})
for key in ["heap_used_end_bytes", "heap_used_peak_sampled_bytes", "nonheap_used_peak_sampled_bytes",
"process_rss_last_sampled_bytes", "process_rss_peak_sampled_bytes", "process_cpu_mean_cores", "gc_time_ms", "observed_ticks_per_second"]:
if key in baseline and key in candidate:
before, after = baseline[key], candidate[key]
rows.append({"metric": key, "baseline": before, "candidate": after,
"change_percent": 100 * (after / before - 1) if before else None})
return {"kind": baseline["kind"], "mismatches": mismatches, "warnings": warnings, "metrics": rows}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("baseline", type=Path)
parser.add_argument("candidate", type=Path)
parser.add_argument("--json", action="store_true")
parser.add_argument("--strict", action="store_true", help="Exit nonzero on unmatched captured settings")
args = parser.parse_args()
result = compare(json.loads(args.baseline.read_text()), json.loads(args.candidate.read_text()))
if args.json:
print(json.dumps(result, indent=2))
else:
for row in result["metrics"]:
change = "n/a" if row["change_percent"] is None else f"{row['change_percent']:+.1f}%"
print(f"{row['metric']:25} {row['baseline']:12.3f} -> {row['candidate']:12.3f} ({change})")
for mismatch in result["mismatches"]:
print("UNMATCHED: " + mismatch)
for warning in result["warnings"]:
print("NOTE: " + warning)
return int(args.strict and bool(result["mismatches"]))
if __name__ == "__main__":
raise SystemExit(main())
+4 -4
View File
@@ -24,7 +24,7 @@ Games:
mod (mods/horizon/src/bench), built here; nothing of the game
is redistributed
vanilla-mods the same + Sodium, Lithium, FerriteCore and ImmediatelyFast
(pinned in packs/mods.lock.json)
(pinned in tools/benchmark-deps/mods.lock.json)
neoforge Minecraft 1.21.1 on NeoForge (dev-only, tools/neoforge_oracle.py
installs it under .local/neoforge-oracle) + a driver mod
(mods/horizon/src/neoforge-bench) + the jars of --mod-jars and
@@ -524,9 +524,9 @@ def build_bench_mod(jars):
def perf_mod_jars():
"""Sodium, Lithium, FerriteCore and ImmediatelyFast for 26.3 (downloaded from
Modrinth, checked against packs/mods.lock.json, never committed), plus
Modrinth, checked against tools/benchmark-deps/mods.lock.json, never committed), plus
Fabric API when one of them needs it."""
lock = json.loads((ROOT / "packs/mods.lock.json").read_text())
lock = json.loads((ROOT / "tools/benchmark-deps/mods.lock.json").read_text())
jars, needs_api = [], False
for name in PERF_MODS:
project = lock["projects"][name]
@@ -866,7 +866,7 @@ def app(args):
jars = [j.name for f in filter(None, (args.mod_jars, args.deps)) for j in sorted(Path(f).glob("*.jar"))]
return {"name": "minecraft", "version": "1.21.1",
"mods": [{"name": "neoforge", "version": neoforge_oracle.NEOFORGE}] + [{"name": j} for j in jars]}
lock = json.loads((ROOT / "packs/mods.lock.json").read_text())
lock = json.loads((ROOT / "tools/benchmark-deps/mods.lock.json").read_text())
mods = [{"name": "fabric-loader", "version": lock["fabric_loader"]}]
if args.game == "vanilla-mods":
mods += [{"name": n, "version": lock["projects"][n]["version"]} for n in PERF_MODS]
+5 -98
View File
@@ -1,19 +1,9 @@
#!/usr/bin/env python3
"""Prepare an isolated game runtime from your own launcher install; local smoke tests use demo mode."""
"""Shared Java dependency and platform helpers for Rust comparison benchmarks."""
from concurrent.futures import ThreadPoolExecutor
import argparse
import json
import os
from pathlib import Path
import platform
import re
import shutil
import subprocess
import sys
import time
import urllib.error
import zipfile
try:
from . import lab
@@ -21,40 +11,11 @@ except ImportError:
import lab
ROOT = lab.ROOT
CACHE = ROOT / ".local" / "runtime"
library_allowed = lab.library_allowed
native_library_matches = lab.native_library_matches
def install_original_mod(source, mod_dir, filename=None):
"""Replace our own previous versions, retaining backups outside Fabric's mod scan."""
mod_dir.mkdir(parents=True, exist_ok=True)
filename = filename or source.name
# Retire both current and pre-rebrand filenames to avoid duplicate mod IDs.
candidates = sorted({*mod_dir.glob("horizon-*.jar"), *mod_dir.glob("horizon-lab-*.jar")})
for old in candidates:
if old.name == filename:
continue
with zipfile.ZipFile(old) as archive:
if json.loads(archive.read("fabric.mod.json"))["id"] != "horizonlab":
raise ValueError(f"Refusing to replace another mod: {old}")
backup = mod_dir.parent / ".horizon-mod-backups"
backup.mkdir(exist_ok=True)
destination = backup / f"{old.stem}-{lab.digest(old, 'sha256')[:12]}.jar"
shutil.move(str(old), str(destination))
if source.resolve() != (mod_dir / filename).resolve():
shutil.copyfile(source, mod_dir / filename)
CACHE = ROOT / ".local" / "runtime"
def jvm_options(heap, compact_headers=False, collector="default"):
options = ["-Xms512M", f"-Xmx{heap}"]
if compact_headers:
options.append("-XX:+UseCompactObjectHeaders")
if collector != "default":
options.append({"g1": "-XX:+UseG1GC", "zgc": "-XX:+UseZGC"}[collector])
return options
def fetch(url, path, checksum, algorithm="sha1"):
for attempt in range(3):
try:
@@ -65,8 +26,9 @@ def fetch(url, path, checksum, algorithm="sha1"):
time.sleep(attempt + 1)
def fabric_libraries():
profile = json.loads((ROOT / "packs/fabric.lock.json").read_text())
profile = json.loads((ROOT / "tools/benchmark-deps/fabric.lock.json").read_text())
jars = []
for library in profile["libraries"]:
group, artifact, version = library["name"].split(":")
@@ -76,6 +38,7 @@ def fabric_libraries():
return jars
def conditional_client_jvm_options(manifest, system=None, architecture=None):
"""Use official OS/architecture flags alongside our managed JVM/classpath options."""
options = []
@@ -88,59 +51,3 @@ def conditional_client_jvm_options(manifest, system=None, architecture=None):
raise ValueError("Unsupported conditional client JVM argument")
options.extend(values)
return options
def prepare_client(assets=True):
"""Classpath and natives from the user's own launcher install; nothing of the game is downloaded."""
lock, directory = lab.config()
manifest, client = lab.import_client(lock, directory)
jars = [client]
natives = CACHE / "natives"
natives.mkdir(parents=True, exist_ok=True)
for artifact in lab.client_library_artifacts(manifest):
jar = lab.import_library(artifact, directory / "libraries/client")
jars.append(jar)
with zipfile.ZipFile(jar) as archive:
for name in archive.namelist():
if name.endswith((".so", ".dll", ".dylib", ".jnilib")):
(natives / Path(name).name).write_bytes(archive.read(name))
jars.extend(fabric_libraries())
if assets:
store, index = lab.launcher_assets(manifest)
unique = {v["hash"] for v in json.loads(index.read_text())["objects"].values()}
print(f"Verifying {len(unique)} game assets in {store}...", flush=True)
with ThreadPoolExecutor(max_workers=4) as pool:
list(pool.map(lambda checksum: lab.launcher_object(store, checksum), sorted(unique)))
return manifest, jars, natives
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=["prepare", "demo"])
parser.add_argument("--instance", type=Path, default=CACHE / "demo")
parser.add_argument("--heap", default="4G")
parser.add_argument("--compact-headers", action="store_true", help="Enable Java 25 compact object headers for this demo process")
parser.add_argument("--gc", choices=["default", "g1", "zgc"], default="default", help="Select a collector for this process; default leaves JVM policy in use")
args = parser.parse_args()
if not re.fullmatch(r"[1-9][0-9]*[MGmg]", args.heap):
parser.error("--heap must look like 4G")
manifest, jars, natives = prepare_client()
if args.command == "prepare":
return
instance = args.instance.resolve()
instance.mkdir(parents=True, exist_ok=True)
command = ["java", *jvm_options(args.heap, args.compact_headers, args.gc),
*conditional_client_jvm_options(manifest),
"--enable-native-access=ALL-UNNAMED",
f"-Djava.library.path={natives}", f"-Dorg.lwjgl.system.SharedLibraryExtractPath={natives}",
"-Dfabric.log.disableAnsi=true", "-cp", os.pathsep.join(map(str, jars)),
"net.fabricmc.loader.impl.launch.knot.KnotClient",
"--username", "HorizonDemo", "--version", manifest["id"], "--gameDir", str(instance),
"--assetsDir", str(lab.minecraft_dir() / "assets"), "--assetIndex", manifest["assetIndex"]["id"],
"--accessToken", "0", "--uuid", "00000000000000000000000000000000", "--demo",
"--width", "1280", "--height", "800"]
subprocess.run(command, cwd=instance, check=True)
if __name__ == "__main__":
main()
+4 -4
View File
@@ -32,8 +32,8 @@ Two more Java servers, both dev-only downloads into .local/server-bench/jars (ne
SHA-256 it publishes, with Paper's own defaults except `connection-throttle` (off: the bots all join from
one address). Paperclip patches the pinned server jar (your copy, put in
its cache so nothing is fetched from Mojang).
- `fabric`: the pinned vanilla server on the pinned Fabric loader (packs/fabric.lock.json) with Lithium from
packs/mods.lock.json (and Fabric API only if Lithium asks for it), checked against the lock's SHA-512.
- `fabric`: the pinned vanilla server on the pinned Fabric loader (tools/benchmark-deps/fabric.lock.json) with Lithium from
tools/benchmark-deps/mods.lock.json (and Fabric API only if Lithium asks for it), checked against the lock's SHA-512.
Both run with the same heap as vanilla (`--heap`).
"""
@@ -190,8 +190,8 @@ def paper_jar():
def fabric_mods():
"""Lithium from packs/mods.lock.json, plus Fabric API if Lithium depends on it (checked, cached)."""
lock = json.loads((ROOT / 'packs/mods.lock.json').read_text())
"""Lithium from tools/benchmark-deps/mods.lock.json, plus Fabric API if Lithium depends on it (checked, cached)."""
lock = json.loads((ROOT / 'tools/benchmark-deps/mods.lock.json').read_text())
def fetch(name):
project = lock['projects'][name]
return runtime.fetch(project['url'], JARS / project['filename'], project['hashes']['sha512'], 'sha512')
-198
View File
@@ -1,198 +0,0 @@
#!/usr/bin/env python3
"""Start real clients and verify a nonuniform GPU framebuffer readback."""
import argparse
import hashlib
import json
import os
import platform
import re
import shutil
import sys
try:
from . import lab
except ImportError:
import lab
try:
from . import build_mod, build_pack
from .test_culling_rays import FixtureTimeout, run_fixture
from .test_dispatch import failure_summary
except ImportError:
import build_mod
import build_pack
from test_culling_rays import FixtureTimeout, run_fixture
from test_dispatch import failure_summary
def graphics_failure_summary(run, report_count):
"""Selected stages/types/codes/methods only; messages and paths stay private."""
result = failure_summary(run, report_count)
pattern = r'(?<![\w.$])((?:java|org\.lwjgl|com\.mojang|net\.minecraft|net\.fabricmc|org\.spongepowered|dev\.horizonlab)\.[A-Za-z_$][\w.$]{0,160}(?:Exception|Error))(?=[:\s]|$)'
observed = set(result['java_exception_types']) | set(re.findall(pattern, run.stdout, re.ASCII))
expected_auth = {name for name in observed if name.startswith(('com.mojang.authlib.', 'com.mojang.realmsclient.'))}
result['java_exception_types'] = sorted(observed - expected_auth)[:8]
result['expected_demo_authentication_errors_observed'] = bool(expected_auth)
for key, marker in [('backend_attempts', 'HORIZON_GRAPHICS_BACKEND_ATTEMPT '),
('created_devices', 'HORIZON_GRAPHICS_DEVICE_CREATED ')]:
result[key] = [json.loads(line.split(marker, 1)[1]) for line in run.stdout.splitlines() if marker in line][:16]
result['client_initializer_reached'] = 'HORIZON_CLIENT_GRAPHICS_INITIALIZER_READY' in run.stdout
result['readback_requested'] = 'HORIZON_CLIENT_GRAPHICS_READBACK_REQUESTED' in run.stdout
result['readback_callback_received'] = 'HORIZON_CLIENT_GRAPHICS_READBACK_RECEIVED' in run.stdout
inspection_marker = 'HORIZON_CLIENT_GRAPHICS_READBACK_INSPECTED '
inspections = []
fields = ('framebuffer_width', 'framebuffer_height', 'sampled_pixels', 'distinct_sampled_rgb')
for line in run.stdout.splitlines():
if inspection_marker not in line:
continue
try:
item = json.loads(line.split(inspection_marker, 1)[1])
except json.JSONDecodeError:
continue
if isinstance(item, dict) and all(type(item.get(field)) is int
and 0 <= item[field] <= 2_147_483_647 for field in fields):
inspections.append({field: item[field] for field in fields})
if len(inspections) == 4:
break
result['readback_inspections'] = inspections
waiting_marker = 'HORIZON_CLIENT_GRAPHICS_WAIT_STATE '
boolean_fields = ('world_loaded', 'player_present', 'overlay_present',
'screen_present', 'window_active', 'paused')
result['last_waiting_state'] = None
for line in run.stdout.splitlines():
if waiting_marker not in line:
continue
try:
state = json.loads(line.split(waiting_marker, 1)[1])
except json.JSONDecodeError:
continue
if (isinstance(state, dict) and all(type(state.get(field)) is bool for field in boolean_fields)
and type(state.get('ready_client_ticks')) is int and 0 <= state['ready_client_ticks'] <= 2_147_483_647):
result['last_waiting_state'] = {field: state[field] for field in (*boolean_fields, 'ready_client_ticks')}
# These exact messages belong to our private fixture. Never emit arbitrary
# exception messages or infer that a separately logged Vulkan code caused it.
categories = {'Uniform title readback': 'uniform_readback',
'Incomplete title framebuffer': 'small_framebuffer',
'Private client graphics readback failed': 'callback_exception',
'Required shader pack is inactive': 'shader_inactive',
'Unexpected active shader pack': 'unexpected_shader_pack'}
result['probe_failure_categories'] = [category for message, category in categories.items()
if 'java.lang.AssertionError: ' + message in run.stdout]
result['vulkan_error_codes'] = sorted(set(re.findall(r'\bVK_ERROR_[A-Z_0-9]{1,100}\b', run.stdout)))[:16]
result['fatal_jvm_error'] = 'A fatal error has been detected by the Java Runtime Environment' in run.stdout
# Only Java stack-frame lines from graphics namespaces. Strip module prefixes,
# filenames, line numbers and messages; these identify locations, not causes.
stack_pattern = (r'^\s*at (?:[A-Za-z0-9_.@$-]{1,80}/{1,2})?'
r'((?:com\.mojang\.(?:renderpearl|blaze3d)\.|'
r'net\.minecraft\.client\.(?:renderer\.|Screenshot\.))'
r'[A-Za-z0-9_.$<>]{1,180})\(')
methods = re.findall(stack_pattern, run.stdout, re.MULTILINE | re.ASCII)
result['observed_graphics_stack_methods'] = list(dict.fromkeys(methods))[:8]
return result
def validate_shader_report(report, required):
if required and (report.get('shader_required') is not True
or report.get('iris_capture_status') != 'available'
or report.get('iris_shaders_enabled') is not True
or report.get('iris_expected_pack_matches') is not True):
raise AssertionError('Required runtime shader activation was not verified')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--native-display', action='store_true')
parser.add_argument('--profile', action='append', choices=sorted(build_pack.PROFILES))
parser.add_argument('--backend', choices=['opengl', 'vulkan'], help='Override backend in these private instances only')
parser.add_argument('--world', action='store_true', help='Create/open a disposable built-in demo world instead of stopping at the title')
parser.add_argument('--shaders', action='store_true', help='Require the pinned cinematic shader pack in a private world instance')
args = parser.parse_args()
if args.shaders and (not args.world or args.profile not in [None, ['cinematic']]):
parser.error('--shaders requires --world and the cinematic profile')
root = build_pack.ROOT
fixture = build_mod.build_integration_fixture('client-graphics',
'dev.horizonlab.metrics.ClientGraphicsIntegration',
fixture_mixins=['GraphicsBackendTraceMixin'], fixture_dependencies={})
version = build_pack.read_lock()['pack_version']
mod = root / 'dist' / f'horizon-{version}.jar'
original_hash = hashlib.sha256(mod.read_bytes()).hexdigest()
results = []
for profile in args.profile or (['cinematic'] if args.shaders else ['core-performance', 'm2-balanced']):
instance = root / '.local/runtime' / ('client-graphics-' + profile
+ ('-override-' + args.backend if args.backend else '') + ('-world' if args.world else '')
+ ('-shaders' if args.shaders else ''))
build_pack.prepare_instance(profile, instance, mod)
if args.world:
(instance / 'config/client-graphics-world.flag').touch()
if args.shaders:
expected = build_pack.read_lock()['projects']['complementary-reimagined']['filename']
(instance / 'config/iris.properties').write_text('enableShaders=true\nshaderPack=' + expected + '\n')
(instance / 'config/client-graphics-shaders.flag').write_text(expected + '\n')
if args.backend:
options = instance / 'options.txt'
text, count = re.subn(r'^preferredGraphicsBackend:.*$',
'preferredGraphicsBackend:' + json.dumps(args.backend), options.read_text(), flags=re.MULTILINE)
if count != 1:
raise AssertionError('Private backend preference is missing or ambiguous')
options.write_text(text)
shutil.copy2(fixture, instance / 'mods/client-graphics-fixture.jar')
command = [sys.executable, str(root / 'tools/runtime.py'), 'demo',
'--instance', str(instance), '--heap', '6G' if args.shaders else '3G']
if platform.system() == 'Linux' and not args.native_display:
command = ['xvfb-run', '-a', *command]
try:
run = run_fixture(command, instance, os.environ.copy())
except FixtureTimeout as error:
diagnostics = graphics_failure_summary(error.run, 0)
diagnostics.update(fixture='client-graphics', profile=profile,
completed=False, timed_out=True, timeout_seconds=240)
print('HORIZON_FIXTURE_FAILURE ' + json.dumps(diagnostics), flush=True)
raise RuntimeError('Graphics probe did not finish; diagnostics remain private') from None
marker = 'HORIZON_CLIENT_GRAPHICS_RESULT '
reports = [json.loads(line.split(marker, 1)[1]) for line in run.stdout.splitlines() if marker in line]
trace_marker = 'HORIZON_GRAPHICS_BACKEND_ATTEMPT '
attempts = [json.loads(line.split(trace_marker, 1)[1]) for line in run.stdout.splitlines() if trace_marker in line]
if run.returncode or len(reports) != 1:
diagnostics = graphics_failure_summary(run, len(reports))
diagnostics['profile'] = profile
diagnostics['preferred_backend'] = args.backend or build_pack.PROFILES[profile].get('backend', 'opengl')
print('HORIZON_FIXTURE_FAILURE ' + json.dumps(diagnostics), flush=True)
raise RuntimeError('Graphics probe failed; diagnostics remain private')
report = reports[0]
validate_shader_report(report, args.shaders)
if report['observed_mode'] != ('world' if args.world else 'title') or report['ready_client_ticks'] < 40:
raise AssertionError('Client never reached the requested graphics scene')
if report['overlay_present'] is not False:
raise AssertionError('Client graphics scene still has a loading overlay')
if args.world:
if report['world_loaded'] is not True or report['window_active'] is not True:
raise AssertionError('Client never reached focused demo-world gameplay')
if not list((instance / 'saves/Demo_World').rglob('*.dat')):
raise AssertionError('Disposable world did not leave saved data')
if report['sampled_pixels'] < 256 or report['distinct_sampled_rgb'] < 8:
raise AssertionError('GPU readback was uniform')
if hashlib.sha256((instance / 'mods' / mod.name).read_bytes()).hexdigest() != original_hash:
raise AssertionError('Runtime original mod differs from the intended build')
screenshot = instance / 'client-graphics-private.png'
if not screenshot.is_file() or hashlib.sha256(screenshot.read_bytes()).hexdigest() != report['private_png_sha256']:
raise AssertionError('Private screenshot evidence is missing or changed')
report.update(profile=profile, preferred_backend=args.backend or build_pack.PROFILES[profile].get('backend', 'opengl'),
launcher_exit_code=run.returncode, backend_attempts=attempts)
results.append(report)
print(json.dumps(report), flush=True)
output = root / 'dist/client-graphics-smoke.json'
output.write_text(json.dumps({'schema': 1, 'platform': platform.platform(),
'architecture': platform.machine(), 'minecraft': lab.VERSION,
'original_mod_sha256': original_hash, 'results': results,
'limits': ['Short scene startup and nonuniform framebuffer readback only.',
'World mode uses the ordinary limited demo; world files remain private.',
'No exhaustive gameplay, visual-correctness, FPS or M2 Pro performance claim.',
'Vulkan preference can fall back; inspect the observed backend.',
'PNG images, assets and full logs remain in the private runtime directory.']}, indent=2) + '\n')
print('Client graphics startup/readback checks passed.', flush=True)
if __name__ == '__main__':
main()
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env python3
"""Check live culling whitelist registration and the optional empty-iterator shortcut."""
import argparse
import json
import os
import platform
import shutil
import subprocess
import sys
try:
from . import lab
except ImportError:
import lab
try:
from . import build_mod, build_pack
except ImportError:
import build_mod
import build_pack
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--native-display", action="store_true")
args = parser.parse_args()
root = build_pack.ROOT
fixture = build_mod.build_culling_empty_fixture()
mod = root / "dist" / f"horizon-{build_pack.read_lock()['pack_version']}.jar"
results = []
for enabled in [False, True]:
instance = root / ".local/runtime" / ("culling-empty-fixture-patched" if enabled else "culling-empty-fixture-upstream")
build_pack.prepare_instance("m2-balanced", instance, mod)
shutil.copy2(fixture, instance / "mods/culling-empty-fixture.jar")
(instance / "config/horizon-culling-empty.properties").write_text(f"enabled={str(enabled).lower()}\n")
command = [sys.executable, str(root / "tools/runtime.py"), "demo", "--instance", str(instance), "--heap", "3G", "--compact-headers"]
if platform.system() == "Linux" and not args.native_display:
command = ["xvfb-run", "-a", *command]
environment = os.environ.copy()
environment["_JAVA_OPTIONS"] = "-Dhorizon.fixture.exit=true"
run = subprocess.run(command, cwd=root, env=environment, text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=240)
(instance / "fixture.log").write_text(run.stdout)
marker = "HORIZON_CULLING_EMPTY_RESULT "
reports = [json.loads(line.split(marker, 1)[1]) for line in run.stdout.splitlines() if marker in line]
if run.returncode or len(reports) != 1:
raise RuntimeError(f"Culling empty fixture failed; inspect {instance / 'fixture.log'}")
report = reports[0]
if report["enabled"] != enabled or not report["live_registration_checked"] or not report["block_callback_order_checked"] or not report["type_guard_checked"] or report["result"]:
raise AssertionError("Applied hook or semantic outcome did not match")
results.append(report)
print(json.dumps(report), flush=True)
if [r["empty_iterator_calls"] for r in results] != [1, 0]:
raise AssertionError("Empty shortcut did not apply to actual methods")
output = root / "dist/culling-empty-integration.json"
output.write_text(json.dumps({"schema": 1, "platform": platform.platform(), "architecture": platform.machine(),
"minecraft": lab.VERSION, "entity_culling": "1.11.2", "results": results,
"notes": ["Actual publisher whitelist methods inside Fabric; live registration and block-entity callbacks checked.",
"Nonempty iteration, callback order, short circuit, null/wrong-type inputs and clearing retain upstream behavior.",
"Empty-state iterator counts use a private instrumented set; allocation loop uses normal empty HashSets.",
"Entity callbacks with a valid game entity are not exercised by this menu-only fixture.",
"JIT escape analysis can remove allocations in an isolated loop; real culling-thread profiles are needed.",
"Not a game FPS, whole-game RAM, visual equivalence or Mac GPU benchmark."]}, indent=2) + "\n")
print(f"Applied empty whitelist checks passed: {output}")
if __name__ == "__main__":
main()
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more