Merge pull request #15 from ludovic111/culling-empty-whitelists

Reduce empty culling allocations and release interrupted DH reads
This commit is contained in:
Ludovic Marie
2026-10-04 03:11:11 +02:00
committed by GitHub
25 changed files with 809 additions and 12 deletions
+12
View File
@@ -63,6 +63,10 @@ jobs:
run: python -c "from tools.build_mod import build_dispatch_fixture; build_dispatch_fixture()"
- 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 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
@@ -72,6 +76,12 @@ jobs:
- 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: 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:
@@ -85,3 +95,5 @@ jobs:
dist/object-header-benchmark.txt
dist/dispatch-integration.json
dist/tick-dedup-integration.json
dist/culling-empty-integration.json
dist/terrain-read-integration.json
+9 -1
View File
@@ -6,7 +6,7 @@ Client-first performance research for Minecraft Java **26.3**, preserving vanill
## Play on your Mac
Download **horizon-mac-starter-0.13.0.zip** from [Releases](https://github.com/ludovic111/minecraft-optimization-lab/releases), 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.
Download **horizon-mac-starter-0.14.0.zip** from [Releases](https://github.com/ludovic111/minecraft-optimization-lab/releases), 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 your 16 GB M2 Pro. 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.
@@ -21,6 +21,7 @@ Use native **Java 25 ARM64**, maximum heap **6144 MiB**, minimum **512 MiB** on
| 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 |
| foreground-first (combined experiment) | 12 | 128 | 6144 MiB |
| foreground-vulkan (combined experiment) | 12 | 128 | 6144 MiB |
| low-memory | 8 | none | 3072 MiB |
@@ -34,6 +35,13 @@ Distant LOD is simplified terrain. Nearby blocks retain full detail. Initial gen
The optional `queue-wait` pack yields during pending distant-terrain debounce retries; `adaptive-queue` combines it with the existing controllers. [Queue experiment and measurement limits](docs/debounce-queue.md).
Version 0.14 adds an isolated [empty culling whitelist experiment](docs/culling-empty.md)
that avoids allocating iterators while there are no dynamic handlers. Live
registration and nonempty callbacks retain upstream behavior. Balanced keeps it
off; whole-game gains still need hardware comparisons. DH profiles also request
a scoped [interruptible blending-read wait](docs/terrain-read-wait.md) to let
background workers leave a pending metadata read during shutdown.
Version 0.13 adds an isolated [tick membership experiment](docs/tick-membership.md)
that reuses compact storage for DH's private per-tick membership checks. The
combined foreground profiles include it too; balanced keeps it off. The applied
@@ -0,0 +1,47 @@
{
"schema": 1,
"platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39",
"architecture": "x86_64",
"minecraft": "26.3",
"entity_culling": "1.11.2",
"results": [
{
"enabled": false,
"empty_iterator_calls": 1,
"live_registration_checked": true,
"block_callback_order_checked": true,
"type_guard_checked": true,
"allocated_bytes": 128001272,
"cpu_ns": 29632156,
"wall_ns": 49712014,
"measured_calls": 4000000,
"result": false
},
{
"enabled": true,
"empty_iterator_calls": 0,
"live_registration_checked": true,
"block_callback_order_checked": true,
"type_guard_checked": true,
"allocated_bytes": 208,
"cpu_ns": 809881,
"wall_ns": 801517,
"measured_calls": 4000000,
"result": false
}
],
"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.",
"Much of an empty warmed loop may be folded by the JIT; CPU timings are not per-check costs or game speed estimates."
],
"provenance": {
"source_commit": "35dd69e9a6279fa89d762a0d24e49b0dbdb11b51",
"ci_run": "https://github.com/ludovic111/minecraft-optimization-lab/actions/runs/37164002916",
"artifact": "horizon-ubuntu-latest"
}
}
@@ -0,0 +1,43 @@
{
"schema": 1,
"platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39",
"architecture": "x86_64",
"minecraft": "26.3",
"distant_horizons": "3.3.4",
"original_mod_sha256": "c506982631880604513de8ef85e3b4461fbb898702e60b4a6f74ca3b3d4a687f",
"results": [
{
"enabled": false,
"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
},
{
"enabled": true,
"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
}
],
"notes": [
"Actual game IOWorker under Fabric, with controlled cached blending-read futures.",
"DH worker interruption releases only its waiter; the shared future remains consumable.",
"Ordinary callers retain join behavior; completed values, cancellation and exception identity are checked.",
"This is a shutdown correctness fixture, not an FPS or world-generation throughput benchmark."
],
"provenance": {
"source_commit": "9a1b22aa56f58a3ee39a3564be859a662d2beedc",
"workflow_run": "https://github.com/ludovic111/minecraft-optimization-lab/actions/runs/37166150119",
"job": "portable-mod (ubuntu-latest)",
"artifact": "horizon-ubuntu-latest",
"command": "python tools/test_terrain_read.py"
}
}
@@ -0,0 +1,41 @@
{
"schema": 1,
"platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43",
"architecture": "x86_64",
"minecraft": "26.3",
"distant_horizons": "3.3.4",
"original_mod_sha256": "c506982631880604513de8ef85e3b4461fbb898702e60b4a6f74ca3b3d4a687f",
"results": [
{
"enabled": false,
"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
},
{
"enabled": true,
"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
}
],
"notes": [
"Actual game IOWorker under Fabric, with controlled cached blending-read futures.",
"DH worker interruption releases only its waiter; the shared future remains consumable.",
"Ordinary callers retain join behavior; completed values, cancellation and exception identity are checked.",
"This is a shutdown correctness fixture, not an FPS or world-generation throughput benchmark."
],
"provenance": {
"source_commit": "9a1b22aa56f58a3ee39a3564be859a662d2beedc",
"environment": "local Linux x64 software graphics; actual Fabric fixture",
"command": "python tools/test_terrain_read.py"
}
}
+58
View File
@@ -0,0 +1,58 @@
# Avoid allocating iterators for empty culling whitelists
A local game JFR found `HashMap.KeySet` iterator allocation in Entity Culling's
dynamic entity/block-entity whitelist checks on `CullThread`. The pinned 1.11.2
methods create a set iterator before discovering that there are no handlers.
The optional hook returns the JDK's shared empty iterator while the live set is
empty. It checks that set every call; it does not cache a whitelist, remember an
empty state, skip a registered handler, or alter culling decisions. Nonempty
sets use their original iterator, callback order, type guard and short circuit.
No per-call callback object is introduced by the hook.
Use the `culling-empty` profile for an isolated comparison with `m2-balanced`.
Both keep the same graphics, LOD radius, worker budget and other controller
settings. The hook is opt-in, restricted to Entity Culling 1.11.2, and can be
disabled with `enabled=false` in `config/horizon-culling-empty.properties` and a
restart. Frame reports record `empty_culling_whitelist`; the CLI and offline
viewer disclose a difference as an experiment variable.
`python tools/test_culling_empty.py` runs the actual publisher methods inside
Fabric. It checks empty iterator dispatch, registration after initialization,
duplicate registration, real block-entity callback order/short circuit, null and
wrong-type inputs, and returning to an empty state. The menu-only fixture does
not exercise callbacks using a valid world entity; nonempty entity iteration
uses the original implementation. Private instrumentation and fixture JARs
never enter release packs. Full game logs remain private.
A local warmed loop of four million empty checks allocated 128,001,248 bytes
upstream versus 1,248 with the hook. CPU was 41 ms versus 33 ms in that single
observation. These counts isolate this allocation path; JIT escape analysis can
produce different results in other calling contexts. They are not game FPS,
whole-game RAM, statistical speed estimates or Mac GPU results. Repeated matching workloads and real hardware tests remain needed. A fixed-order
Linux software-rendered pair observed about 97,445 versus 35,128 allocated bytes
per second on `CullThread`, over separate 64-second counter spans. The captured
graphics matched and both recordings excluded zero frames, but this one pair is
not a repeatable FPS or retained-RAM result. The candidate also encountered an
intermittent shutdown wait after its world had saved; the control exited normally.
See the [terrain read shutdown check](terrain-read-wait.md).
For local world smoke checks, the baseline and candidate use separate copies of
the same peaceful demo-world snapshot. An earlier capture was interrupted by a
zombie killing the player and is excluded from performance comparisons. Release
packs do not change world difficulty. Frame reports now capture actual world
difficulty at their start/end boundaries so comparison tools can flag a mismatch.
The [sanitized Linux CI result](../benchmarks/culling-empty-2026-10-04/linux-applied-hooks.json)
includes its source/workflow provenance and independently checks live registration
and the applied shortcut. CPU timings can include JIT folding of this empty loop;
they are not per-check costs or estimates of game speed.
The final candidate JAR (`c506982631880604513de8ef85e3b4461fbb898702e60b4a6f74ca3b3d4a687f`)
then recorded 579 frame intervals and 2,408 integrated-server tick durations over
120 seconds, with zero excluded frames and matching CSV counts/means. Its actual
frame report retained custom graphics, render/simulation distances 12/12, DH radius
128 at MEDIUM/MEDIUM quality, two workers at runtime ratio 0.5, compact headers,
and both the culling shortcut and scoped terrain-read fix enabled. No captured
visual setting changed. This verifies integration; it is not a repeated hardware
performance comparison.
+50
View File
@@ -0,0 +1,50 @@
# Release a blocked terrain read during shutdown
A local world test saved its chunks and closed the DH databases, then hit the
client shutdown watchdog. Its remaining non-daemon DH world-generation worker
was waiting in `IOWorker.isOldChunkAround` on a cached blending-metadata future.
The unchanged control exited normally, so the failure is intermittent; this
observation does not attribute it to the culling optimization.
The scoped redirect uses an interruptible wait only for pending blending reads
on threads named `DH-World Gen Thread[...]`. The pinned DH 3.3.4 pool uses this
name and interrupts remaining workers during shutdown. Ordinary Minecraft
callers keep `CompletableFuture.join` behavior. Completed results use the original
join path. Failed reads retain join's exception identity and wrapping behavior.
An interrupted DH waiter restores its interrupt flag and throws a completion
exception whose interrupted cause is recognized by DH's existing shutdown
handling. It never cancels or completes the cached future, which belongs to other
callers too. Chunk stores, saves, storage closure and world difficulty are untouched.
This changes cancellation of an interrupted background task, rather than terrain
results from an uninterrupted task.
Profiles with distant terrain request the fix; baseline and low-memory profiles
without DH do not. It is restricted to Minecraft 26.3 and DH 3.3.4. To opt out,
set `enabled=false` in `config/horizon-terrain-read-wait.properties` and restart.
Frame reports record `interruptible_terrain_read`, and comparison tools disclose
changes in that state.
`python tools/test_terrain_read.py` checks the actual transformed game IOWorker
inside Fabric, with controlled cached metadata futures. It forces an interrupted
pending DH read, verifies the shared future remains available to a later caller,
and checks that ordinary callers remain blocked until completion. It also
checks an interrupt already set before a pending read, successful completed
reads with an interrupt already set, cancelled
futures, exceptional completion identity, and recognition by DH's shutdown
exception utility. The private fixture does not enter release packs. This is a
correctness check, not an FPS, throughput or Mac shutdown benchmark.
The [sanitized Linux CI result](../benchmarks/terrain-read-2026-10-04/linux-applied-hooks.json)
includes source/workflow provenance and the original mod checksum. The CI JAR
matches the local candidate byte-for-byte. Windows and hosted Apple Silicon
macOS 26 compile the fixture and pass the portable build/metrics checks; applied
read interruption is exercised on Linux, not a live macOS game.
The exact candidate also completed a 120-second local world recording, saved,
and reloaded that world in the same client. After the first save, thread inspection
found no remaining DH generation workers and no background workers blocked on a
future. After reload, two generation workers existed and neither was waiting in
blending metadata; no background metadata-scan wait was present at that snapshot.
This is one software-rendered lifecycle observation, not proof that every
shutdown race, world or platform is covered.
@@ -0,0 +1,27 @@
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();
}
}
}
@@ -97,6 +97,8 @@ public final class HorizonClient implements ClientModInitializer {
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("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();
@@ -115,6 +117,7 @@ public final class HorizonClient implements ClientModInitializer {
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());
@@ -0,0 +1,19 @@
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();
}
}
@@ -0,0 +1,18 @@
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);
}
}
@@ -16,6 +16,8 @@ public final class HorizonMixinPlugin implements IMixinConfigPlugin {
private static boolean debounceWait;
private static boolean dispatchRetry;
private static boolean tickDedup;
private static boolean cullingEmpty;
private static boolean terrainReadWait;
@Override public void onLoad(String mixinPackage) {
var loader = FabricLoader.getInstance();
@@ -79,17 +81,54 @@ public final class HorizonMixinPlugin implements IMixinConfigPlugin {
} 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);
}
}
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 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("DhTerrainReadWaitMixin")) return terrainReadWait;
return true;
}
@Override public String getRefMapperConfig() { return null; }
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"id": "horizonlab",
"version": "0.13.0",
"version": "0.14.0",
"name": "Horizon Lab",
"description": "Client frame pacing and server tick measurements for reproducible optimization experiments.",
"authors": [
@@ -4,6 +4,6 @@
"plugin": "dev.horizonlab.mixin.HorizonMixinPlugin",
"compatibilityLevel": "JAVA_25",
"mixins": ["ServerTickMixin"],
"client": ["FrameMixin", "DhWorkerPacingMixin", "DhDebounceWaitMixin", "DhPriorityDispatchMixin", "DhTickDedupMixin"],
"client": ["FrameMixin", "DhWorkerPacingMixin", "DhDebounceWaitMixin", "DhPriorityDispatchMixin", "DhTickDedupMixin", "CullingEmptyWhitelistMixin", "DhTerrainReadWaitMixin"],
"injectors": {"defaultRequire": 1}
}
@@ -0,0 +1,100 @@
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;
}
}
@@ -0,0 +1,155 @@
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); }
}
}
+10
View File
@@ -62,6 +62,10 @@ PROFILES
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.
- 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
@@ -154,3 +158,9 @@ 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.
+1 -1
View File
@@ -3,7 +3,7 @@
"minecraft": "26.3",
"java_major": 25,
"fabric_loader": "0.19.5",
"pack_version": "0.13.0",
"pack_version": "0.14.0",
"options_data_version": 5023,
"projects": {
"sodium": {
+21 -2
View File
@@ -87,6 +87,24 @@ class PackTests(unittest.TestCase):
"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_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())
@@ -173,13 +191,14 @@ class ComparisonTests(unittest.TestCase):
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)
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, visual_settings_changed=["render_distance"])
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()
+5 -2
View File
@@ -30,7 +30,7 @@
</main>
<script>
'use strict';
const SETTINGS=['minecraft','kind','os','os_version','architecture','gpu','graphics_backend','graphics_driver','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','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 SETTINGS=['minecraft','kind','os','os_version','architecture','gpu','graphics_backend','graphics_driver','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);
@@ -66,7 +66,7 @@ function compareReports(a,b){
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','dh_thread_count','dh_runtime_ratio']){
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','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]]){
@@ -119,12 +119,15 @@ function renderDetails(side){
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(['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));
}
async function loadFiles(side,files){
+11 -1
View File
@@ -126,10 +126,20 @@ def build_tick_dedup_fixture():
return build_integration_fixture("tick-dedup", "dev.horizonlab.dispatch.TickDedupIntegration")
def build_integration_fixture(name, entrypoint):
def build_culling_empty_fixture():
return build_integration_fixture("culling-empty", "dev.horizonlab.dispatch.CullingEmptyIntegration", ["entityculling"])
def build_integration_fixture(name, entrypoint, extra_projects=()):
"""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)
+6 -1
View File
@@ -25,6 +25,7 @@ PROFILES = {
"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},
"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},
@@ -80,7 +81,7 @@ def overrides(profile_name, mod_bytes):
files = {f"mods/horizon-lab-{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"])
experiment = any(profile.get(key, False) for key in ["adaptive", "pacing", "queue_wait", "dispatch_retry", "tick_dedup", "culling_empty"])
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()
@@ -92,6 +93,10 @@ def overrides(profile_name, mod_bytes):
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-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}")
+2 -2
View File
@@ -9,7 +9,7 @@ MATCH_FIELDS = ("minecraft", "kind", "os", "os_version", "architecture", "gpu",
"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", "dh_capture_status", "dh_rendering_enabled", "dh_renderer_mode",
"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")
@@ -28,7 +28,7 @@ def compare(baseline, candidate):
warnings = ["World, route, warm-up, shader state and LOD cache must also match; these are not verified by this tool."]
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", "dh_thread_count", "dh_runtime_ratio"]:
"wakeable_idle_wait", "dispatch_retry_backoff", "tick_membership_dedup", "empty_culling_whitelist", "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)]:
+64
View File
@@ -0,0 +1,64 @@
#!/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 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-lab-{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": "26.3", "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()
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Exercise interruptible blending reads on an actual transformed Minecraft IOWorker."""
import argparse
import hashlib
import json
import os
import platform
import shutil
import subprocess
import sys
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_integration_fixture("terrain-read", "dev.horizonlab.dispatch.TerrainReadIntegration")
mod = root / "dist" / f"horizon-lab-{build_pack.read_lock()['pack_version']}.jar"
original_sha256 = hashlib.sha256(mod.read_bytes()).hexdigest()
results = []
for enabled in [False, True]:
instance = root / ".local/runtime" / ("terrain-read-fixture-patched" if enabled else "terrain-read-fixture-upstream")
build_pack.prepare_instance("m2-balanced", instance, mod)
if hashlib.sha256((instance / "mods" / mod.name).read_bytes()).hexdigest() != original_sha256:
raise AssertionError("Runtime mod does not match the intended build")
shutil.copy2(fixture, instance / "mods/terrain-read-fixture.jar")
(instance / "config/horizon-terrain-read-wait.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_TERRAIN_READ_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"Terrain read fixture failed; inspect {instance / 'fixture.log'}")
report = reports[0]
if report["enabled"] != enabled or not all(report.get(key) for key in [
"applied_io_worker_checked", "ordinary_join_checked", "shared_future_preserved",
"exception_identity_checked", "completed_interrupt_checked", "cancelled_completion_checked", "preinterrupted_pending_checked"]):
raise AssertionError("Applied terrain read hook did not preserve the required outcomes")
results.append(report)
print(json.dumps(report), flush=True)
output = root / "dist/terrain-read-integration.json"
output.write_text(json.dumps({"schema": 1, "platform": platform.platform(), "architecture": platform.machine(),
"minecraft": "26.3", "distant_horizons": "3.3.4", "original_mod_sha256": original_sha256, "results": results,
"notes": ["Actual game IOWorker under Fabric, with controlled cached blending-read futures.",
"DH worker interruption releases only its waiter; the shared future remains consumable.",
"Ordinary callers retain join behavior; completed values, cancellation and exception identity are checked.",
"This is a shutdown correctness fixture, not an FPS or world-generation throughput benchmark."]}, indent=2) + "\n")
print(f"Applied terrain read checks passed: {output}")
if __name__ == "__main__":
main()