From 750ee87c6b9b960dfa251e805b87ab5456b1ae70 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 13 Oct 2026 13:56:26 -0400 Subject: [PATCH] Fixed distant terrain bounds - Shared terrain bounds retain full coordinates and interpolator identities - Cache regressions cover distant corners and concurrent replacement --- .../iris/generation/runtime/IrisComplex.java | 6 +- .../runtime/SharedCornerBounds.java | 59 ++++----- .../IrisComplexGridBoundsCacheTest.java | 17 +++ .../runtime/SharedCornerBoundsTest.java | 120 ++++++++++++++---- 4 files changed, 135 insertions(+), 67 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index a57ca67b3..0204af6fe 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -1685,8 +1685,8 @@ public class IrisComplex implements DataProvider { // The per-thread table above catches the walk through one chunk; the shared table // catches the same corner reached by another thread or by hydrology planning earlier. SharedCornerBounds shared = sharedCornerBounds(); - long sharedKey = SharedCornerBounds.key(gx, gz, interpolatorIndex); - long packed = shared.get(sharedKey); + long sharedKey = SharedCornerBounds.key(gx, gz); + long packed = shared.get(sharedKey, interpolatorIndex); if (packed == Long.MIN_VALUE) { packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz); if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) { @@ -1694,7 +1694,7 @@ public class IrisComplex implements DataProvider { // hand it back uncached so the next sample recomputes it instead of pinning it. return packed; } - shared.put(sharedKey, packed); + shared.put(sharedKey, interpolatorIndex, packed); } cache.gx[slot] = gx; cache.gz[slot] = gz; diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/SharedCornerBounds.java b/core/src/main/java/art/arcane/iris/generation/runtime/SharedCornerBounds.java index c7cd2bef2..b8b21d50a 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/SharedCornerBounds.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/SharedCornerBounds.java @@ -11,13 +11,12 @@ import java.lang.invoke.VarHandle; * it to even; readers reject a slot whose stamp changed or was odd while they read it. */ final class SharedCornerBounds { - private static final VarHandle STAMPS = MethodHandles.arrayElementVarHandle(int[].class); + private static final VarHandle INTS = MethodHandles.arrayElementVarHandle(int[].class); private static final VarHandle LONGS = MethodHandles.arrayElementVarHandle(long[].class); - private static final long PRESENT = 1L << 63; - private static final long COORDINATE_MASK = (1L << 21) - 1; private final long[] keys; private final long[] values; + private final int[] interpolators; private final int[] stamps; private final int mask; @@ -27,70 +26,58 @@ final class SharedCornerBounds { } this.keys = new long[capacity]; this.values = new long[capacity]; + this.interpolators = new int[capacity]; this.stamps = new int[capacity]; this.mask = capacity - 1; } - /** Never zero, so an untouched slot can never match a real corner. */ - static long key(int cornerX, int cornerZ, int interpolatorIndex) { - return PRESENT - | ((cornerX & COORDINATE_MASK) << 42) - | ((cornerZ & COORDINATE_MASK) << 21) - | (interpolatorIndex & COORDINATE_MASK); + static long key(int cornerX, int cornerZ) { + return ((long) cornerX << 32) | (cornerZ & 0xFFFFFFFFL); } - int slot(long key) { - long mixed = (key ^ (key >>> 30)) * 0xBF58476D1CE4E5B9L; + int slot(long key, int interpolatorIndex) { + long mixed = key ^ (0x9E3779B97F4A7C15L * interpolatorIndex); + mixed = (mixed ^ (mixed >>> 30)) * 0xBF58476D1CE4E5B9L; mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL; return (int) ((mixed ^ (mixed >>> 31)) & mask); } - boolean contains(long key) { - int slot = slot(key); - int before = (int) STAMPS.getAcquire(stamps, slot); - if ((before & 1) != 0) { - return false; - } - long storedKey = (long) LONGS.getAcquire(keys, slot); - VarHandle.loadLoadFence(); - int after = (int) STAMPS.getAcquire(stamps, slot); - return before == after && storedKey == key; - } - /** * The value stored for the key, or {@link Long#MIN_VALUE} when the slot holds another corner - * or is being written. Callers check {@link #contains(long)} first; a value that legitimately - * equals the sentinel is recomputed, which is only a wasted starcast. + * or is being written. A value that legitimately equals the sentinel is recomputed, which is + * only a wasted starcast. */ - long get(long key) { - int slot = slot(key); - int before = (int) STAMPS.getAcquire(stamps, slot); - if ((before & 1) != 0) { + long get(long key, int interpolatorIndex) { + int slot = slot(key, interpolatorIndex); + int before = (int) INTS.getAcquire(stamps, slot); + if (before == 0 || (before & 1) != 0) { return Long.MIN_VALUE; } long storedKey = (long) LONGS.getAcquire(keys, slot); + int storedInterpolator = (int) INTS.getAcquire(interpolators, slot); long value = (long) LONGS.getAcquire(values, slot); // The data loads above must not drift past the second stamp read, or a torn slot could pass. VarHandle.loadLoadFence(); - int after = (int) STAMPS.getAcquire(stamps, slot); - if (before != after || storedKey != key) { + int after = (int) INTS.getAcquire(stamps, slot); + if (before != after || storedKey != key || storedInterpolator != interpolatorIndex) { return Long.MIN_VALUE; } return value; } - void put(long key, long value) { - int slot = slot(key); - int stamp = (int) STAMPS.getAcquire(stamps, slot); + void put(long key, int interpolatorIndex, long value) { + int slot = slot(key, interpolatorIndex); + int stamp = (int) INTS.getAcquire(stamps, slot); if ((stamp & 1) != 0) { // Another writer owns the slot; the corner is recomputed by whoever misses next. return; } - if (!STAMPS.compareAndSet(stamps, slot, stamp, stamp + 1)) { + if (!INTS.compareAndSet(stamps, slot, stamp, stamp + 1)) { return; } LONGS.setRelease(keys, slot, key); + INTS.setRelease(interpolators, slot, interpolatorIndex); LONGS.setRelease(values, slot, value); - STAMPS.setRelease(stamps, slot, stamp + 2); + INTS.setRelease(stamps, slot, stamp + 2); } } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexGridBoundsCacheTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexGridBoundsCacheTest.java index 7efa384c0..3f4b7b36b 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexGridBoundsCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexGridBoundsCacheTest.java @@ -88,6 +88,23 @@ public class IrisComplexGridBoundsCacheTest { assertEquals(1, interpolator.getInvocations()); } + @Test + public void distantCornersKeepTheirOwnBounds() throws Exception { + IrisComplex complex = createComplex(); + Method cornerBounds = cornerBoundsMethod(); + CoordinateInterpolator interpolator = new CoordinateInterpolator(); + int[][] coordinates = {{64, -32}, {64 + (1 << 21), -32}, {64 - (1 << 21), -32}, + {64, -32 + (1 << 21)}, {64, -32 - (1 << 21)}}; + + for (int[] coordinate : coordinates) { + long packed = invokeCornerBounds(complex, cornerBounds, interpolator, + coordinate[0], coordinate[1]); + assertBoundsBitsEqual(packedCoordinateBounds(coordinate[0], coordinate[1]), + new NoiseBounds(unpackLow(packed), unpackHigh(packed))); + } + assertEquals(coordinates.length, interpolator.getInvocations()); + } + @Test public void nonFiniteCornerBoundsAreNeverCached() throws Exception { IrisComplex complex = createComplex(); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/SharedCornerBoundsTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/SharedCornerBoundsTest.java index 1309a133c..7c1149ae3 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/SharedCornerBoundsTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/SharedCornerBoundsTest.java @@ -4,7 +4,9 @@ import org.junit.Test; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -15,65 +17,121 @@ public class SharedCornerBoundsTest { @Test public void storedCornersComeBackAndUnknownCornersMiss() { SharedCornerBounds cache = new SharedCornerBounds(1 << 10); - long key = SharedCornerBounds.key(12, -40, 1); + long key = SharedCornerBounds.key(12, -40); - assertFalse(cache.contains(key)); - cache.put(key, 0x1234_5678_9ABC_DEF0L); + assertEquals(Long.MIN_VALUE, cache.get(key, 1)); + cache.put(key, 1, 0x1234_5678_9ABC_DEF0L); - assertTrue(cache.contains(key)); - assertEquals(0x1234_5678_9ABC_DEF0L, cache.get(key)); - assertFalse(cache.contains(SharedCornerBounds.key(12, -40, 0))); + assertEquals(0x1234_5678_9ABC_DEF0L, cache.get(key, 1)); + assertEquals(Long.MIN_VALUE, cache.get(key, 0)); } @Test - public void keysSeparateCoordinatesAndInterpolators() { - assertNotEquals(SharedCornerBounds.key(1, 2, 0), SharedCornerBounds.key(2, 1, 0)); - assertNotEquals(SharedCornerBounds.key(1, 2, 0), SharedCornerBounds.key(1, 2, 1)); - assertNotEquals(SharedCornerBounds.key(-1, 2, 0), SharedCornerBounds.key(1, 2, 0)); - assertNotEquals(0L, SharedCornerBounds.key(0, 0, 0)); + public void zeroKeyMissesUntilTheOriginIsStored() { + SharedCornerBounds cache = new SharedCornerBounds(2); + long key = SharedCornerBounds.key(0, 0); + + assertEquals(0L, key); + assertEquals(Long.MIN_VALUE, cache.get(key, 0)); + cache.put(key, 0, 73L); + assertEquals(73L, cache.get(key, 0)); + } + + @Test + public void fullCoordinateDomainKeepsDistinctCorners() { + int[] coordinates = {0, 1, -1, 1 << 21, -(1 << 21), 30_000_000, -30_000_000, + Integer.MIN_VALUE, Integer.MAX_VALUE}; + for (int x : coordinates) { + for (int z : coordinates) { + SharedCornerBounds cache = new SharedCornerBounds(2); + long key = SharedCornerBounds.key(x, z); + cache.put(key, 0, 73L); + for (int otherX : coordinates) { + for (int otherZ : coordinates) { + long otherKey = SharedCornerBounds.key(otherX, otherZ); + if (x == otherX && z == otherZ) { + assertEquals(73L, cache.get(otherKey, 0)); + } else { + assertNotEquals(key, otherKey); + assertEquals(Long.MIN_VALUE, cache.get(otherKey, 0)); + } + } + } + } + } + } + + @Test + public void interpolatorsKeepTheirFullIdentity() { + int[] interpolators = {0, 1, -1, 1 << 21, -(1 << 21), Integer.MIN_VALUE, Integer.MAX_VALUE}; + long key = SharedCornerBounds.key(0, 0); + for (int interpolator : interpolators) { + SharedCornerBounds cache = new SharedCornerBounds(2); + cache.put(key, interpolator, 73L); + for (int other : interpolators) { + assertEquals(interpolator == other ? 73L : Long.MIN_VALUE, cache.get(key, other)); + } + } } @Test public void aCollidingCornerReplacesTheOldOne() { SharedCornerBounds cache = new SharedCornerBounds(2); - long first = SharedCornerBounds.key(0, 0, 0); + long first = SharedCornerBounds.key(0, 0); long colliding = first; for (int gx = 1; gx < 100_000; gx++) { - long candidate = SharedCornerBounds.key(gx, 0, 0); - if (cache.slot(candidate) == cache.slot(first)) { + long candidate = SharedCornerBounds.key(gx, 0); + if (cache.slot(candidate, 0) == cache.slot(first, 0)) { colliding = candidate; break; } } assertNotEquals(first, colliding); - cache.put(first, 1L); - cache.put(colliding, 2L); + cache.put(first, 0, 1L); + cache.put(colliding, 0, 2L); - assertFalse(cache.contains(first)); - assertEquals(2L, cache.get(colliding)); + assertEquals(Long.MIN_VALUE, cache.get(first, 0)); + assertEquals(2L, cache.get(colliding, 0)); } @Test public void concurrentReadersNeverSeeAnotherCornersValue() throws InterruptedException { - SharedCornerBounds cache = new SharedCornerBounds(1 << 8); + SharedCornerBounds cache = new SharedCornerBounds(8); AtomicBoolean torn = new AtomicBoolean(); + AtomicInteger hits = new AtomicInteger(); + CountDownLatch start = new CountDownLatch(1); List threads = new ArrayList<>(); - for (int worker = 0; worker < 8; worker++) { + int[] coordinates = {0, 1 << 21, -(1 << 21), 30_000_000, -30_000_000, + Integer.MIN_VALUE, Integer.MAX_VALUE}; + int[] interpolators = {0, 1, -1, 1 << 21, Integer.MIN_VALUE, Integer.MAX_VALUE}; + for (int worker = 0; worker < 4; worker++) { int seed = worker; threads.add(new Thread(() -> { + try { + start.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + torn.set(true); + return; + } long state = 0x9E3779B97F4A7C15L * (seed + 1); - for (int step = 0; step < 400_000; step++) { + for (int step = 0; step < 200_000; step++) { state ^= state << 13; state ^= state >>> 7; state ^= state << 17; - int gx = (int) (state & 1023); - int gz = (int) ((state >>> 10) & 1023); - long key = SharedCornerBounds.key(gx, gz, (int) (state & 1)); - if ((state & 2) == 0) { - cache.put(key, key * 31L); - } else if (cache.contains(key) && cache.get(key) != key * 31L) { - torn.set(true); + int gx = coordinates[(int) ((state & Long.MAX_VALUE) % coordinates.length)]; + int gz = coordinates[(int) ((state >>> 10) % coordinates.length)]; + int interpolator = interpolators[(int) ((state >>> 20) % interpolators.length)]; + long key = SharedCornerBounds.key(gx, gz); + long expected = expectedValue(key, interpolator); + cache.put(key, interpolator, expected); + long actual = cache.get(key, interpolator); + if (actual != Long.MIN_VALUE) { + hits.incrementAndGet(); + if (actual != expected) { + torn.set(true); + } } } })); @@ -81,10 +139,16 @@ public class SharedCornerBoundsTest { for (Thread thread : threads) { thread.start(); } + start.countDown(); for (Thread thread : threads) { thread.join(); } + assertTrue(hits.get() > 0); assertFalse(torn.get()); } + private static long expectedValue(long key, int interpolator) { + long value = Long.rotateLeft(key, 17) ^ (0x9E3779B97F4A7C15L * interpolator); + return value == Long.MIN_VALUE ? 0L : value; + } }