From 618bb285911a9fb9b1386ed38a717db30253d797 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 30 Oct 2026 22:47:20 -0400 Subject: [PATCH] Fixed lattice eviction race - Lattice eviction sorts a snapshot of tile access stamps - Concurrent tile use during eviction no longer breaks the sort - Test covers eviction under concurrent fills --- .../hydrology/grid/HeightLattice.java | 23 ++++++++------ .../hydrology/grid/HeightLatticeTest.java | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java b/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java index cc1804606..68cf87401 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java @@ -138,19 +138,19 @@ public final class HeightLattice { return; } try { - List> idle = new ArrayList<>(tiles.size()); + List idle = new ArrayList<>(tiles.size()); for (Map.Entry entry : tiles.entrySet()) { - if (entry.getValue().pending.get() == 0) { - idle.add(entry); - } - } - idle.sort(Comparator.comparingLong(entry -> entry.getValue().used)); - int target = capacityTiles - (capacityTiles >> 3); - for (int index = 0; index < idle.size() && tiles.size() > target; index++) { - Map.Entry entry = idle.get(index); Tile tile = entry.getValue(); if (tile.pending.get() == 0) { - tiles.remove(entry.getKey(), tile); + idle.add(new IdleTile(entry.getKey(), tile, tile.used)); + } + } + idle.sort(Comparator.comparingLong(IdleTile::used)); + int target = capacityTiles - (capacityTiles >> 3); + for (int index = 0; index < idle.size() && tiles.size() > target; index++) { + IdleTile candidate = idle.get(index); + if (candidate.tile().pending.get() == 0) { + tiles.remove(candidate.key(), candidate.tile()); } } } finally { @@ -386,6 +386,9 @@ public final class HeightLattice { int cellZ(int index); } + private record IdleTile(Long key, Tile tile, long used) { + } + private record Span(int cellX0, int cellZ0, int width, int stride) implements Nodes { @Override public int cellX(int index) { diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java index 5e4da3c2d..bce372d77 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java @@ -100,6 +100,37 @@ public class HeightLatticeTest { assertEquals(DISTINCT_ACROSS_NINE, source.samples()); } + @Test + public void evictionToleratesConcurrentTileUse() throws Exception { + HeightLattice lattice = new HeightLattice(new CountingSource(), 1, false, 96 * 1024); + int threads = 12; + CyclicBarrier start = new CyclicBarrier(threads); + ExecutorService pool = Executors.newFixedThreadPool(threads); + try { + List> results = new ArrayList<>(); + for (int thread = 0; thread < threads; thread++) { + long seed = 31L * thread + 7L; + Callable task = () -> { + Random random = new Random(seed); + HydrologySampleBuffer out = new HydrologySampleBuffer(4); + start.await(); + for (int fill = 0; fill < 40_000; fill++) { + int tileX = random.nextInt(24); + int tileZ = random.nextInt(24); + lattice.fill(tileX * 32 + random.nextInt(31), tileZ * 32 + random.nextInt(31), 2, 2, out); + } + return null; + }; + results.add(pool.submit(task)); + } + for (Future result : results) { + result.get(120, TimeUnit.SECONDS); + } + } finally { + pool.shutdownNow(); + } + } + @Test public void returnsBitIdenticalValues() { HeightLattice full = new HeightLattice(new CountingSource(), 8, false, 1 << 16);