From 0a0cd667d5cfdee7c59f5fe2c7a2eb15bd19e66e Mon Sep 17 00:00:00 2001 From: Ludovic Marie <136255825+ludovic111@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:00:07 +0200 Subject: [PATCH] Reuse discarded terrain quads at fresh allocation sites with isolated controls --- .github/workflows/ci.yml | 6 ++ README.md | 9 +- docs/quad-reuse.md | 42 ++++++++ .../dev/horizonlab/client/HorizonClient.java | 1 + .../horizonlab/mixin/DhQuadReuseMixin.java | 31 ++++++ .../horizonlab/mixin/HorizonMixinPlugin.java | 18 ++++ .../src/main/resources/fabric.mod.json | 2 +- .../src/main/resources/horizonlab.mixins.json | 3 +- .../dispatch/QuadReuseIntegration.java | 99 +++++++++++++++++++ packs/START-HERE.txt | 6 ++ packs/mods.lock.json | 2 +- tests/test_packs.py | 19 ++++ tools/Compare-Benchmarks.html | 3 +- tools/build_pack.py | 5 +- tools/compare_benchmarks.py | 2 +- tools/test_quad_reuse.py | 72 ++++++++++++++ 16 files changed, 313 insertions(+), 7 deletions(-) create mode 100644 docs/quad-reuse.md create mode 100644 mods/horizon-lab/src/main/java/dev/horizonlab/mixin/DhQuadReuseMixin.java create mode 100644 mods/horizon-lab/src/test/java/dev/horizonlab/dispatch/QuadReuseIntegration.java create mode 100644 tools/test_quad_reuse.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64062107c..dfb106f3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,8 @@ jobs: 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 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 @@ -89,6 +91,9 @@ jobs: - 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: Verify interrupted DH reads and preserved shared futures if: runner.os == 'Linux' run: python tools/test_terrain_read.py @@ -109,3 +114,4 @@ jobs: dist/terrain-read-integration.json dist/culling-rays-integration.json dist/terrain-position-integration.json + dist/quad-reuse-integration.json diff --git a/README.md b/README.md index 852869d5e..1b88ae004 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Client-first performance research for Minecraft Java **26.3**, preserving vanill ## Play on your Mac -Download **horizon-mac-starter-0.17.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.18.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. @@ -22,6 +22,7 @@ Use native **Java 25 ARM64**, maximum heap **6144 MiB**, minimum **512 MiB** on | 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 | @@ -104,6 +105,12 @@ python3 tools/compare_benchmarks.py baseline-frames.json optimized-frames.json - ``` 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. +Version 0.18 adds an isolated [terrain quad reuse experiment](docs/quad-reuse.md). +It reuses a temporary quad discarded by successful premerge when the existing +pool would otherwise allocate. Generated geometry and thread ownership checks +pass; allocation results are not whole-game RAM or FPS evidence. It remains off +in combined profiles. + Version 0.17 also records loaded mod versions and the Java vendor, VM and runtime build in client and server reports. Both comparison tools flag software changes as explicit experiment variables; older reports remain usable with an unknown diff --git a/docs/quad-reuse.md b/docs/quad-reuse.md new file mode 100644 index 000000000..52d4f5c65 --- /dev/null +++ b/docs/quad-reuse.md @@ -0,0 +1,42 @@ +# Reusing discarded terrain quads + +A private live allocation recording sampled Distant Horizons 3.3.4's +`LodQuadBuilder.getOrCreateBufferQuad` on terrain render-loader threads. The +builder obtains a temporary quad before trying to merge it with the last face. +After a successful adjacent-face premerge, the incoming temporary quad is +unused. The original method returns without retaining it in its existing pool. + +The isolated `quad-reuse` profile keeps at most one such discarded quad per +builder. It supplies that object only at the two fresh-constructor sites in +`getOrCreateBufferQuad`; selection from the publisher's existing pool stays +upstream. A successful merge requires a false error flag on both quads, matching +the constructor's initial error state. The existing setter replaces every other +stored field before use. This preserves the existing pool's ordering and error +flags instead of adding a discarded object to that pool. No geometry list, +merge predicate, sorting, lighting, texture/material identifier, upload budget, +world data or simulation rule is replaced. + +The hook requires exactly DH 3.3.4 and validates both constructor and both +premerge call sites. It is off in every other profile, including combined +profiles. Disable `enabled` in `config/horizon-quad-reuse.properties` and restart +to compare with the same profile. Reports capture the actual applied flag. + +The applied Fabric fixture generates 55,296 input faces spanning all directions, +opaque/translucent colors, light levels and material/tile identifiers. It checks +all stored quad fields before and after upstream merging, including sort keys +and error flags. A deliberately poisoned existing-pool object checks that its +state stays upstream. Identity checks reject aliases in live geometry lists. +Four concurrent builder workers compare geometry and reject shared live quads. +These are generated geometry checks, not rendered-pixel comparison on each GPU. + +Private exploratory loops measured about 145 MB caller allocation upstream and +2 MB with reuse over 3,072,000 faces. The same reduction appeared in the initial +production-hook fixture. CPU observations varied: one private pair favored the +hook, another favored upstream, and the first production pair favored the hook. +Fixed-order sequential observations do not establish a statistical speedup. +The fixture favors merging and does not represent every terrain workload. + +The hook adds a reference and can retain one discarded object per builder. +Lower allocation is not a measurement of lower retained heap, process RSS, +unified memory, game FPS or Apple Silicon performance. Live startup, capture, +save/reload and independent platform checks are required before recommending it. diff --git a/mods/horizon-lab/src/main/java/dev/horizonlab/client/HorizonClient.java b/mods/horizon-lab/src/main/java/dev/horizonlab/client/HorizonClient.java index 3a0d51b14..0b0c55436 100644 --- a/mods/horizon-lab/src/main/java/dev/horizonlab/client/HorizonClient.java +++ b/mods/horizon-lab/src/main/java/dev/horizonlab/client/HorizonClient.java @@ -97,6 +97,7 @@ 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("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()); diff --git a/mods/horizon-lab/src/main/java/dev/horizonlab/mixin/DhQuadReuseMixin.java b/mods/horizon-lab/src/main/java/dev/horizonlab/mixin/DhQuadReuseMixin.java new file mode 100644 index 000000000..817b198c0 --- /dev/null +++ b/mods/horizon-lab/src/main/java/dev/horizonlab/mixin/DhQuadReuseMixin.java @@ -0,0 +1,31 @@ +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; + } +} diff --git a/mods/horizon-lab/src/main/java/dev/horizonlab/mixin/HorizonMixinPlugin.java b/mods/horizon-lab/src/main/java/dev/horizonlab/mixin/HorizonMixinPlugin.java index 8df59f7dd..86531ea97 100644 --- a/mods/horizon-lab/src/main/java/dev/horizonlab/mixin/HorizonMixinPlugin.java +++ b/mods/horizon-lab/src/main/java/dev/horizonlab/mixin/HorizonMixinPlugin.java @@ -12,6 +12,7 @@ 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; @@ -146,8 +147,24 @@ public final class HorizonMixinPlugin implements IMixinConfigPlugin { } 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; } @@ -165,6 +182,7 @@ public final class HorizonMixinPlugin implements IMixinConfigPlugin { 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; } diff --git a/mods/horizon-lab/src/main/resources/fabric.mod.json b/mods/horizon-lab/src/main/resources/fabric.mod.json index b24b419a8..3dd48296a 100644 --- a/mods/horizon-lab/src/main/resources/fabric.mod.json +++ b/mods/horizon-lab/src/main/resources/fabric.mod.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "id": "horizonlab", - "version": "0.17.0", + "version": "0.18.0", "name": "Horizon Lab", "description": "Client frame pacing and server tick measurements for reproducible optimization experiments.", "authors": [ diff --git a/mods/horizon-lab/src/main/resources/horizonlab.mixins.json b/mods/horizon-lab/src/main/resources/horizonlab.mixins.json index ad486c33c..8b6602b53 100644 --- a/mods/horizon-lab/src/main/resources/horizonlab.mixins.json +++ b/mods/horizon-lab/src/main/resources/horizonlab.mixins.json @@ -15,7 +15,8 @@ "CullingEmptyWhitelistMixin", "DhTerrainReadWaitMixin", "CullingRayScratchMixin", - "DhRelativePositionMixin" + "DhRelativePositionMixin", + "DhQuadReuseMixin" ], "injectors": { "defaultRequire": 1 diff --git a/mods/horizon-lab/src/test/java/dev/horizonlab/dispatch/QuadReuseIntegration.java b/mods/horizon-lab/src/test/java/dev/horizonlab/dispatch/QuadReuseIntegration.java new file mode 100644 index 000000000..3cde5b6de --- /dev/null +++ b/mods/horizon-lab/src/test/java/dev/horizonlab/dispatch/QuadReuseIntegration.java @@ -0,0 +1,99 @@ +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(); + 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[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()); + var barrier=new java.util.concurrent.CyclicBarrier(4); + var futures=new ArrayList>(); + 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)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+"}"); + } +} diff --git a/packs/START-HERE.txt b/packs/START-HERE.txt index 51640b0e3..ab5e5222b 100644 --- a/packs/START-HERE.txt +++ b/packs/START-HERE.txt @@ -66,6 +66,12 @@ PROFILES 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 diff --git a/packs/mods.lock.json b/packs/mods.lock.json index b610e0ed2..feb383d7d 100644 --- a/packs/mods.lock.json +++ b/packs/mods.lock.json @@ -3,7 +3,7 @@ "minecraft": "26.3", "java_major": 25, "fabric_loader": "0.19.5", - "pack_version": "0.17.0", + "pack_version": "0.18.0", "options_data_version": 5023, "projects": { "sodium": { diff --git a/tests/test_packs.py b/tests/test_packs.py index a4d7a6b27..21df70858 100644 --- a/tests/test_packs.py +++ b/tests/test_packs.py @@ -123,6 +123,16 @@ class PackTests(unittest.TestCase): "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") @@ -209,6 +219,15 @@ class ComparisonTests(unittest.TestCase): 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=["minecraft@26.3", "horizonlab@0.16.0", "sodium@0.9.2"], diff --git a/tools/Compare-Benchmarks.html b/tools/Compare-Benchmarks.html index 45a031780..ff7ac9300 100644 --- a/tools/Compare-Benchmarks.html +++ b/tools/Compare-Benchmarks.html @@ -84,7 +84,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','empty_culling_whitelist','culling_ray_scratch','terrain_position_arithmetic','interruptible_terrain_read','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','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]]){ @@ -145,6 +145,7 @@ function renderDetails(side){ 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']); diff --git a/tools/build_pack.py b/tools/build_pack.py index c6e22aa4f..67f4f14be 100644 --- a/tools/build_pack.py +++ b/tools/build_pack.py @@ -26,6 +26,7 @@ PROFILES = { "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}, @@ -83,7 +84,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", "culling_empty", "culling_rays", "terrain_position"]) + 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() @@ -99,6 +100,8 @@ def overrides(profile_name, mod_bytes): 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"] = ( diff --git a/tools/compare_benchmarks.py b/tools/compare_benchmarks.py index 6a4db9413..10f58a9ef 100644 --- a/tools/compare_benchmarks.py +++ b/tools/compare_benchmarks.py @@ -52,7 +52,7 @@ def compare(baseline, candidate): warnings.extend(software_warnings(baseline, candidate)) 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", "interruptible_terrain_read", "dh_thread_count", "dh_runtime_ratio"]: + "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)]: diff --git a/tools/test_quad_reuse.py b/tools/test_quad_reuse.py new file mode 100644 index 000000000..e4ed5d995 --- /dev/null +++ b/tools/test_quad_reuse.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Compare actual terrain quad geometry and allocation with discarded-quad reuse.""" + +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 + + +try: + from .test_culling_rays import run_fixture +except ImportError: + from test_culling_rays import run_fixture + + +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("quad-reuse", "dev.horizonlab.dispatch.QuadReuseIntegration") + 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" / ("quad-reuse-fixture-patched" if enabled else "quad-reuse-fixture-upstream") + build_pack.prepare_instance("m2-balanced", instance, mod) + if hashlib.sha256((instance / "mods" / mod.name).read_bytes()).hexdigest() != original_sha256: + raise AssertionError("Fixture runtime mod does not match the intended build") + shutil.copy2(fixture, instance / "mods/quad-reuse-fixture.jar") + (instance / "config/horizon-quad-reuse.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 = run_fixture(command, instance, environment) + marker = "HORIZON_QUAD_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"Quad reuse fixture failed; inspect {instance / 'fixture.log'}") + report = reports[0] + if report["enabled"] != enabled or report["input_faces"] != 55296 or report["measured_faces"] != 3072000 or report["thread_workers"] != 4 or not report["applied_hook_checked"]: + raise AssertionError("Applied quad reuse fixture did not complete its geometry checks") + results.append(report) + print(json.dumps(report), flush=True) + for key in ["geometry_digest", "geometry_quads", "input_faces", "checksum", "thread_geometry_digest"]: + if results[0][key] != results[1][key]: + raise AssertionError(f"Quad reuse changed upstream geometry: {key}") + output = root / "dist/quad-reuse-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 publisher quad builder inside Fabric with generated faces and lighting/material values.", + "Opaque/translucent geometry, pooled error flags and every retained quad field match before and after upstream merging.", + "Fresh allocations reuse at most one discarded successful-premerge quad; existing pool selection stays upstream.", + "Three sequential warmed caller-loop observations are not independent forks; compilation can vary.", + "Allocation reduction is not retained RAM, game FPS or a Mac GPU benchmark."]}, indent=2) + "\n") + print(f"Applied terrain quad reuse checks passed: {output}") + + +if __name__ == "__main__": + main()