Reviewed distant terrain bounds

- Shared terrain bounds retain complete coordinate and interpolator identities
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-13 13:56:31 -04:00
commit acc2619bed
4 files changed
+135 -67

No files matched your search

@@ -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;
@@ -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);
}
}
@@ -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();
@@ -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<Thread> 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;
}
}