mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Optimize world generation
- Limit terrain cleanup to affected columns and batch cave boundary reads. - Reuse offline generation workers and native air states. - Gather history writes and defer unpublished append synchronization until atomic publication. - Cover terrain parity, worker lifecycle, history durability, and publication failures.
This commit is contained in:
1 parent
7986698c1c
commit
38cf4ac7c7
25 files changed
+1362
-226
No files matched your search
@@ -0,0 +1,153 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
|
||||
import art.arcane.volmlib.util.hunk.bits.DataContainer;
|
||||
import art.arcane.volmlib.util.hunk.storage.PaletteOrHunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TerrainComposedFaceTest {
|
||||
@BeforeClass
|
||||
public static void registerMatter() {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void allFacesMatchScalarCompositionAcrossSectionsAndCapturedNulls() {
|
||||
Matter[] sections = {new IrisMatter(16, 16, 16), null, new IrisMatter(16, 16, 16)};
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(chunk.exists(anyInt())).thenAnswer(call -> (int) call.getArgument(0) != 1);
|
||||
when(chunk.get(anyInt())).thenAnswer(call -> {
|
||||
assertTrue(Thread.holdsLock(chunk));
|
||||
return sections[(int) call.getArgument(0)];
|
||||
});
|
||||
MatterCavern natural = new MatterCavern(true, "natural", (byte) 0);
|
||||
MatterCavern original = new MatterCavern(false, "original", (byte) 2);
|
||||
HydrologyCaveCell[] hydrology = new HydrologyCaveCell[HydrologyCaveAction.values().length + 1];
|
||||
for (int index = 1; index < hydrology.length; index++) {
|
||||
hydrology[index] = new HydrologyCaveCell(HydrologyCaveAction.values()[index - 1], "river", "flooded");
|
||||
}
|
||||
PreObjectMatterCell[] journal = {null, PreObjectMatterCell.string(null),
|
||||
PreObjectMatterCell.cavern(null), PreObjectMatterCell.cavern(original),
|
||||
PreObjectMatterCell.hydrology(null), PreObjectMatterCell.hydrology(hydrology[1]),
|
||||
PreObjectMatterCell.cavern(null).captureHydrology(null),
|
||||
PreObjectMatterCell.cavern(original).captureHydrology(null)};
|
||||
for (Matter matter : sections) {
|
||||
if (matter == null) {
|
||||
continue;
|
||||
}
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int index = x + y * 17 + z * 31;
|
||||
matter.slice(MatterCavern.class).set(x, y, z, index % 3 == 0 ? null : natural);
|
||||
matter.slice(HydrologyCaveCell.class).set(x, y, z, hydrology[index % hydrology.length]);
|
||||
matter.slice(PreObjectMatterCell.class).set(x, y, z, journal[index % journal.length]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (TerrainMatterView.Face face : TerrainMatterView.Face.values()) {
|
||||
MatterCavern[] copied = TerrainMatterView.getComposedFace(chunk, face, 43);
|
||||
assertEquals(43 * 16, copied.length);
|
||||
for (int y = 0; y < 43; y++) {
|
||||
for (int offset = 0; offset < 16; offset++) {
|
||||
int x = face == TerrainMatterView.Face.WEST ? 0 : face == TerrainMatterView.Face.EAST ? 15 : offset;
|
||||
int z = face == TerrainMatterView.Face.NORTH ? 0 : face == TerrainMatterView.Face.SOUTH ? 15 : offset;
|
||||
assertSame(TerrainMatterView.getComposedCavern(chunk, x, y, z), copied[y * 16 + offset]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void paletteFacesUseOneBulkReadAndDoNotLeakAcrossAbsentSlices() {
|
||||
Matter first = new IrisMatter(16, 16, 16);
|
||||
Matter second = new IrisMatter(16, 16, 16);
|
||||
MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0);
|
||||
PaletteOrHunk<MatterCavern> slice = (PaletteOrHunk<MatterCavern>) first.<MatterCavern>slice(MatterCavern.class);
|
||||
slice.setRaw(15, 4, 7, cavern);
|
||||
DataContainer<MatterCavern> palette = spy(slice.palette());
|
||||
slice.setPalette(palette);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(chunk.exists(anyInt())).thenReturn(true);
|
||||
when(chunk.get(0)).thenReturn(first);
|
||||
when(chunk.get(1)).thenReturn(second);
|
||||
doAnswer(call -> {
|
||||
assertTrue(Thread.holdsLock(chunk));
|
||||
return call.callRealMethod();
|
||||
}).when(palette).copyTo(any(int[].class), any(MatterCavern[].class));
|
||||
MatterCavern[] copied = TerrainMatterView.getComposedFace(chunk, TerrainMatterView.Face.EAST, 32);
|
||||
assertSame(cavern, copied[4 * 16 + 7]);
|
||||
for (int index = 16 * 16; index < copied.length; index++) {
|
||||
assertNull(copied[index]);
|
||||
}
|
||||
verify(palette, times(1)).copyTo(any(int[].class), any(MatterCavern[].class));
|
||||
verify(palette, never()).get(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void nonstandardPaletteDimensionsRetainScalarCoordinatesAndBounds() {
|
||||
for (int[] dimensions : new int[][]{{8, 16, 16}, {16, 8, 16}, {16, 16, 8}}) {
|
||||
Matter matter = new IrisMatter(dimensions[0], dimensions[1], dimensions[2]);
|
||||
MatterCavern cavern = new MatterCavern(true, "small", (byte) 0);
|
||||
for (int x = 0; x < dimensions[0]; x++) {
|
||||
for (int y = 0; y < dimensions[1]; y++) {
|
||||
for (int z = 0; z < dimensions[2]; z++) {
|
||||
if ((x + y + z) % 3 == 0) {
|
||||
matter.slice(MatterCavern.class).set(x, y, z, cavern);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(chunk.exists(0)).thenReturn(true);
|
||||
when(chunk.get(0)).thenReturn(matter);
|
||||
for (TerrainMatterView.Face face : TerrainMatterView.Face.values()) {
|
||||
MatterCavern[] copied = TerrainMatterView.getComposedFace(chunk, face, 16);
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int offset = 0; offset < 16; offset++) {
|
||||
int x = face == TerrainMatterView.Face.WEST ? 0 : face == TerrainMatterView.Face.EAST ? 15 : offset;
|
||||
int z = face == TerrainMatterView.Face.NORTH ? 0 : face == TerrainMatterView.Face.SOUTH ? 15 : offset;
|
||||
assertSame(TerrainMatterView.getComposedCavern(chunk, x, y, z), copied[y * 16 + offset]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentFacesAndInvalidHeightsAreBounded() {
|
||||
assertEquals(0, TerrainMatterView.getComposedFace(null, TerrainMatterView.Face.WEST, 0).length);
|
||||
for (MatterCavern cavern : TerrainMatterView.getComposedFace(null, TerrainMatterView.Face.WEST, 17)) {
|
||||
assertNull(cavern);
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class, () -> TerrainMatterView.getComposedFace(null, TerrainMatterView.Face.WEST, -1));
|
||||
assertThrows(ArithmeticException.class, () -> TerrainMatterView.getComposedFace(null, TerrainMatterView.Face.WEST, Integer.MAX_VALUE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.TerrainMatterView;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisCarveBoundaryFacesTest {
|
||||
@BeforeClass
|
||||
public static void registerMatter() {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void borderSnapshotsPreserveCornerOrderAndNeverNestChunkLocks() throws Exception {
|
||||
MantleChunk<Matter>[] chunks = new MantleChunk[5];
|
||||
Matter[][] matter = new Matter[5][2];
|
||||
MatterCavern[] values = new MatterCavern[5];
|
||||
for (int side = 0; side < chunks.length; side++) {
|
||||
int selected = side;
|
||||
chunks[side] = mock(MantleChunk.class);
|
||||
matter[side][0] = new IrisMatter(16, 16, 16);
|
||||
matter[side][1] = new IrisMatter(16, 16, 16);
|
||||
values[side] = new MatterCavern(true, "side-" + side, (byte) 0);
|
||||
when(chunks[side].exists(anyInt())).thenAnswer(call -> {
|
||||
assertTrue(Thread.holdsLock(chunks[selected]));
|
||||
for (int other = 0; other < chunks.length; other++) {
|
||||
if (other != selected && chunks[other] != null) {
|
||||
assertFalse(Thread.holdsLock(chunks[other]));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
when(chunks[side].get(anyInt())).thenAnswer(call -> matter[selected][(int) call.getArgument(0)]);
|
||||
if (side == 0) {
|
||||
continue;
|
||||
}
|
||||
for (int y = 1; y < 32; y++) {
|
||||
for (int offset = 0; offset < 16; offset++) {
|
||||
int x = side == 1 ? 15 : side == 2 ? 0 : offset;
|
||||
int z = side == 3 ? 15 : side == 4 ? 0 : offset;
|
||||
matter[side][y >> 4].slice(MatterCavern.class).set(x, y & 15, z, values[side]);
|
||||
}
|
||||
}
|
||||
}
|
||||
matter[0][0].slice(MatterCavern.class).set(0, 7, 0, values[0]);
|
||||
matter[0][0].slice(MatterCavern.class).set(0, 5, 0, values[0]);
|
||||
matter[0][0].slice(PreObjectMatterCell.class).set(0, 5, 0, PreObjectMatterCell.cavern(null));
|
||||
Mantle<Matter> mantle = mock(Mantle.class, RETURNS_DEEP_STUBS);
|
||||
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
|
||||
when(mantle.getLoadedRegions().get(anyLong())).thenReturn(plate);
|
||||
when(plate.get(31, 0)).thenReturn(chunks[1]);
|
||||
when(plate.get(1, 0)).thenReturn(chunks[2]);
|
||||
when(plate.get(0, 31)).thenReturn(chunks[3]);
|
||||
when(plate.get(0, 1)).thenReturn(chunks[4]);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(0).maxHeight(48).build());
|
||||
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
|
||||
doReturn(engine).when(modifier).getEngine();
|
||||
CarveWallBuffer walls = new CarveWallBuffer(64);
|
||||
CarveColumnMask[] masks = new CarveColumnMask[256];
|
||||
Arrays.setAll(masks, index -> new CarveColumnMask());
|
||||
int[] surfaces = new int[256];
|
||||
Arrays.fill(surfaces, 30);
|
||||
Method method = IrisCarveModifier.class.getDeclaredMethod("addCrossChunkBoundaryWalls",
|
||||
Mantle.class, MantleChunk.class, CarveWallBuffer.class, CarveColumnMask[].class,
|
||||
int.class, int.class, int[].class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(modifier, mantle, chunks[0], walls, masks, 0, 0, surfaces);
|
||||
CarveWallBuffer expectedWalls = new CarveWallBuffer(64);
|
||||
for (int y = 1; y < 32; y++) {
|
||||
for (int offset = 0; offset < 16; offset++) {
|
||||
addScalarWall(expectedWalls, chunks[0], chunks[1], 0, y, offset, 15, offset);
|
||||
addScalarWall(expectedWalls, chunks[0], chunks[2], 15, y, offset, 0, offset);
|
||||
addScalarWall(expectedWalls, chunks[0], chunks[3], offset, y, 0, offset, 15);
|
||||
addScalarWall(expectedWalls, chunks[0], chunks[4], offset, y, 15, offset, 0);
|
||||
}
|
||||
}
|
||||
for (int y = 1; y < 32; y++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
MatterCavern expected = expectedWalls.get(x, y, z);
|
||||
assertSame("voxel " + x + "," + y + "," + z, expected, walls.get(x, y, z));
|
||||
assertTrue(masks[x * 16 + z].contains(y) == (expected != null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addScalarWall(CarveWallBuffer walls, MantleChunk<Matter> chunk,
|
||||
MantleChunk<Matter> neighbor, int x, int y, int z,
|
||||
int neighborX, int neighborZ) {
|
||||
if (TerrainMatterView.getComposedCavern(chunk, x, y, z) != null) {
|
||||
return;
|
||||
}
|
||||
MatterCavern value = TerrainMatterView.getComposedCavern(neighbor, neighborX, y, neighborZ);
|
||||
if (value != null) {
|
||||
walls.put(x, y, z, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.concurrent.BurstExecutor;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||
import art.arcane.iris.generation.decoration.IrisSpeleothems;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.hunk.storage.ArrayHunk;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisPerfectionModifierColumnTest {
|
||||
@Rule
|
||||
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
||||
|
||||
@Test
|
||||
public void randomColumnsMatchOriginalGlobalPasses() {
|
||||
Fixture fixture = new Fixture();
|
||||
Random random = new Random(82714L);
|
||||
NativeBlockState[] palette = {null, fixture.air, fixture.water, fixture.stone,
|
||||
fixture.decorant, fixture.half, fixture.spike(true, false), fixture.spike(false, true)};
|
||||
for (int trial = 0; trial < 8; trial++) {
|
||||
Hunk<NativeBlockState> source = Hunk.newArrayHunk(16, 32, 16);
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = 0; y < 32; y++) {
|
||||
source.setRaw(x, y, z, y == 0 ? fixture.stone : palette[random.nextInt(palette.length)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertParity(fixture, source);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedTallColumnsAreScannedOnceDuringRepeatedCleanup() {
|
||||
Fixture fixture = new Fixture();
|
||||
CountingHunk output = new CountingHunk(768);
|
||||
for (int y = 0; y < 768; y++) {
|
||||
output.setRaw(1, y, 0, fixture.stone);
|
||||
}
|
||||
for (int y = 1; y <= 24; y++) {
|
||||
output.setRaw(0, y, 0, fixture.decorant);
|
||||
}
|
||||
fixture.modifier.onModify(0, 0, output, false, null);
|
||||
assertTrue("clean column must not be scanned for each removed decorant", output.cleanColumnReads < 780);
|
||||
for (int y = 1; y <= 24; y++) {
|
||||
assertTrue(output.getRaw(0, y, 0).isAir());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pairedHalvesMergedSpikesAndWaterloggedSupportsMatchOriginal() {
|
||||
Fixture fixture = new Fixture();
|
||||
Hunk<NativeBlockState> source = Hunk.newArrayHunk(16, 16, 16);
|
||||
for (int x = 0; x < 16; x++) {
|
||||
source.setRaw(x, 1, 0, x % 2 == 0 ? fixture.stone : fixture.water);
|
||||
source.setRaw(x, 8, 0, x % 3 == 0 ? fixture.stone : fixture.decorant);
|
||||
for (int y = 2; y <= 4; y++) {
|
||||
source.setRaw(x, y, 0, fixture.spike(true, x % 2 == 0));
|
||||
}
|
||||
for (int y = 5; y <= 7; y++) {
|
||||
source.setRaw(x, y, 0, fixture.spike(false, x % 2 != 0));
|
||||
}
|
||||
source.setRaw(x, 10, 0, fixture.half);
|
||||
source.setRaw(x, 11, 0, fixture.decorant);
|
||||
}
|
||||
assertParity(fixture, source);
|
||||
}
|
||||
|
||||
private void assertParity(Fixture fixture, Hunk<NativeBlockState> source) {
|
||||
Hunk<NativeBlockState> expected = Hunk.newArrayHunk(16, source.getHeight(), 16);
|
||||
Hunk<NativeBlockState> actual = Hunk.newArrayHunk(16, source.getHeight(), 16);
|
||||
source.iterateSync((x, y, z, state) -> {
|
||||
expected.setRaw(x, y, z, state);
|
||||
actual.setRaw(x, y, z, state);
|
||||
});
|
||||
original(expected, fixture.air, fixture.water);
|
||||
fixture.modifier.onModify(0, 0, actual, false, null);
|
||||
expected.iterateSync((x, y, z, state) -> assertEquals("voxel " + x + "," + y + "," + z,
|
||||
state == null ? null : state.key(), actual.getRaw(x, y, z) == null ? null : actual.getRaw(x, y, z).key()));
|
||||
}
|
||||
|
||||
private static void original(Hunk<NativeBlockState> output, NativeBlockState air, NativeBlockState water) {
|
||||
boolean changed;
|
||||
do {
|
||||
changed = false;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
List<Integer> surfaces = new ArrayList<>();
|
||||
int top = 0;
|
||||
for (int y = output.getHeight() - 1; y >= 0; y--) {
|
||||
NativeBlockState block = output.get(x, y, z);
|
||||
if (block != null && !B.isAir(block) && !B.isFluid(block)) {
|
||||
top = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
boolean inside = true;
|
||||
surfaces.add(top);
|
||||
for (int y = top; y >= 0; y--) {
|
||||
NativeBlockState block = output.get(x, y, z);
|
||||
if (IrisSpeleothems.isSpike(block)) {
|
||||
block = IrisPerfectionModifier.normalizeSpike(block, output, x, z, y, air, water);
|
||||
}
|
||||
boolean now = block != null && !(B.isAir(block) || B.isFluid(block));
|
||||
if (now != inside) {
|
||||
inside = now;
|
||||
if (inside) {
|
||||
surfaces.add(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y : surfaces) {
|
||||
NativeBlockState tip = output.get(x, y, z);
|
||||
if (tip == null) {
|
||||
continue;
|
||||
}
|
||||
boolean remove = false;
|
||||
boolean removeBelow = false;
|
||||
if (B.isDecorant(tip)) {
|
||||
NativeBlockState below = output.get(x, y - 1, z);
|
||||
if (below == null || !B.canPlaceOnto(tip, below)) {
|
||||
remove = true;
|
||||
} else if (IrisProceduralBlocks.hasProperty(below, "half")) {
|
||||
NativeBlockState support = output.get(x, y - 2, z);
|
||||
if (support == null || !B.canPlaceOnto(below, support)) {
|
||||
remove = true;
|
||||
removeBelow = true;
|
||||
}
|
||||
}
|
||||
if (remove) {
|
||||
changed = true;
|
||||
output.set(x, y, z, air);
|
||||
if (removeBelow) {
|
||||
output.set(x, y - 1, z, air);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
}
|
||||
|
||||
private final class Fixture {
|
||||
private final Map<String, NativeBlockState> states = new HashMap<>();
|
||||
private final NativeBlockState air = state("minecraft:air");
|
||||
private final NativeBlockState water = state("minecraft:water");
|
||||
private final NativeBlockState stone = state("minecraft:stone");
|
||||
private final NativeBlockState decorant = state("minecraft:poppy");
|
||||
private final NativeBlockState half = state("minecraft:tall_grass[half=lower]");
|
||||
private final IrisPerfectionModifier modifier;
|
||||
|
||||
private Fixture() {
|
||||
when(platform.registries().block("AIR")).thenReturn(air);
|
||||
when(platform.registries().block("WATER")).thenReturn(water);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(new IrisDimension());
|
||||
when(engine.getMetrics()).thenReturn(mock(EngineMetrics.class, RETURNS_DEEP_STUBS));
|
||||
MultiBurst pool = mock(MultiBurst.class);
|
||||
when(engine.burst()).thenReturn(pool);
|
||||
when(pool.burst(false)).thenAnswer(call -> {
|
||||
BurstExecutor burst = new BurstExecutor(mock(ExecutorService.class), 16);
|
||||
burst.setMulticore(false);
|
||||
return burst;
|
||||
});
|
||||
modifier = new IrisPerfectionModifier(engine);
|
||||
}
|
||||
|
||||
private NativeBlockState spike(boolean upward, boolean waterlogged) {
|
||||
return state("minecraft:pointed_dripstone[thickness=tip,vertical_direction="
|
||||
+ (upward ? "up" : "down") + ",waterlogged=" + waterlogged + "]");
|
||||
}
|
||||
|
||||
private NativeBlockState state(String key) {
|
||||
NativeBlockState existing = states.get(key);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
NativeBlockState block = mock(NativeBlockState.class);
|
||||
states.put(key, block);
|
||||
when(block.key()).thenReturn(key);
|
||||
when(block.isAir()).thenReturn(key.equals("minecraft:air"));
|
||||
when(block.isFluid()).thenReturn(key.equals("minecraft:water"));
|
||||
when(block.isWater()).thenReturn(key.equals("minecraft:water"));
|
||||
when(block.isWaterLogged()).thenReturn(key.contains("waterlogged=true"));
|
||||
when(block.isDecorant()).thenReturn(key.equals("minecraft:poppy") || key.startsWith("minecraft:tall_grass"));
|
||||
when(block.canPlaceOnto(any())).thenAnswer(call -> "minecraft:tall_grass[half=lower]".equals(call.<NativeBlockState>getArgument(0).key()));
|
||||
BlockData handle = mock(BlockData.class);
|
||||
when(block.nativeHandle()).thenReturn(handle);
|
||||
when(handle.isFaceSturdy(any(), any())).thenReturn(key.equals("minecraft:stone"));
|
||||
when(block.withProperty(anyString(), anyString())).thenAnswer(call -> {
|
||||
String property = call.getArgument(0);
|
||||
String value = call.getArgument(1);
|
||||
return state(key.replaceAll(property + "=[^,\\]]+", property + "=" + value));
|
||||
});
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingHunk extends ArrayHunk<NativeBlockState> {
|
||||
private int cleanColumnReads;
|
||||
|
||||
private CountingHunk(int height) {
|
||||
super(16, height, 16);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState getRaw(int x, int y, int z) {
|
||||
if (x == 1 && z == 0) {
|
||||
cleanColumnReads++;
|
||||
}
|
||||
return super.getRaw(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package art.arcane.iris.world.history;
|
||||
|
||||
import art.arcane.iris.world.storage.Durability;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.AdditionalAnswers.delegatesTo;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public final class GenerationUnpublishedHistoryTest extends GenerationHistorySupport {
|
||||
@Test
|
||||
public void unpublishedHistoryRequiresNewRootAndReopeningRestoresImmediateBiomeWrites() throws Exception {
|
||||
Path root = temporaryFolder.getRoot().toPath().resolve("unpublished");
|
||||
Path pack = createPack("unpublished-pack", "alpha");
|
||||
GenerationHistory.FreshCreation creation = new GenerationHistory.FreshCreation(root, pack, fingerprint(pack),
|
||||
42L, contract(), GenerationRegistryContract.empty());
|
||||
GenerationHistory history = GenerationHistory.createUnpublished(creation);
|
||||
assertThrows(FileAlreadyExistsException.class, () -> GenerationHistory.createUnpublished(creation));
|
||||
history.savedBiomes().claimAndPersist(biomes(0));
|
||||
assertBiomeAppendForces(history.savedBiomes(), root, 1, 0);
|
||||
GenerationHistory reopened = GenerationHistory.open(root);
|
||||
assertEquals(biomes(1), reopened.savedBiomes().get(1, 0).orElseThrow());
|
||||
assertBiomeAppendForces(reopened.savedBiomes(), root, 2, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void semanticAppendDeferralDoesNotChangeReplayOrNormalOpenDurability() throws Exception {
|
||||
Path root = temporaryFolder.newFolder("semantics").toPath();
|
||||
GenerationSemanticIndex.initialize(root);
|
||||
GenerationSemanticIndex staged = GenerationSemanticIndex.loadRequired(root, true);
|
||||
staged.claimAndPersist(semantics(0));
|
||||
try (MockedStatic<Durability> forces = mockStatic(Durability.class, CALLS_REAL_METHODS)) {
|
||||
assertTrue(staged.claimAndPersist(semantics(1)));
|
||||
forces.verify(() -> Durability.force(any(FileChannel.class)), times(0));
|
||||
}
|
||||
GenerationSemanticIndex reopened = GenerationSemanticIndex.loadRequired(root);
|
||||
assertEquals(semantics(1), reopened.get(1, 0).orElseThrow());
|
||||
try (MockedStatic<Durability> forces = mockStatic(Durability.class, CALLS_REAL_METHODS)) {
|
||||
assertTrue(reopened.claimAndPersist(semantics(2)));
|
||||
forces.verify(() -> Durability.force(any(FileChannel.class)), times(1));
|
||||
}
|
||||
reopened.compactJournals();
|
||||
assertEquals(3, GenerationSemanticIndex.loadRequired(root).recordCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unpublishedBiomeWriteFailureStillRollsBackAndDoesNotPublishClaim() throws Exception {
|
||||
Path root = temporaryFolder.newFolder("failed-biomes").toPath();
|
||||
SavedBiomeStore store = SavedBiomeStore.open(root, true);
|
||||
store.claimAndPersist(biomes(0));
|
||||
Path region = region(root);
|
||||
byte[] before = Files.readAllBytes(region);
|
||||
try (RandomAccessFile actual = new RandomAccessFile(region.toFile(), "rw")) {
|
||||
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
|
||||
doThrow(new IOException("Expected staged write failure")).when(channel).write(any(ByteBuffer[].class), anyInt(), anyInt());
|
||||
try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class,
|
||||
withSettings().defaultAnswer(delegatesTo(actual)),
|
||||
(file, context) -> doReturn(channel).when(file).getChannel())) {
|
||||
assertThrows(IOException.class, () -> store.claimAndPersist(biomes(1)));
|
||||
}
|
||||
verify(channel, times(0)).force(true);
|
||||
}
|
||||
assertArrayEquals(before, Files.readAllBytes(region));
|
||||
assertTrue(store.get(1, 0).isEmpty());
|
||||
assertFalse(SavedBiomeStore.open(root).get(1, 0).isPresent());
|
||||
assertTrue(store.claimAndPersist(biomes(1)));
|
||||
}
|
||||
|
||||
private static void assertBiomeAppendForces(SavedBiomeStore store, Path root, int x, int count) throws Exception {
|
||||
try (RandomAccessFile actual = new RandomAccessFile(region(root).toFile(), "rw")) {
|
||||
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
|
||||
try (MockedConstruction<RandomAccessFile> ignored = mockConstruction(RandomAccessFile.class,
|
||||
withSettings().defaultAnswer(delegatesTo(actual)),
|
||||
(file, context) -> doReturn(channel).when(file).getChannel())) {
|
||||
assertTrue(store.claimAndPersist(biomes(x)));
|
||||
}
|
||||
verify(channel, times(count)).force(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path region(Path root) {
|
||||
return root.resolve("iris/generation/biomes/r.0.0.ibio");
|
||||
}
|
||||
|
||||
private static ChunkGenerationSemantics semantics(int x) {
|
||||
return ChunkGenerationSemantics.builder(x, 0, 1L).addSurfaceBiome("biome/main").seal().build();
|
||||
}
|
||||
|
||||
private static SavedBiomeChunk biomes(int x) {
|
||||
SavedBiomeChunk.Cell cell = new SavedBiomeChunk.Cell(1L, "biome/main", "region/main");
|
||||
SavedBiomeChunk.Column column = new SavedBiomeChunk.Column(cell, cell,
|
||||
List.of(new SavedBiomeChunk.Span(-64, 320, cell)));
|
||||
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(x, 0, 1L, -64, 384));
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
builder.column(localX, z, column);
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import java.io.RandomAccessFile;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
@@ -35,6 +36,8 @@ import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.AdditionalAnswers.delegatesTo;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -45,6 +48,74 @@ public class SavedBiomeStoreBatchTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void gatheringWritesHandleZeroAndPartialProgressAcrossQueuedFrames() throws Exception {
|
||||
Path root = temporaryFolder.newFolder().toPath();
|
||||
SavedBiomeStore store = SavedBiomeStore.open(root);
|
||||
store.claimAndPersist(chunk(0, 3L));
|
||||
BatchControl control = new BatchControl();
|
||||
control.maximumWriteBytes = 11;
|
||||
control.zeroFirstWrite = true;
|
||||
ExecutorService workers = Executors.newFixedThreadPool(4);
|
||||
List<SavedBiomeChunk> claims = List.of(chunk(1, 3L), chunk(2, 3L), chunk(3, 3L));
|
||||
try {
|
||||
List<Future<ClaimResult>> results = enqueue(store, root, control, workers, claims);
|
||||
assertTrue(control.forcing.await(5, TimeUnit.SECONDS));
|
||||
assertEquals(3, control.maximumGathered.get());
|
||||
assertTrue(control.writes.get() > claims.size());
|
||||
for (SavedBiomeChunk claim : claims) {
|
||||
assertTrue(store.cached(claim.chunkX(), claim.chunkZ()).isEmpty());
|
||||
}
|
||||
control.release.countDown();
|
||||
for (Future<ClaimResult> result : results) {
|
||||
ClaimResult completed = result.get(5, TimeUnit.SECONDS);
|
||||
assertTrue(completed.persisted());
|
||||
assertNull(completed.failure());
|
||||
}
|
||||
assertEquals(1, control.forces.get());
|
||||
SavedBiomeStore reopened = SavedBiomeStore.open(root);
|
||||
for (SavedBiomeChunk claim : claims) {
|
||||
assertEquals(claim, reopened.get(claim.chunkX(), claim.chunkZ()).orElseThrow());
|
||||
}
|
||||
} finally {
|
||||
control.release.countDown();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialGatherFailureRollsBackWholeBatchBeforePublication() throws Exception {
|
||||
Path root = temporaryFolder.newFolder().toPath();
|
||||
SavedBiomeStore store = SavedBiomeStore.open(root);
|
||||
store.claimAndPersist(chunk(0, 3L));
|
||||
Path region = root.resolve("iris/generation/biomes/r.0.0.ibio");
|
||||
byte[] original = Files.readAllBytes(region);
|
||||
BatchControl control = new BatchControl();
|
||||
control.maximumWriteBytes = 11;
|
||||
control.writeFailure = new IOException("Partial gathered append failed");
|
||||
control.release.countDown();
|
||||
ExecutorService workers = Executors.newFixedThreadPool(4);
|
||||
List<SavedBiomeChunk> claims = List.of(chunk(1, 3L), chunk(2, 3L), chunk(3, 3L));
|
||||
try {
|
||||
for (Future<ClaimResult> result : enqueue(store, root, control, workers, claims)) {
|
||||
ClaimResult completed = result.get(5, TimeUnit.SECONDS);
|
||||
assertFalse(completed.persisted());
|
||||
assertSame(control.writeFailure, completed.failure());
|
||||
}
|
||||
assertEquals(2, control.writes.get());
|
||||
assertEquals(1, control.forces.get());
|
||||
assertArrayEquals(original, Files.readAllBytes(region));
|
||||
for (SavedBiomeChunk claim : claims) {
|
||||
assertTrue(store.cached(claim.chunkX(), claim.chunkZ()).isEmpty());
|
||||
assertTrue(store.claimAndPersist(claim));
|
||||
}
|
||||
} finally {
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void independentOriginRegionsPersistWhileOneRegionRemainsLocked() throws Exception {
|
||||
Path root = temporaryFolder.newFolder().toPath();
|
||||
@@ -277,6 +348,33 @@ public class SavedBiomeStoreBatchTest {
|
||||
try (RandomAccessFile actual = new RandomAccessFile(
|
||||
root.resolve("iris/generation/biomes/r.0.0.ibio").toFile(), "rw")) {
|
||||
FileChannel channel = mock(FileChannel.class, delegatesTo(actual.getChannel()));
|
||||
if (control.maximumWriteBytes > 0) {
|
||||
doAnswer(invocation -> {
|
||||
ByteBuffer[] records = invocation.getArgument(0);
|
||||
int offset = invocation.getArgument(1);
|
||||
int count = invocation.getArgument(2);
|
||||
control.maximumGathered.accumulateAndGet(count, Math::max);
|
||||
int write = control.writes.incrementAndGet();
|
||||
if (control.zeroFirstWrite && write == 1) {
|
||||
return 0L;
|
||||
}
|
||||
if (control.writeFailure != null && write == 2) {
|
||||
throw control.writeFailure;
|
||||
}
|
||||
long written = 0L;
|
||||
for (int index = offset; index < offset + count && written < control.maximumWriteBytes; index++) {
|
||||
ByteBuffer record = records[index];
|
||||
int previousLimit = record.limit();
|
||||
record.limit(record.position() + Math.min(record.remaining(), control.maximumWriteBytes - (int) written));
|
||||
try {
|
||||
written += actual.getChannel().write(record);
|
||||
} finally {
|
||||
record.limit(previousLimit);
|
||||
}
|
||||
}
|
||||
return written;
|
||||
}).when(channel).write(any(ByteBuffer[].class), anyInt(), anyInt());
|
||||
}
|
||||
doAnswer(invocation -> {
|
||||
int force = control.forces.incrementAndGet();
|
||||
if (force == 1) {
|
||||
@@ -331,6 +429,11 @@ public class SavedBiomeStoreBatchTest {
|
||||
private final CountDownLatch forcing = new CountDownLatch(1);
|
||||
private final CountDownLatch release = new CountDownLatch(1);
|
||||
private final AtomicInteger forces = new AtomicInteger();
|
||||
private int maximumWriteBytes;
|
||||
private boolean zeroFirstWrite;
|
||||
private IOException writeFailure;
|
||||
private final AtomicInteger writes = new AtomicInteger();
|
||||
private final AtomicInteger maximumGathered = new AtomicInteger();
|
||||
private IOException failure;
|
||||
private RuntimeException uncheckedFailure;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
@@ -103,7 +104,8 @@ public class SavedBiomeStoreWriteTest {
|
||||
(file, context) -> doReturn(channel).when(file).getChannel())) {
|
||||
boolean result = store.claimAndPersist(chunk(1, 3L));
|
||||
assertEquals(1, files.constructed().size());
|
||||
verify(files.constructed().getFirst()).write(any(byte[].class));
|
||||
verify(channel).write(any(ByteBuffer[].class), anyInt(), anyInt());
|
||||
verify(files.constructed().getFirst(), never()).write(any(byte[].class));
|
||||
verify(files.constructed().getFirst(), never()).writeInt(anyInt());
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user