mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Merged wave1 into terrain bake
# Conflicts: # core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java # probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
This commit is contained in:
commit
d2106806a5
71 files changed
+5882
-948
No files matched your search
@@ -476,6 +476,11 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
if (providers.gradleProperty('irisRuntimeJava').getOrElse(rootProject.irisModTarget.java.toString()) == '21') {
|
||||
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/NativeNoiseLibrary*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/program/NativeNoise*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/program/NativeProgramEncoder*.class')
|
||||
exclude('art/arcane/volmlib/util/noise/program/GpuNoise*.class')
|
||||
exclude('art/arcane/volmlib/native/**')
|
||||
}
|
||||
// minimize() stays off. Caffeine reaches its 520 generated cache and node classes through
|
||||
// MethodHandles.Lookup.findClass on a computed name, and the VolmLib command, GUI and hotload
|
||||
|
||||
@@ -16,8 +16,8 @@ art/arcane/iris/generation/block/IrisCustomData.java
|
||||
art/arcane/iris/generation/block/LegacyTileData.java
|
||||
art/arcane/iris/generation/block/TileData.java
|
||||
art/arcane/iris/generation/chunk/ChunkBiomeHunkView.java
|
||||
art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java
|
||||
art/arcane/iris/generation/chunk/ChunkDataHunkView.java
|
||||
art/arcane/iris/generation/chunk/ChunkDataOutput.java
|
||||
art/arcane/iris/generation/chunk/ChunkHunkView.java
|
||||
art/arcane/iris/generation/chunk/LinkedTerrainChunk.java
|
||||
art/arcane/iris/generation/chunk/TerrainChunk.java
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.block.BoundBlockState;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Plain-array block buffer for one chunk. Stages write disjoint columns when they fan out and every hand-off
|
||||
* between threads is a join, so the cells need no per-access fences. Each column tracks its highest stored y;
|
||||
* a raise is a CAS so concurrent writers to one column can never lose the maximum.
|
||||
*/
|
||||
public class ChunkDataHunkHolder extends StorageHunk<NativeBlockState> implements NativeBlockVolume, ColumnExtent {
|
||||
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
||||
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
|
||||
|
||||
private final ChunkData chunk;
|
||||
private final NativeBlockState[] data;
|
||||
private final int[] columnTops;
|
||||
private final int zStride;
|
||||
|
||||
public ChunkDataHunkHolder(ChunkData chunk) {
|
||||
super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16);
|
||||
this.chunk = chunk;
|
||||
this.zStride = 16 * getHeight();
|
||||
this.data = new NativeBlockState[zStride * 16];
|
||||
this.columnTops = new int[256];
|
||||
Arrays.fill(columnTops, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRaw(int x, int y, int z, NativeBlockState t) {
|
||||
// Block-hunk contract: null means "no write", never "erase" — ChunkDataHunkView and
|
||||
// the modded ModdedBlockBuffer already discard nulls, and storing one here made the
|
||||
// Bukkit output diverge from the modded loaders for the same engine emission.
|
||||
if (t == null) {
|
||||
return;
|
||||
}
|
||||
data[index(x, y, z)] = t;
|
||||
int column = (z << 4) | x;
|
||||
int top = (int) COLUMN_TOPS.getOpaque(columnTops, column);
|
||||
while (y > top) {
|
||||
int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y);
|
||||
if (witness == top) {
|
||||
break;
|
||||
}
|
||||
top = witness;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState getRaw(int x, int y, int z) {
|
||||
if (y < 0 || y >= getHeight()) {
|
||||
return AIR.get();
|
||||
}
|
||||
|
||||
NativeBlockState b = data[index(x, y, z)];
|
||||
|
||||
return b != null ? b : AIR.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState getStoredRaw(int x, int y, int z) {
|
||||
return data[index(x, y, z)];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int highestStoredY(int x, int z) {
|
||||
return (int) COLUMN_TOPS.getOpaque(columnTops, (z << 4) | x);
|
||||
}
|
||||
|
||||
private int index(int x, int y, int z) {
|
||||
return (z * zStride) + (y << 4) + x;
|
||||
}
|
||||
|
||||
public void apply() {
|
||||
applyTo(chunk);
|
||||
}
|
||||
|
||||
public void applyTo(ChunkData target) {
|
||||
if (INMS.get().applyChunkDataBlocks(target, this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int height = getHeight();
|
||||
for (int x = 0; x < getWidth(); x++) {
|
||||
for (int z = 0; z < getDepth(); z++) {
|
||||
BlockData activeBlock = null;
|
||||
int runStart = -1;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
NativeBlockState state = getStoredRaw(x, y, z);
|
||||
BlockData block = state == null ? null : (BlockData) state.nativeHandle();
|
||||
// Custom wrappers are not real Bukkit data; write the vanilla base like the
|
||||
// NMS fast path (NMSBinding.applyChunkDataBlocks) does.
|
||||
if (block instanceof IrisCustomData custom) {
|
||||
block = custom.getBase();
|
||||
}
|
||||
if (block == null) {
|
||||
flushRun(target, x, z, runStart, y, activeBlock);
|
||||
activeBlock = null;
|
||||
runStart = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activeBlock != null && activeBlock.equals(block)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flushRun(target, x, z, runStart, y, activeBlock);
|
||||
activeBlock = block;
|
||||
runStart = y;
|
||||
}
|
||||
|
||||
flushRun(target, x, z, runStart, height, activeBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void flushRun(ChunkData target, int x, int z, int startY, int endY, BlockData block) {
|
||||
if (block == null || startY < 0 || endY <= startY) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minY = target.getMinHeight();
|
||||
if (endY - startY == 1) {
|
||||
target.setBlock(x, startY + minY, z, block);
|
||||
return;
|
||||
}
|
||||
|
||||
target.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||
|
||||
public final class ChunkDataOutput {
|
||||
private ChunkDataOutput() {
|
||||
}
|
||||
|
||||
public static void apply(ChunkData target, SectionBuffer blocks) {
|
||||
if (INMS.get().applyChunkDataBlocks(target, blocks)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int height = blocks.getHeight();
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
BlockData activeBlock = null;
|
||||
int runStart = -1;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
NativeBlockState state = blocks.getStoredRaw(x, y, z);
|
||||
BlockData block = state == null ? null : (BlockData) state.nativeHandle();
|
||||
if (block instanceof IrisCustomData custom) {
|
||||
block = custom.getBase();
|
||||
}
|
||||
if (block == null) {
|
||||
flushRun(target, x, z, runStart, y, activeBlock);
|
||||
activeBlock = null;
|
||||
runStart = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activeBlock != null && activeBlock.equals(block)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flushRun(target, x, z, runStart, y, activeBlock);
|
||||
activeBlock = block;
|
||||
runStart = y;
|
||||
}
|
||||
|
||||
flushRun(target, x, z, runStart, height, activeBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void flushRun(ChunkData target, int x, int z, int startY, int endY, BlockData block) {
|
||||
if (block == null || startY < 0 || endY <= startY) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minY = target.getMinHeight();
|
||||
if (endY - startY == 1) {
|
||||
target.setBlock(x, startY + minY, z, block);
|
||||
return;
|
||||
}
|
||||
|
||||
target.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
|
||||
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent {
|
||||
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T> {
|
||||
private final Hunk<T> source;
|
||||
private final Listener<T> listener;
|
||||
|
||||
@@ -35,6 +35,12 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
|
||||
source.setRaw(x, y, z, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, T value) {
|
||||
listener.onRun(x, z, yFromInclusive, yToInclusive, value);
|
||||
ColumnRunHunk.fillColumnRun(source, x, z, yFromInclusive, yToInclusive, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getRaw(int x, int y, int z) {
|
||||
return source.getRaw(x, y, z);
|
||||
@@ -63,5 +69,11 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
|
||||
@FunctionalInterface
|
||||
public interface Listener<T> {
|
||||
void onWrite(int x, int y, int z, T value);
|
||||
|
||||
default void onRun(int x, int z, int yFromInclusive, int yToInclusive, T value) {
|
||||
for (int y = yToInclusive; y >= yFromInclusive; y--) {
|
||||
onWrite(x, y, z, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
|
||||
public interface ColumnRunHunk<T> {
|
||||
void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, T value);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> void fillColumnRun(Hunk<T> hunk, int x, int z, int yFromInclusive, int yToInclusive, T value) {
|
||||
if (hunk instanceof ColumnRunHunk<?> runs) {
|
||||
((ColumnRunHunk<T>) runs).fillColumnRun(x, z, yFromInclusive, yToInclusive, value);
|
||||
return;
|
||||
}
|
||||
for (int y = yToInclusive; y >= yFromInclusive; y--) {
|
||||
hunk.setRaw(x, y, z, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.generation.block.BoundBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeSectionVolume;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState> {
|
||||
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
|
||||
private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class);
|
||||
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
|
||||
private static final int SECTION_CELLS = 4096;
|
||||
private static final int MAX_PALETTE_ID = 0xFFFF;
|
||||
private static final int INITIAL_PALETTE_CAPACITY = 64;
|
||||
private static final int COLUMN_SLICE_SHIFT = 5;
|
||||
|
||||
private final int height;
|
||||
private final short[][] sections;
|
||||
private final int[] columnTops;
|
||||
private final Object paletteLock = new Object();
|
||||
private PaletteEntry[] slots;
|
||||
private NativeBlockState[] states;
|
||||
private int paletteSize;
|
||||
|
||||
public SectionBuffer(int height) {
|
||||
if (height <= 0) {
|
||||
throw new IllegalArgumentException("Unsupported section buffer height " + height);
|
||||
}
|
||||
this.height = height;
|
||||
this.sections = new short[(height + 15) >> 4][];
|
||||
this.columnTops = new int[16 << COLUMN_SLICE_SHIFT];
|
||||
Arrays.fill(columnTops, -1);
|
||||
this.slots = new PaletteEntry[INITIAL_PALETTE_CAPACITY << 1];
|
||||
this.states = new NativeBlockState[INITIAL_PALETTE_CAPACITY];
|
||||
this.paletteSize = 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
public int paletteId(NativeBlockState state) {
|
||||
if (state == null) {
|
||||
return 0;
|
||||
}
|
||||
PaletteEntry[] table = slots;
|
||||
int mask = table.length - 1;
|
||||
int slot = System.identityHashCode(state) & mask;
|
||||
PaletteEntry entry;
|
||||
while ((entry = table[slot]) != null) {
|
||||
if (entry.state == state) {
|
||||
return entry.id;
|
||||
}
|
||||
slot = (slot + 1) & mask;
|
||||
}
|
||||
return insert(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRaw(int x, int y, int z, NativeBlockState t) {
|
||||
if (t == null) {
|
||||
return;
|
||||
}
|
||||
writableSection(y >> 4)[((y & 15) << 8) | (z << 4) | x] = (short) paletteId(t);
|
||||
raiseTop((x << COLUMN_SLICE_SHIFT) | z, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState getRaw(int x, int y, int z) {
|
||||
if (y < 0 || y >= height) {
|
||||
return AIR.get();
|
||||
}
|
||||
short[] section = sections[y >> 4];
|
||||
if (section != null) {
|
||||
int id = section[((y & 15) << 8) | (z << 4) | x] & 0xFFFF;
|
||||
if (id != 0) {
|
||||
return state(id);
|
||||
}
|
||||
}
|
||||
return AIR.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState getStoredRaw(int x, int y, int z) {
|
||||
if (y < 0 || y >= height) {
|
||||
return null;
|
||||
}
|
||||
short[] section = sections[y >> 4];
|
||||
if (section == null) {
|
||||
return null;
|
||||
}
|
||||
int id = section[((y & 15) << 8) | (z << 4) | x] & 0xFFFF;
|
||||
return id == 0 ? null : state(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) {
|
||||
if (value == null || yToInclusive < yFromInclusive) {
|
||||
return;
|
||||
}
|
||||
fillColumnRun(x, z, yFromInclusive, yToInclusive, paletteId(value));
|
||||
}
|
||||
|
||||
public void fillColumnRun(int localX, int localZ, int yFromInclusive, int yToInclusive, int paletteId) {
|
||||
if (paletteId == 0 || yToInclusive < yFromInclusive) {
|
||||
return;
|
||||
}
|
||||
short value = (short) paletteId;
|
||||
int column = (localZ << 4) | localX;
|
||||
int y = yFromInclusive;
|
||||
while (y <= yToInclusive) {
|
||||
int sectionIndex = y >> 4;
|
||||
short[] section = writableSection(sectionIndex);
|
||||
int last = Math.min(yToInclusive, (sectionIndex << 4) | 15);
|
||||
int end = ((last & 15) << 8) | column;
|
||||
for (int index = ((y & 15) << 8) | column; index <= end; index += 256) {
|
||||
section[index] = value;
|
||||
}
|
||||
y = last + 1;
|
||||
}
|
||||
raiseTop((localX << COLUMN_SLICE_SHIFT) | localZ, yToInclusive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int highestStoredY(int x, int z) {
|
||||
return (int) COLUMN_TOPS.getOpaque(columnTops, (x << COLUMN_SLICE_SHIFT) | z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sectionCount() {
|
||||
return sections.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short[] section(int sectionIndex) {
|
||||
return sections[sectionIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState[] palette() {
|
||||
synchronized (paletteLock) {
|
||||
return Arrays.copyOf(states, paletteSize);
|
||||
}
|
||||
}
|
||||
|
||||
private short[] writableSection(int sectionIndex) {
|
||||
short[] section = sections[sectionIndex];
|
||||
if (section != null) {
|
||||
return section;
|
||||
}
|
||||
short[] created = new short[SECTION_CELLS];
|
||||
short[] witness = (short[]) SECTIONS.compareAndExchange(sections, sectionIndex, (short[]) null, created);
|
||||
return witness == null ? created : witness;
|
||||
}
|
||||
|
||||
private void raiseTop(int column, int y) {
|
||||
int top = (int) COLUMN_TOPS.getOpaque(columnTops, column);
|
||||
while (y > top) {
|
||||
int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y);
|
||||
if (witness == top) {
|
||||
return;
|
||||
}
|
||||
top = witness;
|
||||
}
|
||||
}
|
||||
|
||||
private NativeBlockState state(int id) {
|
||||
NativeBlockState[] current = states;
|
||||
if (id < current.length) {
|
||||
NativeBlockState state = current[id];
|
||||
if (state != null) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
synchronized (paletteLock) {
|
||||
return states[id];
|
||||
}
|
||||
}
|
||||
|
||||
private int insert(NativeBlockState state) {
|
||||
synchronized (paletteLock) {
|
||||
PaletteEntry[] table = slots;
|
||||
int mask = table.length - 1;
|
||||
int slot = System.identityHashCode(state) & mask;
|
||||
PaletteEntry entry;
|
||||
while ((entry = table[slot]) != null) {
|
||||
if (entry.state == state) {
|
||||
return entry.id;
|
||||
}
|
||||
slot = (slot + 1) & mask;
|
||||
}
|
||||
int id = paletteSize;
|
||||
if (id > MAX_PALETTE_ID) {
|
||||
throw new IllegalStateException("A chunk wrote more than " + MAX_PALETTE_ID + " distinct block state instances");
|
||||
}
|
||||
NativeBlockState[] current = states;
|
||||
if (id == current.length) {
|
||||
current = Arrays.copyOf(current, current.length << 1);
|
||||
}
|
||||
current[id] = state;
|
||||
states = current;
|
||||
paletteSize = id + 1;
|
||||
PaletteEntry created = new PaletteEntry(state, id);
|
||||
if (id << 1 >= table.length) {
|
||||
slots = rehash(table, created);
|
||||
} else {
|
||||
table[slot] = created;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
private static PaletteEntry[] rehash(PaletteEntry[] table, PaletteEntry created) {
|
||||
PaletteEntry[] grown = new PaletteEntry[table.length << 1];
|
||||
for (PaletteEntry entry : table) {
|
||||
if (entry != null) {
|
||||
place(grown, entry);
|
||||
}
|
||||
}
|
||||
place(grown, created);
|
||||
return grown;
|
||||
}
|
||||
|
||||
private static void place(PaletteEntry[] table, PaletteEntry entry) {
|
||||
int mask = table.length - 1;
|
||||
int slot = System.identityHashCode(entry.state) & mask;
|
||||
while (table[slot] != null) {
|
||||
slot = (slot + 1) & mask;
|
||||
}
|
||||
table[slot] = entry;
|
||||
}
|
||||
|
||||
private record PaletteEntry(NativeBlockState state, int id) {
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
private final AtomicInteger parallelismBaseline = new AtomicInteger();
|
||||
|
||||
public MultiBurst() {
|
||||
this("Iris");
|
||||
this("Iris", Thread.MIN_PRIORITY, MultiBurstSupport::performanceProcessors);
|
||||
}
|
||||
|
||||
public MultiBurst(String name) {
|
||||
@@ -140,8 +140,15 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
|
||||
static final class HydrologyBurst extends MultiBurst {
|
||||
HydrologyBurst() {
|
||||
super("Iris Hydrology", () -> Math.max(2,
|
||||
IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
|
||||
super("Iris Hydrology", HydrologyBurst::plannerParallelism);
|
||||
}
|
||||
|
||||
static int plannerParallelism() {
|
||||
return plannerParallelism(MultiBurstSupport.performanceProcessors());
|
||||
}
|
||||
|
||||
static int plannerParallelism(int performanceProcessors) {
|
||||
return Math.max(4, performanceProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -79,6 +79,14 @@ public final class IrisOreBands {
|
||||
return minY <= maxY;
|
||||
}
|
||||
|
||||
public int lowestY() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
public int highestY() {
|
||||
return maxY;
|
||||
}
|
||||
|
||||
public boolean contains(int y) {
|
||||
return y >= minY && y <= maxY;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.runtime.GenerationClosedException;
|
||||
import art.arcane.iris.generation.runtime.GenerationCounter;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
@@ -43,7 +44,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
private static final byte AROUND_EMPTY = 1;
|
||||
private static final byte AROUND_PUBLISHED = 2;
|
||||
private static final int MAXIMUM_PREGENERATION_TILES = 4096;
|
||||
private static final int PREGENERATION_HALO_BLOCKS = 512;
|
||||
private static final int PREGENERATION_HALO_BLOCKS = 256;
|
||||
private static final HydrologyColumnSample[] NO_COLUMNS = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
|
||||
private static final Cache<SharedTileKey, HydrologyTile> SHARED_TILES = Caffeine.newBuilder()
|
||||
.maximumWeight(HydrologyCacheBudget.sharedStudioBytes())
|
||||
@@ -499,6 +500,9 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.HYDROLOGY_LOOKUPS.increment();
|
||||
}
|
||||
return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ);
|
||||
}
|
||||
|
||||
@@ -507,6 +511,9 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
if (!waitsForbidden()) {
|
||||
return HydrologyColumnSnapshot.ready(columnAt(blockX, blockZ).orElse(null));
|
||||
}
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.HYDROLOGY_LOOKUPS.increment();
|
||||
}
|
||||
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
|
||||
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
|
||||
ChunkColumns local = localChunks(cacheEpoch.get()).get(chunkX, chunkZ);
|
||||
|
||||
@@ -66,6 +66,24 @@ final class CaveCarveScratch {
|
||||
final int[] tileLocalX = new int[4];
|
||||
final int[] tileLocalZ = new int[4];
|
||||
final int[] tileTopY = new int[4];
|
||||
final long[] rowCarveBits = new long[4];
|
||||
final long[] cellPassBits = new long[4];
|
||||
final CaveDensityLattice cellLattice = new CaveDensityLattice();
|
||||
final double[] cellThresholdMin = new double[4];
|
||||
final double[] cellThresholdMax = new double[4];
|
||||
final double[] cellThresholdMaxBoost = new double[4];
|
||||
final int[] cellBreakFloorMin = new int[4];
|
||||
final int[] cellCeilingFadeStart = new int[4];
|
||||
final int[] cellTopY = new int[4];
|
||||
final int[] cellMinTopY = new int[4];
|
||||
final boolean[] cellProtected = new boolean[4];
|
||||
final int[] columnProtectStartY = new int[256];
|
||||
final int[] columnProtectEndY = new int[256];
|
||||
int[] rowActiveCount = new int[0];
|
||||
long solidCells;
|
||||
long openCells;
|
||||
long mixedCells;
|
||||
long exactRows;
|
||||
CaveFieldModuleState[] activeModules = new CaveFieldModuleState[0];
|
||||
double[] activeModuleRemainingMin = new double[0];
|
||||
double[] activeModuleRemainingMax = new double[0];
|
||||
@@ -79,10 +97,33 @@ final class CaveCarveScratch {
|
||||
int adaptiveGeometryStep = -1;
|
||||
int adaptiveGeometryAxisCells = -1;
|
||||
boolean fullWeightsInitialized;
|
||||
int[] latticeY = new int[0];
|
||||
int[] latticeSnapY = new int[0];
|
||||
double[] latticeWarpA = new double[0];
|
||||
double[] latticeWarpB = new double[0];
|
||||
double[] latticeWarpedX = new double[0];
|
||||
double[] latticeWarpedY = new double[0];
|
||||
double[] latticeWarpedZ = new double[0];
|
||||
double[] latticeDensity = new double[0];
|
||||
|
||||
void releaseReferences() {
|
||||
Arrays.fill(sectionMatter, null);
|
||||
Arrays.fill(sectionSlices, null);
|
||||
Arrays.fill(densityColumns, null);
|
||||
cellLattice.releaseReferences();
|
||||
}
|
||||
|
||||
void ensureLatticeBatch(int size) {
|
||||
if (latticeY.length >= size) {
|
||||
return;
|
||||
}
|
||||
latticeY = new int[size];
|
||||
latticeSnapY = new int[size];
|
||||
latticeWarpA = new double[size];
|
||||
latticeWarpB = new double[size];
|
||||
latticeWarpedX = new double[size];
|
||||
latticeWarpedY = new double[size];
|
||||
latticeWarpedZ = new double[size];
|
||||
latticeDensity = new double[size];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
final class CaveCellKernel {
|
||||
static final int NONE = 0;
|
||||
static final int ALL = 1;
|
||||
static final int MIXED = 2;
|
||||
private static final double GUARD_ULPS = 32D;
|
||||
private static final double MAXIMUM_MAGNITUDE = 0x1p500;
|
||||
|
||||
private CaveCellKernel() {
|
||||
}
|
||||
|
||||
static int decide(double d00, double d01, double d10, double d11, double thresholdLow, double thresholdHigh) {
|
||||
double low = Math.min(Math.min(d00, d01), Math.min(d10, d11));
|
||||
double high = Math.max(Math.max(d00, d01), Math.max(d10, d11));
|
||||
double magnitude = Math.max(Math.abs(low), Math.abs(high));
|
||||
if (!(magnitude < MAXIMUM_MAGNITUDE)) {
|
||||
return MIXED;
|
||||
}
|
||||
double guard = Math.ulp(magnitude) * GUARD_ULPS;
|
||||
if (high + guard <= thresholdLow) {
|
||||
return ALL;
|
||||
}
|
||||
if (low - guard > thresholdHigh) {
|
||||
return NONE;
|
||||
}
|
||||
return MIXED;
|
||||
}
|
||||
|
||||
static double threshold(double localThreshold, double normalizationFactor, double inverseNormalization) {
|
||||
return (localThreshold * normalizationFactor) * inverseNormalization;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.cache.ConcurrentClockCache;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
@@ -22,42 +23,63 @@ final class CaveDensityGrid {
|
||||
private static final AtomicInteger OWNERS = new AtomicInteger();
|
||||
|
||||
private final ConcurrentClockCache<Column> columns;
|
||||
private final ConcurrentClockCache<Column> warpColumns;
|
||||
private final ConcurrentHashMap<Object, Owner> owners = new ConcurrentHashMap<>();
|
||||
|
||||
CaveDensityGrid(int capacity) {
|
||||
columns = new ConcurrentClockCache<>(capacity);
|
||||
warpColumns = new ConcurrentClockCache<>(Math.max(1, capacity >> 2));
|
||||
}
|
||||
|
||||
static int nextOwner() {
|
||||
return OWNERS.getAndIncrement();
|
||||
Owner owner(Object fieldKey) {
|
||||
return owners.computeIfAbsent(fieldKey, ignored -> new Owner(OWNERS.getAndIncrement()));
|
||||
}
|
||||
|
||||
Column column(Object owner, int ownerId, int height, int latticeX, int latticeZ) {
|
||||
long key = (long) (ownerId & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK);
|
||||
Column cached = columns.get(key);
|
||||
if (cached == null) {
|
||||
cached = columns.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height));
|
||||
}
|
||||
return cached.matches(owner, latticeX, latticeZ) ? cached : null;
|
||||
Column column(Owner owner, int height, int latticeX, int latticeZ) {
|
||||
return column(columns, owner, height, latticeX, latticeZ);
|
||||
}
|
||||
|
||||
Column warpColumn(Owner owner, int height, int latticeX, int latticeZ) {
|
||||
return column(warpColumns, owner, height, latticeX, latticeZ);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
columns.clear();
|
||||
warpColumns.clear();
|
||||
owners.clear();
|
||||
}
|
||||
|
||||
private static Column column(ConcurrentClockCache<Column> cache, Owner owner, int height, int latticeX, int latticeZ) {
|
||||
long key = (long) (owner.id & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK);
|
||||
Column cached = cache.get(key);
|
||||
if (cached == null) {
|
||||
cached = cache.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height));
|
||||
}
|
||||
return cached.matches(owner, latticeX, latticeZ) ? cached : null;
|
||||
}
|
||||
|
||||
static final class Owner {
|
||||
private final int id;
|
||||
|
||||
private Owner(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Column {
|
||||
private final Object owner;
|
||||
private final Owner owner;
|
||||
private final int latticeX;
|
||||
private final int latticeZ;
|
||||
private final AtomicReferenceArray<double[]> segments;
|
||||
|
||||
private Column(Object owner, int latticeX, int latticeZ, int height) {
|
||||
private Column(Owner owner, int latticeX, int latticeZ, int height) {
|
||||
this.owner = owner;
|
||||
this.latticeX = latticeX;
|
||||
this.latticeZ = latticeZ;
|
||||
segments = new AtomicReferenceArray<>(Math.ceilDiv(Math.max(1, height), SEGMENT_SIZE));
|
||||
}
|
||||
|
||||
boolean matches(Object owner, int latticeX, int latticeZ) {
|
||||
boolean matches(Owner owner, int latticeX, int latticeZ) {
|
||||
return this.owner == owner && this.latticeX == latticeX && this.latticeZ == latticeZ;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class CaveDensityLattice {
|
||||
static final int CORNERS = 9;
|
||||
|
||||
final CaveDensityGrid.Column[] densityColumns = new CaveDensityGrid.Column[CORNERS];
|
||||
final CaveDensityGrid.Column[] warpColumns = new CaveDensityGrid.Column[CORNERS];
|
||||
private double[] values = new double[0];
|
||||
private byte[] rowCells = new byte[0];
|
||||
private short[] rowCorners = new short[0];
|
||||
private int minimumY;
|
||||
private int rows;
|
||||
|
||||
void reset(int minimumY, int rows) {
|
||||
this.minimumY = minimumY;
|
||||
this.rows = rows;
|
||||
if (rowCells.length < rows) {
|
||||
rowCells = new byte[rows];
|
||||
rowCorners = new short[rows];
|
||||
}
|
||||
if (values.length < rows * CORNERS) {
|
||||
values = new double[rows * CORNERS];
|
||||
}
|
||||
Arrays.fill(rowCells, 0, rows, (byte) 0);
|
||||
Arrays.fill(rowCorners, 0, rows, (short) 0);
|
||||
}
|
||||
|
||||
void markRow(int y, int cells, int corners) {
|
||||
int row = y - minimumY;
|
||||
rowCells[row] = (byte) cells;
|
||||
rowCorners[row] = (short) corners;
|
||||
}
|
||||
|
||||
int cells(int y) {
|
||||
return rowCells[y - minimumY];
|
||||
}
|
||||
|
||||
boolean needs(int corner, int y) {
|
||||
return (rowCorners[y - minimumY] & (1 << corner)) != 0;
|
||||
}
|
||||
|
||||
double density(int corner, int y) {
|
||||
return values[corner * rows + (y - minimumY)];
|
||||
}
|
||||
|
||||
void store(int corner, int y, double density) {
|
||||
values[corner * rows + (y - minimumY)] = density;
|
||||
}
|
||||
|
||||
void releaseReferences() {
|
||||
Arrays.fill(densityColumns, null);
|
||||
Arrays.fill(warpColumns, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
import art.arcane.volmlib.util.noise.program.PrimitiveKind;
|
||||
|
||||
record CaveFieldKey(long signature, long probe, NoiseProgram identity) {
|
||||
private static final double[][] PROBE_POINTS = {
|
||||
{0.5D, 3.25D, -7.75D},
|
||||
{-1031.125D, 77.5D, 2049.875D},
|
||||
{16D, 255D, -16D},
|
||||
{123_456.375D, -40.25D, -98_765.625D}
|
||||
};
|
||||
|
||||
static CaveFieldKey of(NoiseProgram program) {
|
||||
if (!shareable(program)) {
|
||||
return new CaveFieldKey(program.structuralSignature(), 0L, program);
|
||||
}
|
||||
long probe = 0xCBF29CE484222325L;
|
||||
for (double[] point : PROBE_POINTS) {
|
||||
probe = (probe ^ Double.doubleToRawLongBits(program.sampleSigned3D(point[0], point[1], point[2]))) * 0x100000001B3L;
|
||||
}
|
||||
return new CaveFieldKey(program.structuralSignature(), probe, null);
|
||||
}
|
||||
|
||||
private static boolean shareable(NoiseProgram program) {
|
||||
for (int node = 0; node < program.nodeCount(); node++) {
|
||||
if (program.nodeOpaque(node) || program.nodeKernel(node).kind() == PrimitiveKind.GENERIC) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
|
||||
final class CaveFieldModuleState {
|
||||
final CNG density;
|
||||
final NoiseProgram density;
|
||||
final int minY;
|
||||
final int maxY;
|
||||
final double weight;
|
||||
@@ -32,7 +32,7 @@ final class CaveFieldModuleState {
|
||||
final double minContribution;
|
||||
final double maxContribution;
|
||||
|
||||
CaveFieldModuleState(IrisCaveFieldModule module, CNG density) {
|
||||
CaveFieldModuleState(IrisCaveFieldModule module, NoiseProgram density) {
|
||||
IrisRange range = module.getVerticalRange();
|
||||
this.density = density;
|
||||
this.minY = (int) Math.floor(range.getMin());
|
||||
@@ -47,12 +47,12 @@ final class CaveFieldModuleState {
|
||||
}
|
||||
|
||||
double sample(double x, int y, double z) {
|
||||
double sampled = density.noiseFastSigned3D(x, y, z);
|
||||
double sampled = density.sampleSigned3D(x, y, z);
|
||||
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
|
||||
}
|
||||
|
||||
double sample(double x, double y, double z) {
|
||||
double sampled = density.noiseFastSigned3D(x, y, z);
|
||||
double sampled = density.sampleSigned3D(x, y, z);
|
||||
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,15 @@ package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationCounter;
|
||||
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
|
||||
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseScratch;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
@@ -43,6 +46,7 @@ import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
public class IrisCaveCarver3D {
|
||||
private static final byte LIQUID_AIR = 0;
|
||||
@@ -62,13 +66,23 @@ public class IrisCaveCarver3D {
|
||||
private static final int MAXIMUM_DENSITY_WORKERS = 4;
|
||||
private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17};
|
||||
private static final int STANDALONE_DENSITY_GRID_COLUMNS = 1024;
|
||||
private static final int[] CELL_CORNER_MASKS = {0b000011011, 0b000110110, 0b011011000, 0b110110000};
|
||||
private static final long[] CELL_Z_MASKS = {0x00FF00FF00FF00FFL, 0xFF00FF00FF00FF00L};
|
||||
private static final LongAdder SOLID_CELLS = new LongAdder();
|
||||
private static final LongAdder OPEN_CELLS = new LongAdder();
|
||||
private static final LongAdder MIXED_CELLS = new LongAdder();
|
||||
private static final LongAdder EXACT_ROWS = new LongAdder();
|
||||
private static final double WARP_B_OFFSET_X = 31.37D;
|
||||
private static final double WARP_B_OFFSET_Y = 17.21D;
|
||||
private static final double WARP_B_OFFSET_Z = 23.91D;
|
||||
private static final ThreadLocal<NoiseScratch> NOISE_SCRATCH = ThreadLocal.withInitial(NoiseScratch::new);
|
||||
|
||||
private final Engine engine;
|
||||
private final IrisData data;
|
||||
private final IrisCaveProfile profile;
|
||||
private final CNG baseDensity;
|
||||
private final CNG detailDensity;
|
||||
private final CNG warpDensity;
|
||||
private final NoiseProgram baseDensity;
|
||||
private final NoiseProgram detailDensity;
|
||||
private final NoiseProgram warpDensity;
|
||||
private final CNG surfaceBreakDensity;
|
||||
private final RNG thresholdRng;
|
||||
private final CaveFieldModuleState[] modules;
|
||||
@@ -98,7 +112,8 @@ public class IrisCaveCarver3D {
|
||||
private final ThreadLocal<Boolean> carving = ThreadLocal.withInitial(() -> false);
|
||||
private final ThreadLocal<CaveCarveScratch> scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new);
|
||||
private final CaveDensityGrid densityGrid;
|
||||
private final int densityGridOwner = CaveDensityGrid.nextOwner();
|
||||
private final CaveDensityGrid.Owner densityOwner;
|
||||
private final CaveDensityGrid.Owner warpOwner;
|
||||
|
||||
public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) {
|
||||
this(engine, profile, new CaveDensityGrid(STANDALONE_DENSITY_GRID_COLUMNS));
|
||||
@@ -116,9 +131,9 @@ public class IrisCaveCarver3D {
|
||||
List<CaveFieldModuleState> moduleStates = new ArrayList<>();
|
||||
|
||||
RNG baseRng = new RNG(engine.getSeedManager().getCarve());
|
||||
this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data);
|
||||
this.detailDensity = profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data);
|
||||
this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data);
|
||||
this.baseDensity = NoiseProgram.compileSigned3D(profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data));
|
||||
this.detailDensity = NoiseProgram.compileSigned3D(profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data));
|
||||
this.warpDensity = NoiseProgram.compileSigned3D(profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data));
|
||||
this.surfaceBreakDensity = profile.getSurfaceBreakStyle().create(baseRng.nextParallelRNG(341_219), data);
|
||||
this.thresholdRng = baseRng.nextParallelRNG(489_112);
|
||||
this.baseWeight = profile.getBaseWeight();
|
||||
@@ -136,7 +151,7 @@ public class IrisCaveCarver3D {
|
||||
int index = 0;
|
||||
for (IrisCaveFieldModule module : profile.getModules()) {
|
||||
CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data);
|
||||
CaveFieldModuleState state = new CaveFieldModuleState(module, moduleDensity);
|
||||
CaveFieldModuleState state = new CaveFieldModuleState(module, NoiseProgram.compileSigned3D(moduleDensity));
|
||||
moduleStates.add(state);
|
||||
weight += Math.abs(state.weight);
|
||||
index++;
|
||||
@@ -153,6 +168,25 @@ public class IrisCaveCarver3D {
|
||||
verticalDensityStep = parallelDensity ? Math.max(1, engine.getDimension().getCaveDensitySamplingStep()) : 1;
|
||||
worldMinimumY = engine.getWorld().minHeight();
|
||||
densityMaximumY = engine.getWorld().getHeight() - 1;
|
||||
CaveFieldKey warpKey = CaveFieldKey.of(warpDensity);
|
||||
warpOwner = densityGrid.owner(new WarpFieldKey(warpKey));
|
||||
densityOwner = densityGrid.owner(new DensityFieldKey(hasWarp, warpKey, Double.doubleToRawLongBits(warpStrength),
|
||||
CaveFieldKey.of(baseDensity), Double.doubleToRawLongBits(baseWeight), CaveFieldKey.of(detailDensity),
|
||||
Double.doubleToRawLongBits(detailWeight), Double.doubleToRawLongBits(inverseNormalization),
|
||||
moduleFieldKeys(modules)));
|
||||
}
|
||||
|
||||
private static List<ModuleFieldKey> moduleFieldKeys(CaveFieldModuleState[] modules) {
|
||||
List<ModuleFieldKey> keys = new ArrayList<>(modules.length);
|
||||
for (CaveFieldModuleState module : modules) {
|
||||
keys.add(new ModuleFieldKey(CaveFieldKey.of(module.density), module.minY, module.maxY,
|
||||
Double.doubleToRawLongBits(module.weight), Double.doubleToRawLongBits(module.threshold), module.invert));
|
||||
}
|
||||
return List.copyOf(keys);
|
||||
}
|
||||
|
||||
public static CellCounters cellCounters() {
|
||||
return new CellCounters(SOLID_CELLS.sum(), OPEN_CELLS.sum(), MIXED_CELLS.sum(), EXACT_ROWS.sum());
|
||||
}
|
||||
|
||||
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
|
||||
@@ -504,7 +538,9 @@ public class IrisCaveCarver3D {
|
||||
int minimumY = pass.minimumY() + index * rows;
|
||||
int maximumY = Math.min(pass.maximumY(), minimumY + rows - 1);
|
||||
if (minimumY <= maximumY) {
|
||||
tasks.add(() -> classifyRows(pass, minimumY, maximumY, classified));
|
||||
tasks.add(pass.cellKernel()
|
||||
? () -> classifyCellRows(pass, minimumY, maximumY, classified)
|
||||
: () -> classifyRows(pass, minimumY, maximumY, classified));
|
||||
}
|
||||
}
|
||||
runDensityTasks(tasks, pool);
|
||||
@@ -697,10 +733,25 @@ public class IrisCaveCarver3D {
|
||||
int[] columns, int[] columnTops, int columnCount, double[] thresholds,
|
||||
long[] fluidBoundaries, boolean[] surfaceBreakColumns, int[] surfaceBreakFloors,
|
||||
double surfaceBreakBoost, double[] verticalFade, CaveCarveScratch sourceScratch,
|
||||
int adaptiveStep, double adaptiveMargin, int[] fluidTops, MatterCavern[] matterByY
|
||||
int adaptiveStep, double adaptiveMargin, int[] fluidTops, MatterCavern[] matterByY, boolean cellKernel
|
||||
) {
|
||||
}
|
||||
|
||||
public record CellCounters(long solidCells, long openCells, long mixedCells, long exactRows) {
|
||||
public long decided() {
|
||||
return solidCells + openCells;
|
||||
}
|
||||
|
||||
public long evaluated() {
|
||||
return mixedCells;
|
||||
}
|
||||
|
||||
public String machineLine() {
|
||||
return "IRIS_GENPROBE_CARVE_CELLS solid=" + solidCells + " open=" + openCells + " mixed=" + mixedCells
|
||||
+ " exact_rows=" + exactRows;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DensityTask implements Runnable, ForkJoinPool.ManagedBlocker {
|
||||
private final Runnable work;
|
||||
private final AtomicBoolean claimed = new AtomicBoolean();
|
||||
@@ -795,7 +846,7 @@ public class IrisCaveCarver3D {
|
||||
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
|
||||
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
|
||||
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
|
||||
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, 0, 0D, fluidMaxY, matterByY));
|
||||
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, 0, 0D, fluidMaxY, matterByY, false));
|
||||
|
||||
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
|
||||
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
|
||||
@@ -944,12 +995,26 @@ public class IrisCaveCarver3D {
|
||||
adaptiveSampleStep,
|
||||
effectiveAdaptiveSampleStep
|
||||
);
|
||||
boolean cellKernel = !skipExistingCarved && usesCellKernel(effectiveAdaptiveSampleStep);
|
||||
if (cellKernel) {
|
||||
prepareCellPlan(scratch, x0, z0, minY, passMaxY, activeColumnIndices, activeColumnCount,
|
||||
surfaceFluidBoundaries, surfaceBreakThresholdBoost);
|
||||
}
|
||||
|
||||
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
|
||||
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
|
||||
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
|
||||
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, effectiveAdaptiveSampleStep, effectiveAdaptiveThresholdMargin,
|
||||
fluidMaxY, matterByY));
|
||||
fluidMaxY, matterByY, cellKernel));
|
||||
|
||||
if (cellKernel) {
|
||||
if (classified == null) {
|
||||
fillCellLattice(scratch, scratch, x0, z0, minY, passMaxY);
|
||||
}
|
||||
return emitCellPass(chunk, scratch, classified, x0, z0, minY, passMaxY, minSection, maxSection,
|
||||
activeColumnIndices, activeColumnTopY, activeColumnCount, surfaceFluidBoundaries,
|
||||
surfaceBreakThresholdBoost, verticalEdgeFade, fluidMaxY, matterByY, fluidSupportPlan);
|
||||
}
|
||||
|
||||
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
|
||||
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
|
||||
@@ -1059,6 +1124,629 @@ public class IrisCaveCarver3D {
|
||||
return adaptiveThresholdMargin + ((effectiveAdaptiveSampleStep - adaptiveSampleStep) * ADAPTIVE_DEEP_MARGIN_BOOST);
|
||||
}
|
||||
|
||||
private boolean usesCellKernel(int effectiveAdaptiveSampleStep) {
|
||||
return effectiveAdaptiveSampleStep == ADAPTIVE_DEEP_SAMPLE_STEP
|
||||
&& verticalDensityStep == 1
|
||||
&& parallelDensity
|
||||
&& (hasWarp || !hasModules);
|
||||
}
|
||||
|
||||
private void prepareCellPlan(
|
||||
CaveCarveScratch plan,
|
||||
int x0,
|
||||
int z0,
|
||||
int minY,
|
||||
int passMaxY,
|
||||
int[] activeColumnIndices,
|
||||
int activeColumnCount,
|
||||
long[] surfaceFluidBoundaries,
|
||||
double surfaceBreakThresholdBoost
|
||||
) {
|
||||
int rows = passMaxY - minY + 1;
|
||||
if (plan.rowActiveCount.length < rows + 1) {
|
||||
plan.rowActiveCount = new int[rows + 1];
|
||||
}
|
||||
int[] rowActiveCount = plan.rowActiveCount;
|
||||
Arrays.fill(rowActiveCount, 0, rows + 1, 0);
|
||||
Arrays.fill(plan.cellPassBits, 0L);
|
||||
Arrays.fill(plan.cellThresholdMin, Double.POSITIVE_INFINITY);
|
||||
Arrays.fill(plan.cellThresholdMax, Double.NEGATIVE_INFINITY);
|
||||
Arrays.fill(plan.cellThresholdMaxBoost, Double.NEGATIVE_INFINITY);
|
||||
Arrays.fill(plan.cellBreakFloorMin, Integer.MAX_VALUE);
|
||||
Arrays.fill(plan.cellCeilingFadeStart, Integer.MAX_VALUE);
|
||||
Arrays.fill(plan.cellTopY, Integer.MIN_VALUE);
|
||||
Arrays.fill(plan.cellMinTopY, Integer.MAX_VALUE);
|
||||
Arrays.fill(plan.cellProtected, false);
|
||||
prepareAdaptiveGeometry(plan, ADAPTIVE_DEEP_SAMPLE_STEP, 2, 3);
|
||||
double[] passThreshold = plan.passThreshold;
|
||||
for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) {
|
||||
int column = activeColumnIndices[activeIndex];
|
||||
int cell = ((column >> 7) << 1) | ((column >> 3) & 1);
|
||||
int top = plan.columnMaxY[column];
|
||||
double threshold = passThreshold[column];
|
||||
plan.cellPassBits[column >> 6] |= 1L << column;
|
||||
plan.cellThresholdMin[cell] = Math.min(plan.cellThresholdMin[cell], threshold);
|
||||
plan.cellThresholdMax[cell] = Math.max(plan.cellThresholdMax[cell], threshold);
|
||||
if (plan.surfaceBreakColumn[column]) {
|
||||
plan.cellThresholdMaxBoost[cell] = Math.max(plan.cellThresholdMaxBoost[cell], threshold + surfaceBreakThresholdBoost);
|
||||
plan.cellBreakFloorMin[cell] = Math.min(plan.cellBreakFloorMin[cell], plan.surfaceBreakFloorY[column]);
|
||||
} else {
|
||||
plan.cellThresholdMaxBoost[cell] = Math.max(plan.cellThresholdMaxBoost[cell], threshold);
|
||||
}
|
||||
if (plan.surfaceCeilingColumn[column]) {
|
||||
plan.cellCeilingFadeStart[cell] = Math.min(plan.cellCeilingFadeStart[cell], top - SURFACE_CEILING_FADE_DEPTH + 1);
|
||||
}
|
||||
plan.cellTopY[cell] = Math.max(plan.cellTopY[cell], top);
|
||||
plan.cellMinTopY[cell] = Math.min(plan.cellMinTopY[cell], top);
|
||||
int protectStart = SurfaceFluidBoundaryPlan.NO_BOUNDARY;
|
||||
int protectEnd = Integer.MIN_VALUE;
|
||||
if (surfaceFluidBoundaries != null) {
|
||||
long boundary = surfaceFluidBoundaries[column];
|
||||
protectStart = SurfaceFluidBoundaryPlan.startY(boundary);
|
||||
protectEnd = SurfaceFluidBoundaryPlan.endY(boundary);
|
||||
}
|
||||
plan.columnProtectStartY[column] = protectStart;
|
||||
plan.columnProtectEndY[column] = protectEnd;
|
||||
rowActiveCount[0]++;
|
||||
rowActiveCount[top - minY + 1]--;
|
||||
int protectedFrom = Math.max(protectStart, minY);
|
||||
int protectedTo = Math.min(protectEnd, top);
|
||||
if (protectedFrom <= protectedTo) {
|
||||
rowActiveCount[protectedFrom - minY]--;
|
||||
rowActiveCount[protectedTo - minY + 1]++;
|
||||
plan.cellProtected[cell] = true;
|
||||
}
|
||||
}
|
||||
for (int row = 1; row < rows; row++) {
|
||||
rowActiveCount[row] += rowActiveCount[row - 1];
|
||||
}
|
||||
int usedCorners = 0;
|
||||
for (int cell = 0; cell < 4; cell++) {
|
||||
if (plan.cellTopY[cell] >= minY) {
|
||||
usedCorners |= CELL_CORNER_MASKS[cell];
|
||||
}
|
||||
}
|
||||
prepareCellLattice(plan, x0 >> CaveDensityGrid.LATTICE_SHIFT, z0 >> CaveDensityGrid.LATTICE_SHIFT, minY, passMaxY,
|
||||
usedCorners);
|
||||
}
|
||||
|
||||
private void prepareCellLattice(CaveCarveScratch plan, int latticeX, int latticeZ, int minY, int passMaxY, int usedCorners) {
|
||||
CaveDensityLattice lattice = plan.cellLattice;
|
||||
lattice.reset(minY, passMaxY - minY + 1);
|
||||
for (int corner = 0; corner < CaveDensityLattice.CORNERS; corner++) {
|
||||
boolean used = (usedCorners & (1 << corner)) != 0;
|
||||
int cornerX = latticeX + (corner / 3);
|
||||
int cornerZ = latticeZ + (corner % 3);
|
||||
lattice.densityColumns[corner] = used ? densityGrid.column(densityOwner, densityMaximumY + 1, cornerX, cornerZ) : null;
|
||||
lattice.warpColumns[corner] = used && hasWarp
|
||||
? densityGrid.warpColumn(warpOwner, densityMaximumY + 2, cornerX, cornerZ) : null;
|
||||
}
|
||||
for (int y = minY; y <= passMaxY; y++) {
|
||||
if (plan.rowActiveCount[y - minY] < ADAPTIVE_MIN_PLANE_COLUMNS) {
|
||||
continue;
|
||||
}
|
||||
int cells = 0;
|
||||
int corners = 0;
|
||||
for (int cell = 0; cell < 4; cell++) {
|
||||
if (isCellActive(plan, cell, y)) {
|
||||
cells |= 1 << cell;
|
||||
corners |= CELL_CORNER_MASKS[cell];
|
||||
}
|
||||
}
|
||||
lattice.markRow(y, cells, corners);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillCellLattice(CaveCarveScratch scratch, CaveCarveScratch plan, int x0, int z0, int fromY, int toY) {
|
||||
CaveDensityLattice lattice = plan.cellLattice;
|
||||
scratch.ensureLatticeBatch(toY - fromY + 1);
|
||||
int[] latticeY = scratch.latticeY;
|
||||
double[] density = scratch.latticeDensity;
|
||||
long reads = 0L;
|
||||
long samples = 0L;
|
||||
for (int corner = 0; corner < CaveDensityLattice.CORNERS; corner++) {
|
||||
CaveDensityGrid.Column column = lattice.densityColumns[corner];
|
||||
int count = 0;
|
||||
for (int y = fromY; y <= toY; y++) {
|
||||
if (!lattice.needs(corner, y)) {
|
||||
continue;
|
||||
}
|
||||
reads++;
|
||||
double cached = column == null ? Double.NaN : column.get(y);
|
||||
if (Double.isNaN(cached)) {
|
||||
latticeY[count++] = y;
|
||||
} else {
|
||||
lattice.store(corner, y, cached);
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
continue;
|
||||
}
|
||||
samples += count;
|
||||
sampleLatticeColumn(scratch, lattice.warpColumns[corner], x0 + (corner / 3) * ADAPTIVE_DEEP_SAMPLE_STEP,
|
||||
z0 + (corner % 3) * ADAPTIVE_DEEP_SAMPLE_STEP, count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
lattice.store(corner, latticeY[index], density[index]);
|
||||
if (column != null) {
|
||||
column.set(latticeY[index], density[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.CAVE_LATTICE_READS.add(reads);
|
||||
GenerationCounter.CAVE_LATTICE_SAMPLES.add(samples);
|
||||
}
|
||||
}
|
||||
|
||||
private void sampleLatticeColumn(CaveCarveScratch scratch, CaveDensityGrid.Column warpColumn, int x, int z, int count) {
|
||||
int[] latticeY = scratch.latticeY;
|
||||
double[] density = scratch.latticeDensity;
|
||||
if (!hasWarp) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
density[index] = sampleDensityNoWarpNoModules(x, latticeY[index], z);
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolveLatticeWarp(scratch, warpColumn, x, z, count);
|
||||
double[] warpedX = scratch.latticeWarpedX;
|
||||
double[] warpedY = scratch.latticeWarpedY;
|
||||
double[] warpedZ = scratch.latticeWarpedZ;
|
||||
for (int index = 0; index < count; index++) {
|
||||
density[index] = baseDensity.sampleSigned3D(warpedX[index], warpedY[index], warpedZ[index]) * baseWeight;
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
density[index] += detailDensity.sampleSigned3D(warpedX[index], warpedY[index], warpedZ[index]) * detailWeight;
|
||||
}
|
||||
for (CaveFieldModuleState module : modules) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
int y = latticeY[index];
|
||||
if (y < module.minY || y > module.maxY) {
|
||||
continue;
|
||||
}
|
||||
double moduleDensity = module.density.sampleSigned3D(warpedX[index], warpedY[index], warpedZ[index]) - module.threshold;
|
||||
if (module.invert) {
|
||||
moduleDensity = -moduleDensity;
|
||||
}
|
||||
density[index] += moduleDensity * module.weight;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
density[index] = density[index] * inverseNormalization;
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveLatticeWarp(CaveCarveScratch scratch, CaveDensityGrid.Column warpColumn, int x, int z, int count) {
|
||||
int[] latticeY = scratch.latticeY;
|
||||
int[] snapY = scratch.latticeSnapY;
|
||||
double[] warpA = scratch.latticeWarpA;
|
||||
double[] warpB = scratch.latticeWarpB;
|
||||
int snapCount = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
int snap = snapWarp(latticeY[index]);
|
||||
if (snapCount == 0 || snapY[snapCount - 1] != snap) {
|
||||
snapY[snapCount++] = snap;
|
||||
}
|
||||
}
|
||||
long computed = 0L;
|
||||
int runStart = -1;
|
||||
for (int snapIndex = 0; snapIndex <= snapCount; snapIndex++) {
|
||||
boolean missing = false;
|
||||
if (snapIndex < snapCount) {
|
||||
int snap = snapY[snapIndex];
|
||||
double cachedA = warpColumn == null ? Double.NaN : warpColumn.get(snap);
|
||||
double cachedB = warpColumn == null ? Double.NaN : warpColumn.get(snap + 1);
|
||||
missing = Double.isNaN(cachedA) || Double.isNaN(cachedB);
|
||||
warpA[snapIndex] = cachedA;
|
||||
warpB[snapIndex] = cachedB;
|
||||
}
|
||||
if (runStart >= 0 && (!missing || snapY[snapIndex] != snapY[snapIndex - 1] + warpResolution)) {
|
||||
computeLatticeWarpRun(scratch, warpColumn, x, z, runStart, snapIndex);
|
||||
computed += snapIndex - runStart;
|
||||
runStart = -1;
|
||||
}
|
||||
if (missing && runStart < 0) {
|
||||
runStart = snapIndex;
|
||||
}
|
||||
}
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.CAVE_WARP_SAMPLES.add(computed);
|
||||
}
|
||||
double[] warpedX = scratch.latticeWarpedX;
|
||||
double[] warpedY = scratch.latticeWarpedY;
|
||||
double[] warpedZ = scratch.latticeWarpedZ;
|
||||
int snapIndex = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
int y = latticeY[index];
|
||||
int snap = snapWarp(y);
|
||||
while (snapY[snapIndex] != snap) {
|
||||
snapIndex++;
|
||||
}
|
||||
double a = warpA[snapIndex];
|
||||
double b = warpB[snapIndex];
|
||||
warpedX[index] = x + (a * warpStrength);
|
||||
warpedY[index] = y + (b * warpStrength);
|
||||
warpedZ[index] = z + ((a - b) * 0.5D * warpStrength);
|
||||
}
|
||||
}
|
||||
|
||||
private void computeLatticeWarpRun(CaveCarveScratch scratch, CaveDensityGrid.Column warpColumn, int x, int z, int from, int to) {
|
||||
int[] snapY = scratch.latticeSnapY;
|
||||
double[] warpA = scratch.latticeWarpA;
|
||||
double[] warpB = scratch.latticeWarpB;
|
||||
int length = to - from;
|
||||
if (length == 1) {
|
||||
warpA[from] = warpDensity.sampleSigned3D(x, snapY[from], z);
|
||||
} else {
|
||||
double[] column = scratch.latticeWarpedX;
|
||||
warpDensity.fillColumn3D(NOISE_SCRATCH.get(), x, z, snapY[from], warpResolution, length, column);
|
||||
System.arraycopy(column, 0, warpA, from, length);
|
||||
}
|
||||
for (int snapIndex = from; snapIndex < to; snapIndex++) {
|
||||
int snap = snapY[snapIndex];
|
||||
warpB[snapIndex] = warpDensity.sampleSigned3D(x + WARP_B_OFFSET_X, snap - WARP_B_OFFSET_Y, z + WARP_B_OFFSET_Z);
|
||||
if (warpColumn != null) {
|
||||
warpColumn.set(snap, warpA[snapIndex]);
|
||||
warpColumn.set(snap + 1, warpB[snapIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int emitCellPass(
|
||||
MantleChunk<Matter> chunk,
|
||||
CaveCarveScratch scratch,
|
||||
byte[] classified,
|
||||
int x0,
|
||||
int z0,
|
||||
int minY,
|
||||
int passMaxY,
|
||||
int minSection,
|
||||
int maxSection,
|
||||
int[] activeColumnIndices,
|
||||
int[] activeColumnTopY,
|
||||
int activeColumnCount,
|
||||
long[] surfaceFluidBoundaries,
|
||||
double surfaceBreakThresholdBoost,
|
||||
double[] verticalEdgeFade,
|
||||
int[] fluidMaxY,
|
||||
MatterCavern[] matterByY,
|
||||
CaveFluidSupportPlan fluidSupportPlan
|
||||
) {
|
||||
int carved = 0;
|
||||
long[] rowBits = scratch.rowCarveBits;
|
||||
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
|
||||
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
|
||||
int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
|
||||
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, sectionIndex);
|
||||
for (int y = sectionMinY; y <= sectionMaxY; y++) {
|
||||
int fadeIndex = y - minY;
|
||||
if (scratch.rowActiveCount[fadeIndex] == 0) {
|
||||
continue;
|
||||
}
|
||||
MatterCavern verticalMatter = matterByY[fadeIndex];
|
||||
if (classified != null) {
|
||||
carved += emitClassifiedRow(cavernSlice, classified, fadeIndex, y, verticalMatter, fluidSupportPlan);
|
||||
continue;
|
||||
}
|
||||
classifyAdaptiveRow(scratch, scratch, x0, z0, y, minY, activeColumnIndices, activeColumnTopY,
|
||||
activeColumnCount, surfaceFluidBoundaries, surfaceBreakThresholdBoost, verticalEdgeFade, rowBits);
|
||||
for (int word = 0; word < 4; word++) {
|
||||
long bits = rowBits[word];
|
||||
while (bits != 0L) {
|
||||
int column = (word << 6) | Long.numberOfTrailingZeros(bits);
|
||||
bits &= bits - 1L;
|
||||
int localX = PowerOfTwoCoordinates.unpackLocal16X(column);
|
||||
int localZ = column & 15;
|
||||
MatterCavern matter = verticalMatter;
|
||||
if (verticalMatter != carveLava && y <= fluidMaxY[column]) {
|
||||
matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ, column, fluidMaxY,
|
||||
cellThreshold(scratch, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade));
|
||||
}
|
||||
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
|
||||
carved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
flushCellCounters(scratch);
|
||||
return carved;
|
||||
}
|
||||
|
||||
private int emitClassifiedRow(MatterSlice<MatterCavern> cavernSlice, byte[] classified, int fadeIndex, int y,
|
||||
MatterCavern verticalMatter, CaveFluidSupportPlan fluidSupportPlan) {
|
||||
int carved = 0;
|
||||
int offset = fadeIndex * 256;
|
||||
for (int column = 0; column < 256; column++) {
|
||||
byte flags = classified[offset + column];
|
||||
if ((flags & DENSITY_CARVE) == 0) {
|
||||
continue;
|
||||
}
|
||||
MatterCavern matter = (flags & DENSITY_FLUID) != 0 ? carveFluid : verticalMatter;
|
||||
writeCavern(cavernSlice, PowerOfTwoCoordinates.unpackLocal16X(column), y, column & 15, matter, fluidSupportPlan);
|
||||
carved++;
|
||||
}
|
||||
return carved;
|
||||
}
|
||||
|
||||
private void classifyCellRows(DensityPass pass, int minimumY, int maximumY, byte[] classified) {
|
||||
CaveCarveScratch scratch = new CaveCarveScratch();
|
||||
CaveCarveScratch plan = pass.sourceScratch();
|
||||
fillCellLattice(scratch, plan, pass.x(), pass.z(), minimumY, maximumY);
|
||||
long[] rowBits = scratch.rowCarveBits;
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
int fadeIndex = y - pass.minimumY();
|
||||
if (plan.rowActiveCount[fadeIndex] == 0) {
|
||||
continue;
|
||||
}
|
||||
classifyAdaptiveRow(scratch, plan, pass.x(), pass.z(), y, pass.minimumY(), pass.columns(), pass.columnTops(),
|
||||
pass.columnCount(), pass.fluidBoundaries(), pass.surfaceBreakBoost(), pass.verticalFade(), rowBits);
|
||||
MatterCavern verticalMatter = pass.matterByY()[fadeIndex];
|
||||
int offset = fadeIndex * 256;
|
||||
for (int word = 0; word < 4; word++) {
|
||||
long bits = rowBits[word];
|
||||
while (bits != 0L) {
|
||||
int column = (word << 6) | Long.numberOfTrailingZeros(bits);
|
||||
bits &= bits - 1L;
|
||||
MatterCavern matter = verticalMatter;
|
||||
if (verticalMatter != carveLava && y <= pass.fluidTops()[column]) {
|
||||
matter = resolveMatter(scratch, verticalMatter, pass.x() + PowerOfTwoCoordinates.unpackLocal16X(column),
|
||||
y, pass.z() + (column & 15), column, pass.fluidTops(),
|
||||
cellThreshold(plan, column, y, pass.minimumY(), pass.surfaceBreakBoost(), pass.verticalFade()));
|
||||
}
|
||||
classified[offset + column] = matter == carveFluid ? DENSITY_CARVE | DENSITY_FLUID : DENSITY_CARVE;
|
||||
}
|
||||
}
|
||||
}
|
||||
flushCellCounters(scratch);
|
||||
}
|
||||
|
||||
private void classifyAdaptiveRow(
|
||||
CaveCarveScratch scratch,
|
||||
CaveCarveScratch plan,
|
||||
int x0,
|
||||
int z0,
|
||||
int y,
|
||||
int minY,
|
||||
int[] activeColumnIndices,
|
||||
int[] activeColumnTopY,
|
||||
int activeColumnCount,
|
||||
long[] surfaceFluidBoundaries,
|
||||
double surfaceBreakThresholdBoost,
|
||||
double[] verticalEdgeFade,
|
||||
long[] rowBits
|
||||
) {
|
||||
Arrays.fill(rowBits, 0L);
|
||||
if (plan.rowActiveCount[y - minY] >= ADAPTIVE_MIN_PLANE_COLUMNS) {
|
||||
classifyCellRow(scratch, plan, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade, rowBits);
|
||||
return;
|
||||
}
|
||||
int[] planeColumnIndices = scratch.planeColumnIndices;
|
||||
double[] planeThresholdLimit = scratch.planeThresholdLimit;
|
||||
boolean[] planeCarve = scratch.planeCarve;
|
||||
int planeCount = 0;
|
||||
for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) {
|
||||
if (activeColumnTopY[activeIndex] < y) {
|
||||
continue;
|
||||
}
|
||||
int columnIndex = activeColumnIndices[activeIndex];
|
||||
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
|
||||
continue;
|
||||
}
|
||||
planeColumnIndices[planeCount] = columnIndex;
|
||||
planeThresholdLimit[planeCount] = cellLocalThreshold(plan, columnIndex, y, minY, surfaceBreakThresholdBoost,
|
||||
verticalEdgeFade) * normalizationFactor;
|
||||
planeCount++;
|
||||
}
|
||||
classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
|
||||
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
|
||||
if (planeCarve[planeIndex]) {
|
||||
int columnIndex = planeColumnIndices[planeIndex];
|
||||
rowBits[columnIndex >> 6] |= 1L << columnIndex;
|
||||
}
|
||||
}
|
||||
scratch.exactRows++;
|
||||
}
|
||||
|
||||
private void classifyCellRow(
|
||||
CaveCarveScratch scratch,
|
||||
CaveCarveScratch plan,
|
||||
int y,
|
||||
int minY,
|
||||
double surfaceBreakThresholdBoost,
|
||||
double[] verticalEdgeFade,
|
||||
long[] rowBits
|
||||
) {
|
||||
CaveDensityLattice lattice = plan.cellLattice;
|
||||
int activeCells = lattice.cells(y);
|
||||
if (activeCells == 0) {
|
||||
return;
|
||||
}
|
||||
int fadeIndex = y - minY;
|
||||
double fade = verticalEdgeFade[fadeIndex];
|
||||
double closure = plan.surfaceClosureThreshold[fadeIndex];
|
||||
for (int cell = 0; cell < 4; cell++) {
|
||||
if ((activeCells & (1 << cell)) == 0) {
|
||||
continue;
|
||||
}
|
||||
int corner = ((cell >> 1) * 3) + (cell & 1);
|
||||
double d00 = lattice.density(corner, y);
|
||||
double d01 = lattice.density(corner + 1, y);
|
||||
double d10 = lattice.density(corner + 3, y);
|
||||
double d11 = lattice.density(corner + 4, y);
|
||||
double low = plan.cellThresholdMin[cell] - fade;
|
||||
if (y >= plan.cellCeilingFadeStart[cell]) {
|
||||
low = Math.min(low, closure);
|
||||
}
|
||||
double high = (y >= plan.cellBreakFloorMin[cell] ? plan.cellThresholdMaxBoost[cell] : plan.cellThresholdMax[cell]) - fade;
|
||||
int decision = CaveCellKernel.decide(d00, d01, d10, d11,
|
||||
CaveCellKernel.threshold(low, normalizationFactor, inverseNormalization),
|
||||
CaveCellKernel.threshold(high, normalizationFactor, inverseNormalization));
|
||||
boolean uniform = !plan.cellProtected[cell] && y <= plan.cellMinTopY[cell];
|
||||
if (decision == CaveCellKernel.NONE) {
|
||||
scratch.solidCells++;
|
||||
} else if (decision == CaveCellKernel.ALL) {
|
||||
scratch.openCells++;
|
||||
if (uniform) {
|
||||
int word = (cell >> 1) << 1;
|
||||
long mask = CELL_Z_MASKS[cell & 1];
|
||||
rowBits[word] |= plan.cellPassBits[word] & mask;
|
||||
rowBits[word + 1] |= plan.cellPassBits[word + 1] & mask;
|
||||
} else {
|
||||
markActiveCellColumns(plan, cell, y, rowBits);
|
||||
}
|
||||
} else {
|
||||
scratch.mixedCells++;
|
||||
if (uniform && y < plan.cellCeilingFadeStart[cell] && y < plan.cellBreakFloorMin[cell]) {
|
||||
classifyPlainMixedCell(plan, cell, fade, d00, d01, d10, d11, rowBits);
|
||||
} else {
|
||||
classifyMixedCell(plan, cell, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade, d00, d01, d10, d11, rowBits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void classifyMixedCell(
|
||||
CaveCarveScratch plan,
|
||||
int cell,
|
||||
int y,
|
||||
int minY,
|
||||
double surfaceBreakThresholdBoost,
|
||||
double[] verticalEdgeFade,
|
||||
double d00,
|
||||
double d01,
|
||||
double d10,
|
||||
double d11,
|
||||
long[] rowBits
|
||||
) {
|
||||
double[] adaptiveTx = plan.adaptiveTx;
|
||||
double[] adaptiveTz = plan.adaptiveTz;
|
||||
int firstLocalX = (cell >> 1) << 3;
|
||||
int firstLocalZ = (cell & 1) << 3;
|
||||
for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) {
|
||||
int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ);
|
||||
double tx = adaptiveTx[rowColumn];
|
||||
double dx0 = d00 + ((d10 - d00) * tx);
|
||||
double dx1 = d01 + ((d11 - d01) * tx);
|
||||
double slope = dx1 - dx0;
|
||||
for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) {
|
||||
if (!isCellColumnActive(plan, column, y)) {
|
||||
continue;
|
||||
}
|
||||
double predictedDensity = dx0 + (slope * adaptiveTz[column]);
|
||||
if (predictedDensity <= cellThreshold(plan, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade)) {
|
||||
rowBits[column >> 6] |= 1L << column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void classifyPlainMixedCell(
|
||||
CaveCarveScratch plan,
|
||||
int cell,
|
||||
double fade,
|
||||
double d00,
|
||||
double d01,
|
||||
double d10,
|
||||
double d11,
|
||||
long[] rowBits
|
||||
) {
|
||||
double[] passThreshold = plan.passThreshold;
|
||||
double[] adaptiveTx = plan.adaptiveTx;
|
||||
double[] adaptiveTz = plan.adaptiveTz;
|
||||
int firstLocalX = (cell >> 1) << 3;
|
||||
int firstLocalZ = (cell & 1) << 3;
|
||||
for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) {
|
||||
int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ);
|
||||
double tx = adaptiveTx[rowColumn];
|
||||
double dx0 = d00 + ((d10 - d00) * tx);
|
||||
double dx1 = d01 + ((d11 - d01) * tx);
|
||||
double slope = dx1 - dx0;
|
||||
long bits = 0L;
|
||||
for (int offset = 0; offset < ADAPTIVE_DEEP_SAMPLE_STEP; offset++) {
|
||||
int column = rowColumn + offset;
|
||||
double predictedDensity = dx0 + (slope * adaptiveTz[column]);
|
||||
if (predictedDensity <= CaveCellKernel.threshold(passThreshold[column] - fade, normalizationFactor,
|
||||
inverseNormalization)) {
|
||||
bits |= 1L << offset;
|
||||
}
|
||||
}
|
||||
rowBits[rowColumn >> 6] |= bits << (rowColumn & 63);
|
||||
}
|
||||
}
|
||||
|
||||
private static void markActiveCellColumns(CaveCarveScratch plan, int cell, int y, long[] rowBits) {
|
||||
int firstLocalX = (cell >> 1) << 3;
|
||||
int firstLocalZ = (cell & 1) << 3;
|
||||
for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) {
|
||||
int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ);
|
||||
for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) {
|
||||
if (isCellColumnActive(plan, column, y)) {
|
||||
rowBits[column >> 6] |= 1L << column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isCellActive(CaveCarveScratch plan, int cell, int y) {
|
||||
if (y > plan.cellTopY[cell]) {
|
||||
return false;
|
||||
}
|
||||
if (!plan.cellProtected[cell]) {
|
||||
return true;
|
||||
}
|
||||
int firstLocalX = (cell >> 1) << 3;
|
||||
int firstLocalZ = (cell & 1) << 3;
|
||||
for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) {
|
||||
int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ);
|
||||
for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) {
|
||||
if (isCellColumnActive(plan, column, y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isCellColumnActive(CaveCarveScratch plan, int column, int y) {
|
||||
return (plan.cellPassBits[column >> 6] & (1L << column)) != 0L
|
||||
&& plan.columnMaxY[column] >= y
|
||||
&& (y < plan.columnProtectStartY[column] || y > plan.columnProtectEndY[column]);
|
||||
}
|
||||
|
||||
private double cellThreshold(CaveCarveScratch plan, int column, int y, int minY, double surfaceBreakThresholdBoost,
|
||||
double[] verticalEdgeFade) {
|
||||
return CaveCellKernel.threshold(cellLocalThreshold(plan, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade),
|
||||
normalizationFactor, inverseNormalization);
|
||||
}
|
||||
|
||||
private double cellLocalThreshold(CaveCarveScratch plan, int column, int y, int minY, double surfaceBreakThresholdBoost,
|
||||
double[] verticalEdgeFade) {
|
||||
double localThreshold = plan.passThreshold[column];
|
||||
if (plan.surfaceBreakColumn[column] && y >= plan.surfaceBreakFloorY[column]) {
|
||||
localThreshold += surfaceBreakThresholdBoost;
|
||||
}
|
||||
localThreshold -= verticalEdgeFade[y - minY];
|
||||
return applySurfaceCeilingFade(plan, localThreshold, column, y, minY);
|
||||
}
|
||||
|
||||
private static void flushCellCounters(CaveCarveScratch scratch) {
|
||||
if (scratch.solidCells != 0L) {
|
||||
SOLID_CELLS.add(scratch.solidCells);
|
||||
scratch.solidCells = 0L;
|
||||
}
|
||||
if (scratch.openCells != 0L) {
|
||||
OPEN_CELLS.add(scratch.openCells);
|
||||
scratch.openCells = 0L;
|
||||
}
|
||||
if (scratch.mixedCells != 0L) {
|
||||
MIXED_CELLS.add(scratch.mixedCells);
|
||||
scratch.mixedCells = 0L;
|
||||
}
|
||||
if (scratch.exactRows != 0L) {
|
||||
EXACT_ROWS.add(scratch.exactRows);
|
||||
scratch.exactRows = 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private int carvePassLattice(
|
||||
MantleChunk<Matter> chunk,
|
||||
int x0,
|
||||
@@ -2131,7 +2819,7 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
private boolean classifyDensityPointNoWarpNoModules(int x, int y, int z, double thresholdLimit) {
|
||||
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
|
||||
double density = baseDensity.sampleSigned3D(x, y, z) * baseWeight;
|
||||
if ((density + detailMinContribution) > thresholdLimit) {
|
||||
return false;
|
||||
}
|
||||
@@ -2139,7 +2827,7 @@ public class IrisCaveCarver3D {
|
||||
return true;
|
||||
}
|
||||
|
||||
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
|
||||
density += detailDensity.sampleSigned3D(x, y, z) * detailWeight;
|
||||
return density <= thresholdLimit;
|
||||
}
|
||||
|
||||
@@ -2157,7 +2845,7 @@ public class IrisCaveCarver3D {
|
||||
return classifyDensityPointNoWarpNoModules(x, y, z, thresholdLimit);
|
||||
}
|
||||
|
||||
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
|
||||
double density = baseDensity.sampleSigned3D(x, y, z) * baseWeight;
|
||||
if ((density + detailMinContribution + remainingMin[0]) > thresholdLimit) {
|
||||
return false;
|
||||
}
|
||||
@@ -2165,7 +2853,7 @@ public class IrisCaveCarver3D {
|
||||
return true;
|
||||
}
|
||||
|
||||
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
|
||||
density += detailDensity.sampleSigned3D(x, y, z) * detailWeight;
|
||||
if ((density + remainingMin[0]) > thresholdLimit) {
|
||||
return false;
|
||||
}
|
||||
@@ -2207,7 +2895,7 @@ public class IrisCaveCarver3D {
|
||||
double warpedX = x + (warpA * warpStrength);
|
||||
double warpedY = y + (warpB * warpStrength);
|
||||
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
|
||||
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
|
||||
double density = baseDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
|
||||
if ((density + detailMinContribution) > thresholdLimit) {
|
||||
return false;
|
||||
}
|
||||
@@ -2215,7 +2903,7 @@ public class IrisCaveCarver3D {
|
||||
return true;
|
||||
}
|
||||
|
||||
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
|
||||
density += detailDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
|
||||
return density <= thresholdLimit;
|
||||
}
|
||||
|
||||
@@ -2243,7 +2931,7 @@ public class IrisCaveCarver3D {
|
||||
double warpedX = x + (warpA * warpStrength);
|
||||
double warpedY = y + (warpB * warpStrength);
|
||||
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
|
||||
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
|
||||
double density = baseDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
|
||||
if ((density + detailMinContribution + remainingMin[0]) > thresholdLimit) {
|
||||
return false;
|
||||
}
|
||||
@@ -2251,7 +2939,7 @@ public class IrisCaveCarver3D {
|
||||
return true;
|
||||
}
|
||||
|
||||
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
|
||||
density += detailDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
|
||||
if ((density + remainingMin[0]) > thresholdLimit) {
|
||||
return false;
|
||||
}
|
||||
@@ -2477,8 +3165,8 @@ public class IrisCaveCarver3D {
|
||||
int latticeZ = z >> CaveDensityGrid.LATTICE_SHIFT;
|
||||
int slot = ((latticeX & 3) << 2) | (latticeZ & 3);
|
||||
CaveDensityGrid.Column column = parallelDensity ? scratch.densityColumns[slot] : null;
|
||||
if (column == null || !column.matches(this, latticeX, latticeZ)) {
|
||||
column = densityGrid.column(this, densityGridOwner, densityMaximumY + 1, latticeX, latticeZ);
|
||||
if (column == null || !column.matches(densityOwner, latticeX, latticeZ)) {
|
||||
column = densityGrid.column(densityOwner, densityMaximumY + 1, latticeX, latticeZ);
|
||||
if (parallelDensity) {
|
||||
scratch.densityColumns[slot] = column;
|
||||
}
|
||||
@@ -2507,8 +3195,8 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
private double sampleDensityNoWarpNoModules(int x, int y, int z) {
|
||||
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
|
||||
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
|
||||
double density = baseDensity.sampleSigned3D(x, y, z) * baseWeight;
|
||||
density += detailDensity.sampleSigned3D(x, y, z) * detailWeight;
|
||||
return density * inverseNormalization;
|
||||
}
|
||||
|
||||
@@ -2519,11 +3207,11 @@ public class IrisCaveCarver3D {
|
||||
}
|
||||
|
||||
CaveFieldModuleState[] localModules = scratch.activeModules;
|
||||
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
|
||||
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
|
||||
double density = baseDensity.sampleSigned3D(x, y, z) * baseWeight;
|
||||
density += detailDensity.sampleSigned3D(x, y, z) * detailWeight;
|
||||
for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) {
|
||||
CaveFieldModuleState module = localModules[moduleIndex];
|
||||
double moduleDensity = module.density.noiseFastSigned3D(x, y, z) - module.threshold;
|
||||
double moduleDensity = module.density.sampleSigned3D(x, y, z) - module.threshold;
|
||||
if (module.invert) {
|
||||
moduleDensity = -moduleDensity;
|
||||
}
|
||||
@@ -2544,8 +3232,8 @@ public class IrisCaveCarver3D {
|
||||
double warpedX = x + (warpA * warpStrength);
|
||||
double warpedY = y + (warpB * warpStrength);
|
||||
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
|
||||
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
|
||||
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
|
||||
double density = baseDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
|
||||
density += detailDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
|
||||
return density * inverseNormalization;
|
||||
}
|
||||
|
||||
@@ -2569,11 +3257,11 @@ public class IrisCaveCarver3D {
|
||||
double warpedX = x + (warpA * warpStrength);
|
||||
double warpedY = y + (warpB * warpStrength);
|
||||
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
|
||||
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
|
||||
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
|
||||
double density = baseDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
|
||||
density += detailDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
|
||||
for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) {
|
||||
CaveFieldModuleState module = localModules[moduleIndex];
|
||||
double moduleDensity = module.density.noiseFastSigned3D(warpedX, warpedY, warpedZ) - module.threshold;
|
||||
double moduleDensity = module.density.sampleSigned3D(warpedX, warpedY, warpedZ) - module.threshold;
|
||||
if (module.invert) {
|
||||
moduleDensity = -moduleDensity;
|
||||
}
|
||||
@@ -2597,8 +3285,8 @@ public class IrisCaveCarver3D {
|
||||
scratch.warpCacheX[slot] = sx;
|
||||
scratch.warpCacheY[slot] = sy;
|
||||
scratch.warpCacheZ[slot] = sz;
|
||||
scratch.warpCacheA[slot] = warpDensity.noiseFastSigned3D(sx, sy, sz);
|
||||
scratch.warpCacheB[slot] = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D);
|
||||
scratch.warpCacheA[slot] = warpDensity.sampleSigned3D(sx, sy, sz);
|
||||
scratch.warpCacheB[slot] = warpDensity.sampleSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D);
|
||||
return slot;
|
||||
}
|
||||
|
||||
@@ -2714,7 +3402,7 @@ public class IrisCaveCarver3D {
|
||||
private boolean isAquiferCandidate(CaveCarveScratch scratch, int x, int y, int z, double localThreshold) {
|
||||
double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D));
|
||||
double cutoff = 0.35D + (depthFactor * 0.2D);
|
||||
if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) {
|
||||
if (detailDensity.sampleSigned3D(x, y * 0.5D, z) <= cutoff) {
|
||||
return false;
|
||||
}
|
||||
return !fluidRequiresFloor || hasAquiferCupSupport(scratch, x, y, z, localThreshold);
|
||||
@@ -2887,4 +3575,15 @@ public class IrisCaveCarver3D {
|
||||
|
||||
return verticalEdgeFade;
|
||||
}
|
||||
|
||||
private record WarpFieldKey(CaveFieldKey field) {
|
||||
}
|
||||
|
||||
private record ModuleFieldKey(CaveFieldKey field, int minY, int maxY, long weight, long threshold, boolean invert) {
|
||||
}
|
||||
|
||||
private record DensityFieldKey(boolean warp, CaveFieldKey warpField, long warpStrength, CaveFieldKey base, long baseWeight,
|
||||
CaveFieldKey detail, long detailWeight, long inverseNormalization,
|
||||
List<ModuleFieldKey> modules) {
|
||||
}
|
||||
}
|
||||
+60
-37
@@ -44,7 +44,6 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
@@ -598,7 +597,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
@Override
|
||||
protected int computeRadius() {
|
||||
int maxObjectExtent = 0;
|
||||
Map<String, IrisBlockVector> sizeCache = new HashMap<>();
|
||||
Map<String, PlacementReach.Extent> extents = new HashMap<>();
|
||||
Set<String> warnedLargeObjects = new HashSet<>();
|
||||
try {
|
||||
IrisData data = getData();
|
||||
@@ -608,15 +607,17 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
continue;
|
||||
}
|
||||
for (IrisFloatingChildBiomes entry : entries) {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getFloatingObjects(), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getExtraObjects(), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getTopObjectOverrides(), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getBottomObjectOverrides(), data, sizeCache, warnedLargeObjects));
|
||||
PlacementScan scan = new PlacementScan(data, extents, warnedLargeObjects,
|
||||
entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D);
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getFloatingObjects(), false));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getExtraObjects(), true));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getTopObjectOverrides(), true));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getBottomObjectOverrides(), true));
|
||||
try {
|
||||
IrisBiome target = entry.getRealBiome(biome, data);
|
||||
if (target != null) {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveTopObjects(target), true));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveBottomObjects(target), true));
|
||||
} else {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
|
||||
}
|
||||
@@ -635,44 +636,66 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
return maxObjectExtent;
|
||||
}
|
||||
|
||||
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
|
||||
Map<String, IrisBlockVector> sizeCache,
|
||||
Set<String> warnedLargeObjects) {
|
||||
if (placements == null) {
|
||||
return MINIMUM_PLACEMENT_RADIUS;
|
||||
private final class PlacementScan {
|
||||
private final IrisData data;
|
||||
private final Map<String, PlacementReach.Extent> extents;
|
||||
private final Set<String> warnedLargeObjects;
|
||||
private final double shrink;
|
||||
|
||||
private PlacementScan(IrisData data, Map<String, PlacementReach.Extent> extents, Set<String> warnedLargeObjects,
|
||||
double shrink) {
|
||||
this.data = data;
|
||||
this.extents = extents;
|
||||
this.warnedLargeObjects = warnedLargeObjects;
|
||||
this.shrink = shrink;
|
||||
}
|
||||
if (placements.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int radius = MINIMUM_PLACEMENT_RADIUS;
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null || placement.getPlace() == null) {
|
||||
continue;
|
||||
|
||||
private int radius(KList<IrisObjectPlacement> placements, boolean anchored) {
|
||||
if (placements == null) {
|
||||
return MINIMUM_PLACEMENT_RADIUS;
|
||||
}
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
IrisBlockVector size = sizeCache.get(objectKey);
|
||||
if (size == null) {
|
||||
File file = data.getObjectLoader().findFile(objectKey);
|
||||
if (file == null) {
|
||||
if (placements.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int radius = MINIMUM_PLACEMENT_RADIUS;
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null || placement.getPlace() == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Profile profile = PlacementReach.profile(placement);
|
||||
double scale = placement.getMaximumScale(getDimension());
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
PlacementReach.Extent extent = extent(objectKey);
|
||||
if (extent == null) {
|
||||
continue;
|
||||
}
|
||||
size = IrisObject.sampleSize(file);
|
||||
sizeCache.put(objectKey, size);
|
||||
PlacementReach.Extent scaled = extent.scaled(scale).scaled(shrink);
|
||||
int reach = anchored ? profile.anchored(scaled).horizontal() : profile.of(scaled).horizontal();
|
||||
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
|
||||
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
|
||||
}
|
||||
radius = Math.max(radius, reach);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
int reach = MantleObjectComponent.calculatePlacementReach(size, placement, placement.getMaximumScale(getDimension()));
|
||||
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
|
||||
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
|
||||
}
|
||||
radius = Math.max(radius, reach);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private PlacementReach.Extent extent(String objectKey) {
|
||||
if (extents.containsKey(objectKey)) {
|
||||
return extents.get(objectKey);
|
||||
}
|
||||
IrisObject object = data.getObjectLoader().load(objectKey);
|
||||
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
|
||||
extents.put(objectKey, extent);
|
||||
return extent;
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private static final class PlacementSequence {
|
||||
private int ordinal;
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainPlan;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
@@ -69,12 +68,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.block.DataProvider;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -94,6 +93,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
private final ObjectSourcePlanCache sourcePlans;
|
||||
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
|
||||
private volatile int collisionRuleState;
|
||||
private volatile int inputReach;
|
||||
|
||||
public MantleObjectComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.OBJECT, 2);
|
||||
@@ -134,7 +134,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
|
||||
@Override
|
||||
public int getInputRadius() {
|
||||
return calculateInputRadius(getRadius(), hasCollisionRules());
|
||||
int radius = getRadius();
|
||||
return calculateInputRadius(radius, inputReach, hasCollisionRules());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -297,11 +298,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
|
||||
}
|
||||
|
||||
static int calculateInputRadius(int radius, boolean sourceAnchoredCollisions) {
|
||||
static int calculateInputRadius(int radius, int inputReach, boolean sourceAnchoredCollisions) {
|
||||
int normalizedRadius = Math.max(0, radius);
|
||||
long sourceChunkRadius = sourceChunkRadius(normalizedRadius);
|
||||
long sourceLegs = sourceAnchoredCollisions ? 2L : 1L;
|
||||
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + normalizedRadius + 1L;
|
||||
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + Math.max(normalizedRadius, inputReach) + 1L;
|
||||
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
|
||||
}
|
||||
|
||||
@@ -1370,7 +1371,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
int id = rng.i(0, Integer.MAX_VALUE);
|
||||
IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey());
|
||||
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
placement.setRotation(buildUpsideDownRotation());
|
||||
placement.setRotation(upsideDownRotation());
|
||||
placement.setCarvingSupport(CarvingMode.ANYWHERE);
|
||||
if (forcePlace) {
|
||||
placement.setForcePlace(true);
|
||||
@@ -1408,7 +1409,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private IrisObjectRotation buildUpsideDownRotation() {
|
||||
private static IrisObjectRotation upsideDownRotation() {
|
||||
IrisObjectRotation rt = new IrisObjectRotation();
|
||||
rt.setEnabled(true);
|
||||
rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D));
|
||||
@@ -1785,135 +1786,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
|
||||
protected int computeRadius() {
|
||||
KSet<String> warnedLargeObjects = new KSet<>();
|
||||
int radius = computeDimensionRadius(getDimension(), this::getData, warnedLargeObjects);
|
||||
PlacementReach reach = new ReachScan(getDimension(), this::getData, warnedLargeObjects).dimension();
|
||||
UpperDimensionContext upperContext = activeUpperContext();
|
||||
if (upperContext != null) {
|
||||
radius = Math.max(radius, computeDimensionRadius(
|
||||
upperContext.getDimension(),
|
||||
upperContext,
|
||||
warnedLargeObjects
|
||||
));
|
||||
reach = reach.union(new ReachScan(upperContext.getDimension(), upperContext, warnedLargeObjects).upperDimension());
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private int computeDimensionRadius(
|
||||
IrisDimension dimension,
|
||||
DataProvider dataProvider,
|
||||
KSet<String> warnedLargeObjects
|
||||
) {
|
||||
KMap<String, IrisBlockVector> sizeCache = new KMap<>();
|
||||
int radius = 0;
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
radius = Math.max(radius, computePlacementRadius(
|
||||
region.getObjects(),
|
||||
dimension,
|
||||
dataProvider,
|
||||
sizeCache,
|
||||
warnedLargeObjects
|
||||
));
|
||||
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dimension, dataProvider));
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
|
||||
if (biome == null) {
|
||||
continue;
|
||||
}
|
||||
radius = Math.max(radius, computePlacementRadius(
|
||||
biome.getObjects(),
|
||||
dimension,
|
||||
dataProvider,
|
||||
sizeCache,
|
||||
warnedLargeObjects
|
||||
));
|
||||
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dimension, dataProvider));
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private int computePlacementRadius(
|
||||
KList<IrisObjectPlacement> placements,
|
||||
IrisDimension dimension,
|
||||
DataProvider dataProvider,
|
||||
KMap<String, IrisBlockVector> sizeCache,
|
||||
KSet<String> warnedLargeObjects
|
||||
) {
|
||||
int radius = 0;
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
IrisBlockVector size = loadObjectSize(dataProvider, sizeCache, objectKey);
|
||||
if (size == null) {
|
||||
continue;
|
||||
}
|
||||
int reach = calculatePlacementReach(size, placement, placement.getMaximumScale(dimension));
|
||||
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
|
||||
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
|
||||
}
|
||||
radius = Math.max(radius, reach);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private int computeProceduralRadius(IrisProceduralObjects procedural, IrisDimension dimension, DataProvider dataProvider) {
|
||||
if (procedural == null || procedural.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int radius = 0;
|
||||
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
|
||||
if (variants == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObjectPlacement objectPlacement = placement.asPlacement();
|
||||
for (IrisObject variant : variants) {
|
||||
if (variant == null) {
|
||||
continue;
|
||||
}
|
||||
IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD());
|
||||
radius = Math.max(radius, calculatePlacementReach(size, objectPlacement, objectPlacement.getMaximumScale(dimension)));
|
||||
}
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement, double scale) {
|
||||
if (size == null) {
|
||||
return 0;
|
||||
}
|
||||
if (placement == null) {
|
||||
return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ()));
|
||||
}
|
||||
|
||||
int width = scaledDimension(size.getBlockX(), scale);
|
||||
int height = scaledDimension(size.getBlockY(), scale);
|
||||
int depth = scaledDimension(size.getBlockZ(), scale);
|
||||
IrisObjectRotation rotation = placement.getRotation();
|
||||
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
|
||||
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
|
||||
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
|
||||
int translation = calculateTranslationReach(placement.getTranslate(), rotation, rotateX || rotateZ);
|
||||
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
|
||||
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
|
||||
: 0;
|
||||
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
|
||||
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
|
||||
: 0;
|
||||
long reach = (long) footprint + translation + warp + vacuum;
|
||||
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
|
||||
inputReach = reach.input();
|
||||
return reach.horizontal();
|
||||
}
|
||||
|
||||
static boolean allowsObjectPlacement(
|
||||
@@ -1926,9 +1805,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
if (complex == null || object == null) {
|
||||
return true;
|
||||
}
|
||||
int reach = calculatePlacementReach(
|
||||
new IrisBlockVector(object.getW(), object.getH(), object.getD()),
|
||||
placement, 1D);
|
||||
int reach = transitionFootprint(object, placement);
|
||||
return complex.allowsNewGenerationFootprint(
|
||||
saturatedOffset(anchorX, -reach),
|
||||
saturatedOffset(anchorZ, -reach),
|
||||
@@ -1936,20 +1813,34 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
saturatedOffset(anchorZ, reach));
|
||||
}
|
||||
|
||||
private static int transitionFootprint(IrisObject object, IrisObjectPlacement placement) {
|
||||
int width = Math.abs(object.getW());
|
||||
int height = Math.abs(object.getH());
|
||||
int depth = Math.abs(object.getD());
|
||||
if (placement == null) {
|
||||
return Math.max(width, depth);
|
||||
}
|
||||
IrisObjectRotation rotation = placement.getRotation();
|
||||
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
|
||||
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
|
||||
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
|
||||
int translation = transitionTranslation(placement.getTranslate(), rotation, rotateX || rotateZ);
|
||||
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
|
||||
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
|
||||
: 0;
|
||||
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
|
||||
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
|
||||
: 0;
|
||||
long reach = (long) footprint + translation + warp + vacuum;
|
||||
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
|
||||
}
|
||||
|
||||
private static int saturatedOffset(int coordinate, int offset) {
|
||||
long result = (long) coordinate + offset;
|
||||
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
|
||||
}
|
||||
|
||||
private static int scaledDimension(int dimension, double scale) {
|
||||
int absoluteDimension = Math.abs(dimension);
|
||||
if (scale == 1D) {
|
||||
return absoluteDimension;
|
||||
}
|
||||
return (int) Math.ceil(absoluteDimension * scale);
|
||||
}
|
||||
|
||||
private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
|
||||
private static int transitionTranslation(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
|
||||
if (translate == null || !translate.canTranslate()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -1963,22 +1854,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return rotateY ? (int) Math.ceil(Math.hypot(x, z)) : (int) Math.max(Math.abs(x), Math.abs(z));
|
||||
}
|
||||
|
||||
private IrisBlockVector loadObjectSize(
|
||||
DataProvider dataProvider,
|
||||
KMap<String, IrisBlockVector> sizeCache,
|
||||
String objectKey
|
||||
) {
|
||||
return sizeCache.computeIfAbsent(objectKey, k -> {
|
||||
try {
|
||||
return IrisObject.sampleSize(dataProvider.getData().getObjectLoader().findFile(objectKey));
|
||||
} catch (IOException e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private UpperDimensionContext activeUpperContext() {
|
||||
if (!getDimension().isUpperDimensionObjects()) {
|
||||
return null;
|
||||
@@ -2047,6 +1922,187 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ReachScan {
|
||||
private final IrisDimension dimension;
|
||||
private final DataProvider dataProvider;
|
||||
private final KSet<String> warnedLargeObjects;
|
||||
private final Map<String, PlacementReach.Extent> extents;
|
||||
private final Set<ObjectPlaceMode> caveVacuumModes;
|
||||
|
||||
private ReachScan(IrisDimension dimension, DataProvider dataProvider, KSet<String> warnedLargeObjects) {
|
||||
this.dimension = dimension;
|
||||
this.dataProvider = dataProvider;
|
||||
this.warnedLargeObjects = warnedLargeObjects;
|
||||
this.extents = new HashMap<>();
|
||||
this.caveVacuumModes = caveVacuumModes(dimension, dataProvider);
|
||||
}
|
||||
|
||||
private PlacementReach dimension() {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region != null) {
|
||||
reach = reach.union(placements(region.getObjects())).union(procedural(region.getProceduralObjects()));
|
||||
}
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
|
||||
if (biome != null) {
|
||||
reach = reach.union(placements(biome.getObjects())).union(procedural(biome.getProceduralObjects()));
|
||||
}
|
||||
}
|
||||
return reach.withReadMargin(PlacementReach.supportReadMargin(dimension.getObjectSurfaceSupportBuffer()));
|
||||
}
|
||||
|
||||
private PlacementReach upperDimension() {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region != null) {
|
||||
reach = reach.union(upperPlacements(region.getSurfaceObjects()));
|
||||
}
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
|
||||
if (biome != null) {
|
||||
reach = reach.union(upperPlacements(biome.getSurfaceObjects()));
|
||||
}
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private PlacementReach placements(KList<IrisObjectPlacement> placements) {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
if (placements == null) {
|
||||
return reach;
|
||||
}
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement != null) {
|
||||
reach = reach.union(objects(placement, profiles(placement)));
|
||||
}
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private PlacementReach upperPlacements(KList<IrisObjectPlacement> placements) {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
if (placements == null) {
|
||||
return reach;
|
||||
}
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObjectPlacement upper = placement.toPlacement(placement.getPlace().toArray(new String[0]));
|
||||
upper.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
upper.setRotation(upsideDownRotation());
|
||||
upper.setCarvingSupport(CarvingMode.ANYWHERE);
|
||||
reach = reach.union(objects(placement, new KList<>(PlacementReach.profile(upper))));
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private PlacementReach objects(IrisObjectPlacement placement, KList<PlacementReach.Profile> profiles) {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
double scale = placement.getMaximumScale(dimension);
|
||||
for (String objectKey : placement.getPlace()) {
|
||||
try {
|
||||
PlacementReach.Extent extent = extent(objectKey);
|
||||
if (extent == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Extent scaled = extent.scaled(scale);
|
||||
for (PlacementReach.Profile profile : profiles) {
|
||||
PlacementReach objectReach = profile.of(scaled);
|
||||
if (objectReach.horizontal() > 128 && warnedLargeObjects.add(objectKey)) {
|
||||
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + objectReach.horizontal() + " blocks) and may increase memory usage!");
|
||||
}
|
||||
reach = reach.union(objectReach);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private PlacementReach procedural(IrisProceduralObjects procedural) {
|
||||
PlacementReach reach = PlacementReach.NONE;
|
||||
if (procedural == null || procedural.isEmpty()) {
|
||||
return reach;
|
||||
}
|
||||
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
|
||||
if (variants == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObjectPlacement objectPlacement = placement.asPlacement();
|
||||
KList<PlacementReach.Profile> profiles = profiles(objectPlacement);
|
||||
double scale = objectPlacement.getMaximumScale(dimension);
|
||||
for (IrisObject variant : variants) {
|
||||
if (variant == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Extent extent = PlacementReach.Extent.of(variant).scaled(scale);
|
||||
for (PlacementReach.Profile profile : profiles) {
|
||||
reach = reach.union(profile.of(extent));
|
||||
}
|
||||
}
|
||||
}
|
||||
return reach;
|
||||
}
|
||||
|
||||
private KList<PlacementReach.Profile> profiles(IrisObjectPlacement placement) {
|
||||
KList<PlacementReach.Profile> profiles = new KList<>(PlacementReach.profile(placement));
|
||||
if (placement.getMode() != ObjectPlaceMode.CENTER_HEIGHT
|
||||
|| placement.getCarvingSupport() == CarvingMode.SURFACE_ONLY) {
|
||||
return profiles;
|
||||
}
|
||||
for (ObjectPlaceMode mode : caveVacuumModes) {
|
||||
IrisObjectPlacement cave = placement.toPlacement(placement.getPlace().toArray(new String[0]));
|
||||
cave.setMode(mode);
|
||||
profiles.add(PlacementReach.profile(cave));
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
private PlacementReach.Extent extent(String objectKey) {
|
||||
if (extents.containsKey(objectKey)) {
|
||||
return extents.get(objectKey);
|
||||
}
|
||||
IrisObject object = dataProvider.getData().getObjectLoader().load(objectKey);
|
||||
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
|
||||
extents.put(objectKey, extent);
|
||||
return extent;
|
||||
}
|
||||
|
||||
private static Set<ObjectPlaceMode> caveVacuumModes(IrisDimension dimension, DataProvider dataProvider) {
|
||||
Set<ObjectPlaceMode> modes = EnumSet.noneOf(ObjectPlaceMode.class);
|
||||
addCaveVacuumMode(modes, dimension.getCaveProfile());
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region != null) {
|
||||
addCaveVacuumMode(modes, region.getCaveProfile());
|
||||
}
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
|
||||
if (biome != null) {
|
||||
addCaveVacuumMode(modes, biome.getCaveProfile());
|
||||
}
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
private static void addCaveVacuumMode(Set<ObjectPlaceMode> modes, IrisCaveProfile profile) {
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
ObjectPlaceMode mode = profile.getDefaultObjectPlaceMode();
|
||||
if (mode != null && IrisObjectVacuum.isVacuumMode(mode)) {
|
||||
modes.add(mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface SourceChunkConsumer {
|
||||
void accept(int chunkX, int chunkZ);
|
||||
|
||||
@@ -23,13 +23,14 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public interface MatterGenerator {
|
||||
MultiBurst DISPATCHER = MultiBurst.burst;
|
||||
ConcurrentHashMap<MatterTaskKey, MatterComponentTask> IN_FLIGHT_COMPONENTS = new ConcurrentHashMap<>();
|
||||
int COMPONENT_SUBMISSION_BATCH_SIZE = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors()));
|
||||
int COMPONENT_SUBMISSION_LIMIT = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors()));
|
||||
long COMPONENT_TASK_POLL_MS = 1000L;
|
||||
long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L);
|
||||
|
||||
@@ -87,7 +88,7 @@ public interface MatterGenerator {
|
||||
if (passPlans.length == 0) {
|
||||
return;
|
||||
}
|
||||
int prefetchRadius = passPlans[0].passChunkRadius();
|
||||
int prefetchRadius = phasePrefetchRadius(passPlans, phase);
|
||||
LongOpenHashSet partialChunks = new LongOpenHashSet();
|
||||
List<MantleComponent> requiredComponents = enabledComponents();
|
||||
boolean terrainOnAccess = false;
|
||||
@@ -104,8 +105,9 @@ public interface MatterGenerator {
|
||||
// close() releases the cached chunks, even when a pass throws, or detached pool threads
|
||||
// write into released chunks.
|
||||
List<MatterComponentTask> outstandingTasks = null;
|
||||
List<MatterComponentTask> submittedTasks = multicore && !DISPATCHER.ownsCurrentThread()
|
||||
? new ArrayList<>(COMPONENT_SUBMISSION_BATCH_SIZE)
|
||||
List<DeferredComponent> deferredComponents = null;
|
||||
SubmissionWindow submissions = multicore && !DISPATCHER.ownsCurrentThread()
|
||||
? new SubmissionWindow()
|
||||
: null;
|
||||
|
||||
try {
|
||||
@@ -229,15 +231,17 @@ public interface MatterGenerator {
|
||||
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
|
||||
MantleComponent component = eligibleComponents[componentIndex];
|
||||
outstandingTasks.add(runComponentAsync(
|
||||
chunk, component, writer, finalPassX, finalPassZ, context, submittedTasks));
|
||||
if (submittedTasks != null && submittedTasks.size() >= COMPONENT_SUBMISSION_BATCH_SIZE) {
|
||||
awaitComponentTasks(submittedTasks);
|
||||
}
|
||||
chunk, component, writer, finalPassX, finalPassZ, context, submissions));
|
||||
}
|
||||
} else {
|
||||
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
|
||||
MantleComponent component = eligibleComponents[componentIndex];
|
||||
runComponentInline(chunk, component, writer, finalPassX, finalPassZ, context);
|
||||
if (!tryRunComponentInline(chunk, component, writer, finalPassX, finalPassZ, context)) {
|
||||
if (deferredComponents == null) {
|
||||
deferredComponents = new ArrayList<>();
|
||||
}
|
||||
deferredComponents.add(new DeferredComponent(chunk, component, finalPassX, finalPassZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,9 +249,8 @@ public interface MatterGenerator {
|
||||
|
||||
if (asyncComponents) {
|
||||
awaitComponentTasks(outstandingTasks);
|
||||
if (submittedTasks != null) {
|
||||
submittedTasks.clear();
|
||||
}
|
||||
} else if (deferredComponents != null) {
|
||||
runDeferredComponents(deferredComponents, writer, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +323,25 @@ public interface MatterGenerator {
|
||||
return new MatterGenerationPlan(plans, writerChunkRadius * 2, phase);
|
||||
}
|
||||
|
||||
private int phasePrefetchRadius(MatterPassPlan[] passPlans, MatterGenerationPhase phase) {
|
||||
int radius = 0;
|
||||
for (MatterPassPlan passPlan : passPlans) {
|
||||
if (passPlan.passChunkRadius() > radius && generatesInPhase(passPlan.pass(), phase)) {
|
||||
radius = passPlan.passChunkRadius();
|
||||
}
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
|
||||
private boolean generatesInPhase(MantlePass pass, MatterGenerationPhase phase) {
|
||||
for (MantleComponent component : pass.components()) {
|
||||
if (phase.includes(component) && shouldGenerateComponent(component)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<MantleComponent> enabledComponents() {
|
||||
List<MantleComponent> components = new ArrayList<>();
|
||||
for (MantlePass pass : getComponents()) {
|
||||
@@ -376,7 +398,7 @@ public interface MatterGenerator {
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ChunkContext context,
|
||||
List<MatterComponentTask> submittedTasks
|
||||
SubmissionWindow submissions
|
||||
) {
|
||||
MantleFlag flag = component.getFlag();
|
||||
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag);
|
||||
@@ -386,19 +408,35 @@ public interface MatterGenerator {
|
||||
return task;
|
||||
}
|
||||
|
||||
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
try {
|
||||
if (DISPATCHER.ownsCurrentThread()) {
|
||||
if (submissions == null) {
|
||||
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
// A pool thread runs its claim inline: a task queued behind pool threads that block
|
||||
// on unmanaged waits (structure builds, cache locks) could starve the whole pool.
|
||||
completeComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
|
||||
return task;
|
||||
} catch (Throwable throwable) {
|
||||
task.cancel(throwable);
|
||||
throw throwable;
|
||||
}
|
||||
}
|
||||
|
||||
MatterComponentTask running = IN_FLIGHT_COMPONENTS.get(key);
|
||||
if (running != null) {
|
||||
return running;
|
||||
}
|
||||
submissions.acquire();
|
||||
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
|
||||
if (existing != null) {
|
||||
submissions.release();
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
IrisContext callerContext = IrisContext.get();
|
||||
submissions.track(task);
|
||||
task.setSubmission(DISPATCHER.submit(() -> completeComponentTask(
|
||||
task,
|
||||
chunk,
|
||||
@@ -408,7 +446,6 @@ public interface MatterGenerator {
|
||||
chunkZ,
|
||||
context,
|
||||
callerContext)));
|
||||
submittedTasks.add(task);
|
||||
return task;
|
||||
} catch (Throwable throwable) {
|
||||
task.cancel(throwable);
|
||||
@@ -416,6 +453,42 @@ public interface MatterGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryRunComponentInline(
|
||||
MantleChunk<Matter> chunk,
|
||||
MantleComponent component,
|
||||
MantleWriter writer,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
ChunkContext context
|
||||
) {
|
||||
MantleFlag flag = component.getFlag();
|
||||
if (!chunk.tryClaim(flag)) {
|
||||
return chunk.isFlagged(flag);
|
||||
}
|
||||
boolean generated = false;
|
||||
try {
|
||||
writer.withComponentPriority(component.getPriority(),
|
||||
() -> component.generateLayer(writer, chunkX, chunkZ, context));
|
||||
generated = true;
|
||||
} finally {
|
||||
if (generated) {
|
||||
chunk.completeClaim(flag);
|
||||
} else {
|
||||
chunk.abandonClaim(flag);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void runDeferredComponents(List<DeferredComponent> deferredComponents, MantleWriter writer,
|
||||
ChunkContext context) {
|
||||
for (DeferredComponent deferred : deferredComponents) {
|
||||
runComponentInline(deferred.chunk(), deferred.component(), writer, deferred.chunkX(), deferred.chunkZ(),
|
||||
context);
|
||||
}
|
||||
deferredComponents.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass barrier. Waits for every launched task, even if one of them failed, so that no task is
|
||||
* still writing through the writer once this returns. The first failure is rethrown.
|
||||
@@ -698,6 +771,36 @@ public interface MatterGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
final class SubmissionWindow {
|
||||
private final Semaphore slots = new Semaphore(COMPONENT_SUBMISSION_LIMIT);
|
||||
private volatile Throwable failure;
|
||||
|
||||
void acquire() {
|
||||
slots.acquireUninterruptibly();
|
||||
Throwable observed = failure;
|
||||
if (observed != null) {
|
||||
slots.release();
|
||||
throw new CompletionException(observed);
|
||||
}
|
||||
}
|
||||
|
||||
void release() {
|
||||
slots.release();
|
||||
}
|
||||
|
||||
void track(MatterComponentTask task) {
|
||||
task.future.whenComplete((ignored, throwable) -> {
|
||||
if (throwable != null && failure == null) {
|
||||
failure = throwable;
|
||||
}
|
||||
slots.release();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
record DeferredComponent(MantleChunk<Matter> chunk, MantleComponent component, int chunkX, int chunkZ) {
|
||||
}
|
||||
|
||||
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.block.VectorMap;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.structure.object.IrisObjectTranslate;
|
||||
import art.arcane.iris.structure.object.IrisObjectVacuum;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.Vector3i;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record PlacementReach(int horizontal, int readMargin) {
|
||||
public static final PlacementReach NONE = new PlacementReach(0, 0);
|
||||
private static final int SPIN_PERIOD = 360;
|
||||
private static final int MAXIMUM_ROTATION_KERNELS = 1024;
|
||||
private static final double ROUNDING_SLACK = 1.0E-9D;
|
||||
|
||||
public PlacementReach {
|
||||
if (horizontal < 0 || readMargin < 0) {
|
||||
throw new IllegalArgumentException("Placement reach must be non-negative: " + horizontal + "+" + readMargin);
|
||||
}
|
||||
}
|
||||
|
||||
public static PlacementReach of(IrisObject object, IrisObjectPlacement placement, double scale) {
|
||||
return of(Extent.of(object).scaled(scale), placement);
|
||||
}
|
||||
|
||||
public static PlacementReach of(Extent extent, IrisObjectPlacement placement) {
|
||||
return profile(placement).of(extent);
|
||||
}
|
||||
|
||||
public static PlacementReach anchored(Extent extent, IrisObjectPlacement placement) {
|
||||
return profile(placement).anchored(extent);
|
||||
}
|
||||
|
||||
public static Profile profile(IrisObjectPlacement placement) {
|
||||
return new Profile(Objects.requireNonNull(placement, "placement"));
|
||||
}
|
||||
|
||||
public static int supportReadMargin(int surfaceSupportBuffer) {
|
||||
return 1 + Math.max(0, surfaceSupportBuffer);
|
||||
}
|
||||
|
||||
public int input() {
|
||||
return saturate((long) horizontal + readMargin);
|
||||
}
|
||||
|
||||
public PlacementReach union(PlacementReach other) {
|
||||
int unionHorizontal = Math.max(horizontal, other.horizontal);
|
||||
int unionInput = Math.max(input(), other.input());
|
||||
return new PlacementReach(unionHorizontal, unionInput - unionHorizontal);
|
||||
}
|
||||
|
||||
public PlacementReach withReadMargin(int minimumReadMargin) {
|
||||
return minimumReadMargin <= readMargin ? this : new PlacementReach(horizontal, minimumReadMargin);
|
||||
}
|
||||
|
||||
private static int roundedBound(double maximumMagnitude) {
|
||||
return saturate((long) Math.floor(maximumMagnitude + 0.5D + ROUNDING_SLACK));
|
||||
}
|
||||
|
||||
private static int saturate(long value) {
|
||||
return value >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, value);
|
||||
}
|
||||
|
||||
private static IrisBlockVector corner(Extent extent, int corner) {
|
||||
return new IrisBlockVector(
|
||||
(corner & 1) == 0 ? extent.x().minimum() : extent.x().maximum(),
|
||||
(corner & 2) == 0 ? extent.y().minimum() : extent.y().maximum(),
|
||||
(corner & 4) == 0 ? extent.z().minimum() : extent.z().maximum());
|
||||
}
|
||||
|
||||
public static final class Profile {
|
||||
private final IrisObjectRotation rotation;
|
||||
private final IrisBlockVector translation;
|
||||
private final boolean rotating;
|
||||
private final boolean radial;
|
||||
private final boolean vertical;
|
||||
private final int[] spinsX;
|
||||
private final int[] spinsY;
|
||||
private final int[] spinsZ;
|
||||
private final int additive;
|
||||
private final int readMargin;
|
||||
|
||||
private Profile(IrisObjectPlacement placement) {
|
||||
IrisObjectRotation configuredRotation = placement.getRotation();
|
||||
IrisObjectTranslate translate = placement.getTranslate();
|
||||
this.rotation = configuredRotation;
|
||||
this.translation = translate != null && translate.canTranslate()
|
||||
? new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ())
|
||||
: null;
|
||||
this.rotating = configuredRotation != null && configuredRotation.canRotate();
|
||||
this.spinsX = rotating ? distinctSpins(configuredRotation, Axis.X) : new int[]{0};
|
||||
this.spinsY = rotating ? distinctSpins(configuredRotation, Axis.Y) : new int[]{0};
|
||||
this.spinsZ = rotating ? distinctSpins(configuredRotation, Axis.Z) : new int[]{0};
|
||||
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
|
||||
this.radial = placement.isRotateTowardsSlope() || (rotating && kernels > MAXIMUM_ROTATION_KERNELS);
|
||||
this.vertical = placement.isRotateTowardsSlope()
|
||||
|| (rotating && (configuredRotation.canRotateX() || configuredRotation.canRotateZ()));
|
||||
this.additive = saturate((long) warpReach(placement.getWarp()) + vacuumReach(placement));
|
||||
this.readMargin = supportReadMargin(placement.getSurfaceSupportBuffer());
|
||||
}
|
||||
|
||||
public PlacementReach of(Extent extent) {
|
||||
Objects.requireNonNull(extent, "extent");
|
||||
return new PlacementReach(saturate((long) rotatedReach(extent) + additive), readMargin);
|
||||
}
|
||||
|
||||
public PlacementReach anchored(Extent extent) {
|
||||
Objects.requireNonNull(extent, "extent");
|
||||
long span = Math.max(extent.x().span(), extent.z().span());
|
||||
return new PlacementReach(saturate(span + additive), readMargin);
|
||||
}
|
||||
|
||||
private int rotatedReach(Extent extent) {
|
||||
if (radial) {
|
||||
return radialReach(extent);
|
||||
}
|
||||
if (!rotating) {
|
||||
return unrotatedReach(extent);
|
||||
}
|
||||
double maximum = 0D;
|
||||
for (int spinX : spinsX) {
|
||||
for (int spinY : spinsY) {
|
||||
for (int spinZ : spinsZ) {
|
||||
maximum = Math.max(maximum, rotatedCornerMaximum(extent, spinX, spinY, spinZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
return roundedBound(maximum);
|
||||
}
|
||||
|
||||
private double rotatedCornerMaximum(Extent extent, int spinX, int spinY, int spinZ) {
|
||||
IrisBlockVector offset = translation == null ? null : rotation.rotate(translation, spinX, spinY, spinZ);
|
||||
double maximum = 0D;
|
||||
for (int corner = 0; corner < 8; corner++) {
|
||||
IrisBlockVector rotated = rotation.rotate(corner(extent, corner), spinX, spinY, spinZ);
|
||||
double x = offset == null ? rotated.getX() : rotated.getX() + offset.getX();
|
||||
double z = offset == null ? rotated.getZ() : rotated.getZ() + offset.getZ();
|
||||
maximum = Math.max(maximum, Math.max(Math.abs(x), Math.abs(z)));
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
private int unrotatedReach(Extent extent) {
|
||||
double translateX = translation == null ? 0D : translation.getX();
|
||||
double translateZ = translation == null ? 0D : translation.getZ();
|
||||
double x = Math.max(Math.abs(extent.x().minimum() + translateX), Math.abs(extent.x().maximum() + translateX));
|
||||
double z = Math.max(Math.abs(extent.z().minimum() + translateZ), Math.abs(extent.z().maximum() + translateZ));
|
||||
return roundedBound(Math.max(x, z));
|
||||
}
|
||||
|
||||
private int radialReach(Extent extent) {
|
||||
double translateX = translation == null ? 0D : translation.getX();
|
||||
double translateY = translation == null ? 0D : translation.getY();
|
||||
double translateZ = translation == null ? 0D : translation.getZ();
|
||||
double maximum = 0D;
|
||||
for (int corner = 0; corner < 8; corner++) {
|
||||
IrisBlockVector position = corner(extent, corner);
|
||||
double x = position.getX() + translateX;
|
||||
double y = vertical ? position.getY() + translateY : 0D;
|
||||
double z = position.getZ() + translateZ;
|
||||
maximum = Math.max(maximum, Math.sqrt((x * x) + (y * y) + (z * z)));
|
||||
}
|
||||
return roundedBound(maximum);
|
||||
}
|
||||
|
||||
private static int warpReach(IrisGeneratorStyle warp) {
|
||||
if (warp == null || warp.isFlat()) {
|
||||
return 0;
|
||||
}
|
||||
return saturate((long) Math.ceil(Math.abs(warp.getMultiplier()) / 2D));
|
||||
}
|
||||
|
||||
private static int vacuumReach(IrisObjectPlacement placement) {
|
||||
if (!IrisObjectVacuum.isVacuumMode(placement.getMode())) {
|
||||
return 0;
|
||||
}
|
||||
return IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings());
|
||||
}
|
||||
|
||||
private static int[] distinctSpins(IrisObjectRotation rotation, Axis axis) {
|
||||
boolean rotates = switch (axis) {
|
||||
case X -> rotation.canRotateX();
|
||||
case Y -> rotation.canRotateY();
|
||||
case Z -> rotation.canRotateZ();
|
||||
};
|
||||
if (!rotates) {
|
||||
return new int[]{0};
|
||||
}
|
||||
LongOpenHashSet angles = new LongOpenHashSet();
|
||||
IntArrayList spins = new IntArrayList();
|
||||
for (int spin = 0; spin < SPIN_PERIOD; spin++) {
|
||||
double angle = switch (axis) {
|
||||
case X -> rotation.getXRotation(spin);
|
||||
case Y -> rotation.getYRotation(spin);
|
||||
case Z -> rotation.getZRotation(spin);
|
||||
};
|
||||
if (angles.add(Double.doubleToLongBits(angle))) {
|
||||
spins.add(spin);
|
||||
}
|
||||
}
|
||||
return spins.toIntArray();
|
||||
}
|
||||
}
|
||||
|
||||
private enum Axis {
|
||||
X,
|
||||
Y,
|
||||
Z
|
||||
}
|
||||
|
||||
public record Extent(Span x, Span y, Span z) {
|
||||
public Extent {
|
||||
Objects.requireNonNull(x, "x");
|
||||
Objects.requireNonNull(y, "y");
|
||||
Objects.requireNonNull(z, "z");
|
||||
}
|
||||
|
||||
public static Extent of(IrisObject object) {
|
||||
Objects.requireNonNull(object, "object");
|
||||
Vector3i center = object.getCenter();
|
||||
int[] keys = keyBounds(object.getBlocks());
|
||||
return new Extent(
|
||||
Span.of(object.getW(), center.getBlockX(), keys[0], keys[3]),
|
||||
Span.of(object.getH(), center.getBlockY(), keys[1], keys[4]),
|
||||
Span.of(object.getD(), center.getBlockZ(), keys[2], keys[5]));
|
||||
}
|
||||
|
||||
public Extent scaled(double scale) {
|
||||
if (scale == 1D) {
|
||||
return this;
|
||||
}
|
||||
return new Extent(x.scaled(scale), y.scaled(scale), z.scaled(scale));
|
||||
}
|
||||
|
||||
private static int[] keyBounds(VectorMap<NativeBlockState> blocks) {
|
||||
int[] bounds = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,
|
||||
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE};
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector key = cursor.key();
|
||||
bounds[0] = Math.min(bounds[0], key.getBlockX());
|
||||
bounds[1] = Math.min(bounds[1], key.getBlockY());
|
||||
bounds[2] = Math.min(bounds[2], key.getBlockZ());
|
||||
bounds[3] = Math.max(bounds[3], key.getBlockX());
|
||||
bounds[4] = Math.max(bounds[4], key.getBlockY());
|
||||
bounds[5] = Math.max(bounds[5], key.getBlockZ());
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
public record Span(int size, int center, int minimum, int maximum, boolean framed) {
|
||||
public Span {
|
||||
if (minimum > -center || maximum < size - 1 - center) {
|
||||
throw new IllegalArgumentException("Span must cover the object frame: " + minimum + ".." + maximum
|
||||
+ " for size " + size + " centered at " + center);
|
||||
}
|
||||
}
|
||||
|
||||
public static Span of(int size, int center, int keyMinimum, int keyMaximum) {
|
||||
int frameMinimum = -center;
|
||||
int frameMaximum = size - 1 - center;
|
||||
boolean framed = keyMinimum >= frameMinimum && keyMaximum <= frameMaximum;
|
||||
return new Span(size, center, Math.min(keyMinimum, frameMinimum), Math.max(keyMaximum, frameMaximum), framed);
|
||||
}
|
||||
|
||||
public int span() {
|
||||
return maximum - minimum;
|
||||
}
|
||||
|
||||
public Span scaled(double scale) {
|
||||
int scaledSize = (int) Math.ceil(size * scale);
|
||||
int scaledCenter = scaledSize / 2;
|
||||
int frameMinimum = -scaledCenter;
|
||||
int frameMaximum = scaledSize - 1 - scaledCenter;
|
||||
if (framed) {
|
||||
return new Span(scaledSize, scaledCenter, frameMinimum, frameMaximum, true);
|
||||
}
|
||||
int scaledMinimum = (int) Math.floor((minimum + center) * scale) - scaledCenter;
|
||||
double centerExcess = Math.max(0D, center - (size / 2D));
|
||||
int scaledMaximum = (int) Math.ceil(((maximum + 1 + centerExcess) * scale) + 0.5D + ROUNDING_SLACK) - 1;
|
||||
return new Span(scaledSize, scaledCenter, Math.min(scaledMinimum, frameMinimum),
|
||||
Math.max(scaledMaximum, frameMaximum), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,4 +50,18 @@ final class ChunkBlockUpdateListener implements ColumnExtentListeningHunk.Listen
|
||||
}
|
||||
quiet = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState state) {
|
||||
if (state == null || state == quiet) {
|
||||
return;
|
||||
}
|
||||
if (B.isUpdatable(state) || state.isCustom()) {
|
||||
for (int y = yToInclusive; y >= yFromInclusive; y--) {
|
||||
updater.catchBlockUpdates(originX + x, y, originZ + z, state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
quiet = state;
|
||||
}
|
||||
}
|
||||
@@ -24,63 +24,65 @@ import lombok.Data;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.DoubleAdder;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
@Data
|
||||
public class EngineMetrics {
|
||||
private final AtomicRollingSequence total;
|
||||
private final AtomicRollingSequence updates;
|
||||
private final AtomicRollingSequence terrain;
|
||||
private final AtomicRollingSequence biome;
|
||||
private final AtomicRollingSequence parallax;
|
||||
private final AtomicRollingSequence parallaxInsert;
|
||||
private final AtomicRollingSequence post;
|
||||
private final AtomicRollingSequence perfection;
|
||||
private final AtomicRollingSequence api;
|
||||
private final AtomicRollingSequence decoration;
|
||||
private final AtomicRollingSequence cave;
|
||||
private final AtomicRollingSequence ravine;
|
||||
private final AtomicRollingSequence deposit;
|
||||
private final AtomicRollingSequence carveResolve;
|
||||
private final AtomicRollingSequence carveApply;
|
||||
private final AtomicRollingSequence noiseHeightInterpolate;
|
||||
private final AtomicRollingSequence noiseHeightGenerator;
|
||||
private final AtomicRollingSequence contextPrefill;
|
||||
private final AtomicRollingSequence contextPrefillHeight;
|
||||
private final AtomicRollingSequence contextPrefillBiome;
|
||||
private final AtomicRollingSequence contextPrefillRock;
|
||||
private final AtomicRollingSequence contextPrefillFluid;
|
||||
private final AtomicRollingSequence contextPrefillRegion;
|
||||
private final AtomicRollingSequence contextPrefillCave;
|
||||
private final AtomicRollingSequence pregenWaitPermit;
|
||||
private final AtomicRollingSequence pregenWaitAdaptive;
|
||||
private final StageTimer total;
|
||||
private final StageTimer updates;
|
||||
private final StageTimer terrain;
|
||||
private final StageTimer biome;
|
||||
private final StageTimer parallax;
|
||||
private final StageTimer parallaxInsert;
|
||||
private final StageTimer post;
|
||||
private final StageTimer perfection;
|
||||
private final StageTimer api;
|
||||
private final StageTimer decoration;
|
||||
private final StageTimer cave;
|
||||
private final StageTimer ravine;
|
||||
private final StageTimer deposit;
|
||||
private final StageTimer carveResolve;
|
||||
private final StageTimer carveApply;
|
||||
private final StageTimer noiseHeightInterpolate;
|
||||
private final StageTimer noiseHeightGenerator;
|
||||
private final StageTimer contextPrefill;
|
||||
private final StageTimer contextPrefillHeight;
|
||||
private final StageTimer contextPrefillBiome;
|
||||
private final StageTimer contextPrefillRock;
|
||||
private final StageTimer contextPrefillFluid;
|
||||
private final StageTimer contextPrefillRegion;
|
||||
private final StageTimer contextPrefillCave;
|
||||
private final StageTimer pregenWaitPermit;
|
||||
private final StageTimer pregenWaitAdaptive;
|
||||
|
||||
public EngineMetrics(int mem) {
|
||||
this.total = new AtomicRollingSequence(mem);
|
||||
this.terrain = new AtomicRollingSequence(mem);
|
||||
this.api = new AtomicRollingSequence(mem);
|
||||
this.biome = new AtomicRollingSequence(mem);
|
||||
this.perfection = new AtomicRollingSequence(mem);
|
||||
this.parallax = new AtomicRollingSequence(mem);
|
||||
this.parallaxInsert = new AtomicRollingSequence(mem);
|
||||
this.post = new AtomicRollingSequence(mem);
|
||||
this.decoration = new AtomicRollingSequence(mem);
|
||||
this.updates = new AtomicRollingSequence(mem);
|
||||
this.cave = new AtomicRollingSequence(mem);
|
||||
this.ravine = new AtomicRollingSequence(mem);
|
||||
this.deposit = new AtomicRollingSequence(mem);
|
||||
this.carveResolve = new AtomicRollingSequence(mem);
|
||||
this.carveApply = new AtomicRollingSequence(mem);
|
||||
this.noiseHeightInterpolate = new AtomicRollingSequence(mem);
|
||||
this.noiseHeightGenerator = new AtomicRollingSequence(mem);
|
||||
this.contextPrefill = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillHeight = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillBiome = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillRock = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillFluid = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillRegion = new AtomicRollingSequence(mem);
|
||||
this.contextPrefillCave = new AtomicRollingSequence(mem);
|
||||
this.pregenWaitPermit = new AtomicRollingSequence(mem);
|
||||
this.pregenWaitAdaptive = new AtomicRollingSequence(mem);
|
||||
this.total = new StageTimer(mem);
|
||||
this.terrain = new StageTimer(mem);
|
||||
this.api = new StageTimer(mem);
|
||||
this.biome = new StageTimer(mem);
|
||||
this.perfection = new StageTimer(mem);
|
||||
this.parallax = new StageTimer(mem);
|
||||
this.parallaxInsert = new StageTimer(mem);
|
||||
this.post = new StageTimer(mem);
|
||||
this.decoration = new StageTimer(mem);
|
||||
this.updates = new StageTimer(mem);
|
||||
this.cave = new StageTimer(mem);
|
||||
this.ravine = new StageTimer(mem);
|
||||
this.deposit = new StageTimer(mem);
|
||||
this.carveResolve = new StageTimer(mem);
|
||||
this.carveApply = new StageTimer(mem);
|
||||
this.noiseHeightInterpolate = new StageTimer(mem);
|
||||
this.noiseHeightGenerator = new StageTimer(mem);
|
||||
this.contextPrefill = new StageTimer(mem);
|
||||
this.contextPrefillHeight = new StageTimer(mem);
|
||||
this.contextPrefillBiome = new StageTimer(mem);
|
||||
this.contextPrefillRock = new StageTimer(mem);
|
||||
this.contextPrefillFluid = new StageTimer(mem);
|
||||
this.contextPrefillRegion = new StageTimer(mem);
|
||||
this.contextPrefillCave = new StageTimer(mem);
|
||||
this.pregenWaitPermit = new StageTimer(mem);
|
||||
this.pregenWaitAdaptive = new StageTimer(mem);
|
||||
}
|
||||
|
||||
public KMap<String, Double> pull() {
|
||||
@@ -134,4 +136,61 @@ public class EngineMetrics {
|
||||
averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D);
|
||||
return Map.copyOf(averages);
|
||||
}
|
||||
|
||||
public Map<String, StageTotal> stageTotals() {
|
||||
Map<String, StageTotal> totals = new LinkedHashMap<>();
|
||||
totals.put("total", total.snapshot());
|
||||
totals.put("terrain", terrain.snapshot());
|
||||
totals.put("biome", biome.snapshot());
|
||||
totals.put("parallax", parallax.snapshot());
|
||||
totals.put("parallax.insert", parallaxInsert.snapshot());
|
||||
totals.put("post", post.snapshot());
|
||||
totals.put("perfection", perfection.snapshot());
|
||||
totals.put("decoration", decoration.snapshot());
|
||||
totals.put("api", api.snapshot());
|
||||
totals.put("updates", updates.snapshot());
|
||||
totals.put("cave", cave.snapshot());
|
||||
totals.put("ravine", ravine.snapshot());
|
||||
totals.put("deposit", deposit.snapshot());
|
||||
totals.put("carve.resolve", carveResolve.snapshot());
|
||||
totals.put("carve.apply", carveApply.snapshot());
|
||||
totals.put("noise.height.interpolate", noiseHeightInterpolate.snapshot());
|
||||
totals.put("noise.height.generator", noiseHeightGenerator.snapshot());
|
||||
totals.put("context.prefill", contextPrefill.snapshot());
|
||||
totals.put("context.prefill.height", contextPrefillHeight.snapshot());
|
||||
totals.put("context.prefill.biome", contextPrefillBiome.snapshot());
|
||||
totals.put("context.prefill.rock", contextPrefillRock.snapshot());
|
||||
totals.put("context.prefill.fluid", contextPrefillFluid.snapshot());
|
||||
totals.put("context.prefill.region", contextPrefillRegion.snapshot());
|
||||
totals.put("context.prefill.cave", contextPrefillCave.snapshot());
|
||||
totals.put("pregen.wait.permit", pregenWaitPermit.snapshot());
|
||||
totals.put("pregen.wait.adaptive", pregenWaitAdaptive.snapshot());
|
||||
return totals;
|
||||
}
|
||||
|
||||
public static final class StageTimer extends AtomicRollingSequence {
|
||||
private final DoubleAdder totalMillis = new DoubleAdder();
|
||||
private final LongAdder samples = new LongAdder();
|
||||
|
||||
public StageTimer(int size) {
|
||||
super(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(double millis) {
|
||||
super.put(millis);
|
||||
totalMillis.add(millis);
|
||||
samples.increment();
|
||||
}
|
||||
|
||||
public StageTotal snapshot() {
|
||||
return new StageTotal(totalMillis.sum(), samples.sum());
|
||||
}
|
||||
}
|
||||
|
||||
public record StageTotal(double millis, long samples) {
|
||||
public StageTotal since(StageTotal earlier) {
|
||||
return new StageTotal(millis - earlier.millis(), samples - earlier.samples());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
public enum GenerationCounter {
|
||||
GENERATOR_EVALUATIONS("generator_evaluations"),
|
||||
CORNER_STARCASTS("corner_starcasts"),
|
||||
HYDROLOGY_LOOKUPS("hydrology_lookups"),
|
||||
CAVE_LATTICE_READS("cave_lattice_reads"),
|
||||
CAVE_LATTICE_SAMPLES("cave_lattice_samples"),
|
||||
CAVE_WARP_SAMPLES("cave_warp_samples");
|
||||
|
||||
public static final boolean ENABLED = Boolean.getBoolean("iris.generation.counters");
|
||||
|
||||
private final String key;
|
||||
private final LongAdder count = new LongAdder();
|
||||
|
||||
GenerationCounter(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void add(long amount) {
|
||||
count.add(amount);
|
||||
}
|
||||
|
||||
public void increment() {
|
||||
count.increment();
|
||||
}
|
||||
|
||||
public long sum() {
|
||||
return count.sum();
|
||||
}
|
||||
}
|
||||
@@ -2212,6 +2212,9 @@ public class IrisComplex implements DataProvider {
|
||||
long sharedKey = SharedCornerBounds.key(gx, gz);
|
||||
long packed = shared.get(sharedKey, interpolatorIndex);
|
||||
if (packed == Long.MIN_VALUE) {
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.CORNER_STARCASTS.increment();
|
||||
}
|
||||
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
|
||||
if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
|
||||
// A non-finite corner is a transient sampling fault, not a fact about the terrain:
|
||||
@@ -2576,6 +2579,9 @@ public class IrisComplex implements DataProvider {
|
||||
return height / generators.length;
|
||||
}
|
||||
GeneratorEvaluations.scalar(generators.length);
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.GENERATOR_EVALUATIONS.add(generators.length);
|
||||
}
|
||||
for (IrisGenerator generator : generators) {
|
||||
double noise = generator.getHeight(x, z, seed);
|
||||
if (!Double.isFinite(noise)) {
|
||||
|
||||
@@ -177,7 +177,7 @@ public class IrisEngine implements Engine {
|
||||
volatile GenerationHistoryRuntimeRouter generationHistoryRuntimeRouter;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private boolean generationHistoryRoutingRequired;
|
||||
private volatile boolean generationHistoryRoutingRequired;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
volatile EngineTarget publishedTarget;
|
||||
@@ -1011,13 +1011,7 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
|
||||
public Optional<GenerationHistoryRuntimeRouter> getGenerationHistoryRuntimeRouter() {
|
||||
GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter;
|
||||
if (router != null) {
|
||||
return Optional.of(router);
|
||||
}
|
||||
synchronized (generationHistoryRuntimeRouterLock) {
|
||||
return Optional.ofNullable(generationHistoryRuntimeRouter);
|
||||
}
|
||||
return Optional.ofNullable(generationHistoryRuntimeRouter);
|
||||
}
|
||||
|
||||
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
|
||||
@@ -1028,13 +1022,14 @@ public class IrisEngine implements Engine {
|
||||
if (router != null) {
|
||||
return router.openCoordinateScope(blockX, blockZ);
|
||||
}
|
||||
synchronized (generationHistoryRuntimeRouterLock) {
|
||||
router = generationHistoryRuntimeRouter;
|
||||
if (router == null && generationHistoryRoutingRequired) {
|
||||
throw new IllegalStateException("Iris generation-history runtime router is detached.");
|
||||
}
|
||||
if (!generationHistoryRoutingRequired) {
|
||||
return null;
|
||||
}
|
||||
return router == null ? null : router.openCoordinateScope(blockX, blockZ);
|
||||
router = generationHistoryRuntimeRouter;
|
||||
if (router == null) {
|
||||
throw new IllegalStateException("Iris generation-history runtime router is detached.");
|
||||
}
|
||||
return router.openCoordinateScope(blockX, blockZ);
|
||||
}
|
||||
|
||||
public boolean hasGenerationRuntimeScope() {
|
||||
|
||||
@@ -79,15 +79,20 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
|
||||
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
|
||||
private static final int SUBMERGED_FLOOR_SUBSTRATE_DEPTH = 8;
|
||||
private static final long RANDOM_MULTIPLIER = 0x5DEECE66DL;
|
||||
private static final long RANDOM_ADDEND = 0xBL;
|
||||
private static final long RANDOM_MASK = (1L << 48) - 1;
|
||||
private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0);
|
||||
private final RNG rng;
|
||||
private final long caveBlendSeed;
|
||||
private final NativeBlockState AIR = B.getState("CAVE_AIR");
|
||||
private final NativeBlockState LAVA = B.getState("LAVA");
|
||||
private final IrisDecorantActuator decorant;
|
||||
|
||||
public IrisCarveModifier(Engine engine) {
|
||||
super(engine, "Carve");
|
||||
rng = new RNG(getEngine().getSeedManager().getCarve());
|
||||
caveBlendSeed = getEngine().getSeedManager().getCarve();
|
||||
rng = new RNG(caveBlendSeed);
|
||||
decorant = new IrisDecorantActuator(engine);
|
||||
}
|
||||
|
||||
@@ -97,9 +102,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start();
|
||||
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
|
||||
CaveInputs resolverState = new CaveInputs(getEngine());
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(2048);
|
||||
IrisCarveScratch scratch = SCRATCH.get();
|
||||
scratch.reset();
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = scratch.caveBiomeCache;
|
||||
CarveWallBuffer walls = scratch.walls;
|
||||
CarveColumnMask[] columnMasks = scratch.columnMasks;
|
||||
CarveColumnMask[] boundaryMasks = scratch.boundaryMasks;
|
||||
@@ -1231,7 +1236,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return null;
|
||||
}
|
||||
|
||||
int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
|
||||
int roll = Math.floorMod(caveBlendRandom(caveBlendSeed, x, y, z), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
|
||||
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
|
||||
return center;
|
||||
}
|
||||
@@ -1245,6 +1250,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
|
||||
return neighbor != null ? neighbor : center;
|
||||
}
|
||||
|
||||
static int caveBlendRandom(long seed, int x, int y, int z) {
|
||||
long scrambled = ((seed + BlockPosition.toLong(x, y, z)) ^ RANDOM_MULTIPLIER) & RANDOM_MASK;
|
||||
return (int) (((scrambled * RANDOM_MULTIPLIER + RANDOM_ADDEND) & RANDOM_MASK) >>> 16);
|
||||
}
|
||||
|
||||
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
|
||||
long key = BlockPosition.toLong(x, y, z);
|
||||
IrisBiome cachedBiome = caveBiomeCache.get(key);
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.stage;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -32,6 +33,7 @@ final class IrisCarveScratch {
|
||||
final int[] surfaceHeights = new int[256];
|
||||
final CarveWallBuffer walls = new CarveWallBuffer(512);
|
||||
final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
|
||||
final Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(4096);
|
||||
int[] upperSurfaceHeights;
|
||||
boolean customCaveBiomePresent;
|
||||
|
||||
@@ -56,6 +58,7 @@ final class IrisCarveScratch {
|
||||
}
|
||||
walls.clear();
|
||||
customBiomeCache.clear();
|
||||
caveBiomeCache.clear();
|
||||
customCaveBiomePresent = false;
|
||||
}
|
||||
}
|
||||
|
||||
+105
-26
@@ -36,6 +36,7 @@ import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig;
|
||||
import art.arcane.iris.generation.hydrology.IrisSurfaceRiverBankConfig;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.generation.block.BoundBlockState;
|
||||
import art.arcane.iris.generation.chunk.ColumnRunHunk;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.generation.context.ChunkedDataCache;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
@@ -176,6 +177,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private IrisOreBands regionSurfaceOres;
|
||||
private IrisOreBands biomeUndergroundOres;
|
||||
private IrisOreBands regionUndergroundOres;
|
||||
private int surfaceOreTop;
|
||||
private int surfaceOreFloor;
|
||||
private int undergroundOreTop;
|
||||
private int undergroundOreFloor;
|
||||
private int columnX;
|
||||
private int columnZ;
|
||||
private NativeBlockState runState;
|
||||
private int runTop = -1;
|
||||
private int runBottom;
|
||||
|
||||
private ColumnPainter(int x, int z, Hunk<NativeBlockState> h, ChunkContext context) {
|
||||
this.x = x;
|
||||
@@ -260,8 +270,12 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
regionSurfaceOres = hideOres ? IrisOreBands.EMPTY : region.getSurfaceOreBands();
|
||||
biomeUndergroundOres = hideOres ? IrisOreBands.EMPTY : biome.getUndergroundOreBands();
|
||||
regionUndergroundOres = hideOres ? IrisOreBands.EMPTY : region.getUndergroundOreBands();
|
||||
boolean hasSurfaceOres = biomeSurfaceOres.hasOres() || regionSurfaceOres.hasOres() || dimensionSurfaceOres.hasOres();
|
||||
boolean hasUndergroundOres = biomeUndergroundOres.hasOres() || regionUndergroundOres.hasOres() || dimensionUndergroundOres.hasOres();
|
||||
surfaceOreTop = oreTop(biomeSurfaceOres, regionSurfaceOres, dimensionSurfaceOres);
|
||||
surfaceOreFloor = oreFloor(biomeSurfaceOres, regionSurfaceOres, dimensionSurfaceOres);
|
||||
undergroundOreTop = oreTop(biomeUndergroundOres, regionUndergroundOres, dimensionUndergroundOres);
|
||||
undergroundOreFloor = oreFloor(biomeUndergroundOres, regionUndergroundOres, dimensionUndergroundOres);
|
||||
columnX = xf;
|
||||
columnZ = zf;
|
||||
int bedrockFloor = bedrockEnabled ? 1 : 0;
|
||||
KList<NativeBlockState> blocks = null;
|
||||
KList<NativeBlockState> ceilingBlocks = null;
|
||||
@@ -269,7 +283,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
|
||||
for (int i = topY; i >= 0; i--) {
|
||||
if (i == 0 && bedrockEnabled) {
|
||||
h.setRaw(xf, i, zf, bedrock);
|
||||
put(i, bedrock);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -280,16 +294,16 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
ceilingBlocks = null;
|
||||
}
|
||||
if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) {
|
||||
h.setRaw(xf, i, zf, air);
|
||||
put(i, air);
|
||||
continue;
|
||||
}
|
||||
layerSurfaceY = terrainColumn.floor(terrainSpan);
|
||||
layerCeilingY = terrainColumn.ceiling(terrainSpan);
|
||||
}
|
||||
|
||||
NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, i, realZ) : null;
|
||||
NativeBlockState ore = i <= surfaceOreTop && i >= surfaceOreFloor ? surfaceOre(realX, i, realZ) : null;
|
||||
if (ore != null) {
|
||||
h.setRaw(xf, i, zf, ore);
|
||||
put(i, ore);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -298,7 +312,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
if (hydrologyFluid == null && fblocks == null) {
|
||||
fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, data);
|
||||
}
|
||||
h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(
|
||||
put(i, HydrologyFluidLayerSelector.select(
|
||||
fblocks,
|
||||
fdepth,
|
||||
fluid,
|
||||
@@ -310,7 +324,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
if (i <= he) {
|
||||
int depth = layerSurfaceY - i;
|
||||
if (depth == 0 && mappedSurfaceBlock != null) {
|
||||
h.setRaw(xf, i, zf, mappedSurfaceBlock);
|
||||
put(i, mappedSurfaceBlock);
|
||||
continue;
|
||||
}
|
||||
if (blocks == null) {
|
||||
@@ -324,7 +338,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
|
||||
int deepFloor = Math.max(bedrockFloor, layerCeilingY > 0 ? layerCeilingY + 2 : 0);
|
||||
if (i >= deepFloor && depth >= Math.max(blocks.size(), paintDepth)) {
|
||||
paintDeep(xf, zf, realX, realZ, i, deepFloor, rock, hasSurfaceOres, hasUndergroundOres);
|
||||
paintDeep(realX, realZ, i, deepFloor, rock);
|
||||
i = deepFloor;
|
||||
continue;
|
||||
}
|
||||
@@ -335,7 +349,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
2, layerCeilingY, data, complex);
|
||||
}
|
||||
int ceilingDepth = i - layerCeilingY;
|
||||
h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth)
|
||||
put(i, ceilingBlocks.hasIndex(ceilingDepth)
|
||||
? ceilingBlocks.get(ceilingDepth) : rock);
|
||||
continue;
|
||||
}
|
||||
@@ -348,17 +362,19 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
||||
layerBlock = riverBed.getPaddingPalette().get(rng, realX, i, realZ, data);
|
||||
}
|
||||
h.setRaw(xf, i, zf, layerBlock);
|
||||
put(i, layerBlock);
|
||||
continue;
|
||||
}
|
||||
|
||||
ore = hasUndergroundOres ? undergroundOre(realX, i, realZ) : null;
|
||||
h.setRaw(xf, i, zf, ore != null ? ore : rock);
|
||||
ore = i <= undergroundOreTop && i >= undergroundOreFloor ? undergroundOre(realX, i, realZ) : null;
|
||||
put(i, ore != null ? ore : rock);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
|
||||
if (upperContext != null) {
|
||||
paintUpper(xf, zf, realX, realZ);
|
||||
paintUpper(realX, realZ);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,15 +382,78 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
* Paints the solid run from {@code top} down to {@code floor}, which lies below every biome
|
||||
* layer, river material, mapped surface and cave ceiling of its span: only ores or rock land there.
|
||||
*/
|
||||
private void paintDeep(int xf, int zf, int realX, int realZ, int top, int floor, NativeBlockState rock,
|
||||
boolean hasSurfaceOres, boolean hasUndergroundOres) {
|
||||
for (int y = top; y >= floor; y--) {
|
||||
NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, y, realZ) : null;
|
||||
if (ore == null && hasUndergroundOres) {
|
||||
private void paintDeep(int realX, int realZ, int top, int floor, NativeBlockState rock) {
|
||||
int bandTop = Math.min(top, Math.max(surfaceOreTop, undergroundOreTop));
|
||||
int bandFloor = Math.max(floor, Math.min(surfaceOreFloor, undergroundOreFloor));
|
||||
if (bandTop < bandFloor) {
|
||||
putRun(top, floor, rock);
|
||||
return;
|
||||
}
|
||||
if (top > bandTop) {
|
||||
putRun(top, bandTop + 1, rock);
|
||||
}
|
||||
for (int y = bandTop; y >= bandFloor; y--) {
|
||||
NativeBlockState ore = y <= surfaceOreTop && y >= surfaceOreFloor ? surfaceOre(realX, y, realZ) : null;
|
||||
if (ore == null && y <= undergroundOreTop && y >= undergroundOreFloor) {
|
||||
ore = undergroundOre(realX, y, realZ);
|
||||
}
|
||||
h.setRaw(xf, y, zf, ore != null ? ore : rock);
|
||||
put(y, ore != null ? ore : rock);
|
||||
}
|
||||
if (bandFloor > floor) {
|
||||
putRun(bandFloor - 1, floor, rock);
|
||||
}
|
||||
}
|
||||
|
||||
private void put(int y, NativeBlockState state) {
|
||||
putRun(y, y, state);
|
||||
}
|
||||
|
||||
private void putRun(int top, int bottom, NativeBlockState state) {
|
||||
if (runTop >= 0 && state == runState && top == runBottom - 1) {
|
||||
runBottom = bottom;
|
||||
return;
|
||||
}
|
||||
flush();
|
||||
runState = state;
|
||||
runTop = top;
|
||||
runBottom = bottom;
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
if (runTop < 0) {
|
||||
return;
|
||||
}
|
||||
ColumnRunHunk.fillColumnRun(h, columnX, columnZ, runBottom, runTop, runState);
|
||||
runTop = -1;
|
||||
runState = null;
|
||||
}
|
||||
|
||||
private static int oreTop(IrisOreBands first, IrisOreBands second, IrisOreBands third) {
|
||||
int top = Integer.MIN_VALUE;
|
||||
if (first.hasOres()) {
|
||||
top = Math.max(top, first.highestY());
|
||||
}
|
||||
if (second.hasOres()) {
|
||||
top = Math.max(top, second.highestY());
|
||||
}
|
||||
if (third.hasOres()) {
|
||||
top = Math.max(top, third.highestY());
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
private static int oreFloor(IrisOreBands first, IrisOreBands second, IrisOreBands third) {
|
||||
int floor = Integer.MAX_VALUE;
|
||||
if (first.hasOres()) {
|
||||
floor = Math.min(floor, first.lowestY());
|
||||
}
|
||||
if (second.hasOres()) {
|
||||
floor = Math.min(floor, second.lowestY());
|
||||
}
|
||||
if (third.hasOres()) {
|
||||
floor = Math.min(floor, third.lowestY());
|
||||
}
|
||||
return floor;
|
||||
}
|
||||
|
||||
private NativeBlockState surfaceOre(int realX, int y, int realZ) {
|
||||
@@ -405,7 +484,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
return ore;
|
||||
}
|
||||
|
||||
private void paintUpper(int xf, int zf, int realX, int realZ) {
|
||||
private void paintUpper(int realX, int realZ) {
|
||||
UpperDimensionContext.Column upperColumn = upperContext.sampleColumn(realX, realZ);
|
||||
int upperSurfaceY = upperColumn.surfaceY();
|
||||
if (upperSurfaceY >= chunkHeight - 1) {
|
||||
@@ -420,11 +499,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
|
||||
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
|
||||
if (y == chunkHeight - 1 && bedrockEnabled) {
|
||||
h.setRaw(xf, y, zf, bedrock);
|
||||
put(y, bedrock);
|
||||
continue;
|
||||
}
|
||||
if (!upperColumn.isSolid(y)) {
|
||||
h.setRaw(xf, y, zf, air);
|
||||
put(y, air);
|
||||
continue;
|
||||
}
|
||||
int faceY = upperColumn.faceY(y);
|
||||
@@ -434,7 +513,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
upperBlocks = null;
|
||||
}
|
||||
if (y == faceY && upperMappedSurface != null) {
|
||||
h.setRaw(xf, y, zf, upperMappedSurface);
|
||||
put(y, upperMappedSurface);
|
||||
continue;
|
||||
}
|
||||
int depthFromFace = y - faceY;
|
||||
@@ -444,9 +523,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY));
|
||||
}
|
||||
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
|
||||
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
|
||||
put(y, upperBlocks.get(depthFromFace));
|
||||
} else {
|
||||
h.setRaw(xf, y, zf, upperRock);
|
||||
put(y, upperRock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,21 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class SubterrainPlanner {
|
||||
private static final int PLAN_CACHE_SLOTS = 4096;
|
||||
private static final int TILE_CACHE_SLOTS = 1024;
|
||||
private static final Comparator<SubterrainPlan> PLAN_ORDER =
|
||||
Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id);
|
||||
|
||||
private final List<Definition> definitions;
|
||||
private final long seed;
|
||||
private final int minY;
|
||||
private final int maxYExclusive;
|
||||
private final ThreadLocal<CandidateTile> sampleTile = new ThreadLocal<>();
|
||||
private final AtomicReferenceArray<CachedPlan> planCache = new AtomicReferenceArray<>(PLAN_CACHE_SLOTS);
|
||||
private final AtomicReferenceArray<CandidateTile> tileCache = new AtomicReferenceArray<>(TILE_CACHE_SLOTS);
|
||||
private final Predicate<SubterrainBounds> allowedBounds;
|
||||
private final RegionResolver regionResolver;
|
||||
|
||||
@@ -70,8 +77,9 @@ public final class SubterrainPlanner {
|
||||
if (minX > maxX || minZ > maxZ) {
|
||||
throw new IllegalArgumentException("Subterrain query bounds are inverted");
|
||||
}
|
||||
List<SubterrainPlan> result = new ArrayList<>();
|
||||
for (Definition definition : definitions) {
|
||||
List<SubterrainPlan> result = null;
|
||||
for (int definitionIndex = 0; definitionIndex < definitions.size(); definitionIndex++) {
|
||||
Definition definition = definitions.get(definitionIndex);
|
||||
long firstX = Math.floorDiv(minX, definition.spacing()) - 1L;
|
||||
long lastX = Math.floorDiv(maxX, definition.spacing()) + 1L;
|
||||
long firstZ = Math.floorDiv(minZ, definition.spacing()) - 1L;
|
||||
@@ -81,14 +89,20 @@ public final class SubterrainPlanner {
|
||||
}
|
||||
for (long gridX = firstX; gridX <= lastX; gridX++) {
|
||||
for (long gridZ = firstZ; gridZ <= lastZ; gridZ++) {
|
||||
SubterrainPlan plan = create(definition, gridX, gridZ);
|
||||
SubterrainPlan plan = plan(definitionIndex, definition, gridX, gridZ);
|
||||
if (plan != null && plan.bounds().intersects(minX, minZ, maxX, maxZ)) {
|
||||
if (result == null) {
|
||||
result = new ArrayList<>();
|
||||
}
|
||||
result.add(plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id));
|
||||
if (result == null) {
|
||||
return List.of();
|
||||
}
|
||||
result.sort(PLAN_ORDER);
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
@@ -96,20 +110,7 @@ public final class SubterrainPlanner {
|
||||
if (isEmpty() || y < minY || y >= maxYExclusive) {
|
||||
return SubterrainCell.OUTSIDE;
|
||||
}
|
||||
int chunkX = Math.floorDiv(x, 16);
|
||||
int chunkZ = Math.floorDiv(z, 16);
|
||||
CandidateTile tile = sampleTile.get();
|
||||
if (tile == null || tile.chunkX() != chunkX || tile.chunkZ() != chunkZ) {
|
||||
long baseX = (long) chunkX * 16;
|
||||
long baseZ = (long) chunkZ * 16;
|
||||
tile = new CandidateTile(chunkX, chunkZ, plansForBounds(
|
||||
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
|
||||
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
|
||||
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
|
||||
(int) Math.min(Integer.MAX_VALUE, baseZ + 16)));
|
||||
sampleTile.set(tile);
|
||||
}
|
||||
return sample(tile.plans(), x, y, z);
|
||||
return sample(candidates(Math.floorDiv(x, 16), Math.floorDiv(z, 16)), x, y, z);
|
||||
}
|
||||
|
||||
public SubterrainCell sample(List<SubterrainPlan> candidates, int x, int y, int z) {
|
||||
@@ -127,10 +128,38 @@ public final class SubterrainPlanner {
|
||||
return selected;
|
||||
}
|
||||
|
||||
private List<SubterrainPlan> candidates(int chunkX, int chunkZ) {
|
||||
int slot = (int) mix(((long) chunkX << 32) ^ (chunkZ & 0xffffffffL)) & (TILE_CACHE_SLOTS - 1);
|
||||
CandidateTile tile = tileCache.getAcquire(slot);
|
||||
if (tile != null && tile.chunkX() == chunkX && tile.chunkZ() == chunkZ) {
|
||||
return tile.plans();
|
||||
}
|
||||
long baseX = (long) chunkX * 16;
|
||||
long baseZ = (long) chunkZ * 16;
|
||||
List<SubterrainPlan> plans = plansForBounds(
|
||||
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
|
||||
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
|
||||
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
|
||||
(int) Math.min(Integer.MAX_VALUE, baseZ + 16));
|
||||
tileCache.setRelease(slot, new CandidateTile(chunkX, chunkZ, plans));
|
||||
return plans;
|
||||
}
|
||||
|
||||
private SubterrainPlan plan(int definitionIndex, Definition definition, long gridX, long gridZ) {
|
||||
int slot = (int) mix(mix(gridX ^ definition.seedSalt()) ^ Long.rotateLeft(gridZ, 29)) & (PLAN_CACHE_SLOTS - 1);
|
||||
CachedPlan cached = planCache.getAcquire(slot);
|
||||
if (cached != null && cached.definition() == definitionIndex && cached.gridX() == gridX && cached.gridZ() == gridZ) {
|
||||
return cached.plan();
|
||||
}
|
||||
SubterrainPlan plan = create(definition, gridX, gridZ);
|
||||
planCache.setRelease(slot, new CachedPlan(definitionIndex, gridX, gridZ, plan));
|
||||
return plan;
|
||||
}
|
||||
|
||||
private SubterrainCell raw(List<SubterrainPlan> candidates, int x, int y, int z) {
|
||||
SubterrainCell selected = SubterrainCell.OUTSIDE;
|
||||
for (SubterrainPlan plan : candidates) {
|
||||
SubterrainCell cell = plan.sample(x, y, z);
|
||||
for (int index = 0; index < candidates.size(); index++) {
|
||||
SubterrainCell cell = candidates.get(index).sample(x, y, z);
|
||||
if (cell.solid() && cell.room().reservedSolid()) {
|
||||
return cell;
|
||||
}
|
||||
@@ -212,6 +241,9 @@ public final class SubterrainPlanner {
|
||||
private record CandidateTile(int chunkX, int chunkZ, List<SubterrainPlan> plans) {
|
||||
}
|
||||
|
||||
private record CachedPlan(int definition, long gridX, long gridZ, SubterrainPlan plan) {
|
||||
}
|
||||
|
||||
record Definition(String id, IrisSubterrainFamily family, IrisSubterrainFluid fluid, String biome, int spacing, double probability,
|
||||
int length, int radius, int height, double shapeWarp, int fluidDepth, int chimneyHeight, int terraceCount,
|
||||
int pillarSpacing, double formationFraction, String solid, int priority,
|
||||
|
||||
@@ -109,6 +109,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Data
|
||||
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
private static final int CUSTOM_BIOME_KEY_CACHE_LIMIT = 4096;
|
||||
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
|
||||
// Loaders (cached or detached) that currently have registered engines; see hasActiveEngines.
|
||||
// Identity-keyed on purpose: Lombok's @Data hashCode over this class's mutable loader state
|
||||
@@ -159,6 +160,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
@Setter(AccessLevel.NONE)
|
||||
@EqualsAndHashCode.Exclude
|
||||
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
@EqualsAndHashCode.Exclude
|
||||
private transient volatile CustomBiomeKeyCache customBiomeKeyCache;
|
||||
private transient volatile Engine soleEngine;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
@@ -234,6 +239,32 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
|
||||
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
|
||||
ContentGate gate = resolver == null ? getContentGate() : null;
|
||||
Object owner = resolver == null ? gate : resolver;
|
||||
CustomBiomeKeyCache cache = customBiomeKeyCache;
|
||||
if (cache == null || cache.owner() != owner) {
|
||||
cache = new CustomBiomeKeyCache(owner, new ConcurrentHashMap<>());
|
||||
customBiomeKeyCache = cache;
|
||||
}
|
||||
CustomBiomeKey key = new CustomBiomeKey(requiredDimension.getLoadKey(), requiredBiome);
|
||||
String cached = cache.keys().get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
String resolved = resolveCustomBiomeResourceKey(requiredDimension, requiredBiome, resolver, gate);
|
||||
if (cache.keys().size() >= CUSTOM_BIOME_KEY_CACHE_LIMIT) {
|
||||
cache.keys().clear();
|
||||
}
|
||||
cache.keys().put(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String resolveCustomBiomeResourceKey(
|
||||
IrisDimension requiredDimension,
|
||||
IrisBiomeCustom requiredBiome,
|
||||
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver,
|
||||
ContentGate gate
|
||||
) {
|
||||
if (resolver != null) {
|
||||
try {
|
||||
return resolver.resolve(
|
||||
@@ -248,7 +279,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
return GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||
requiredDimension.getLoadKey(),
|
||||
requiredBiome,
|
||||
getContentGate()
|
||||
gate
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1035,4 +1066,29 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
return immutable;
|
||||
}
|
||||
}
|
||||
|
||||
private record CustomBiomeKeyCache(Object owner, ConcurrentHashMap<CustomBiomeKey, String> keys) {
|
||||
}
|
||||
|
||||
private static final class CustomBiomeKey {
|
||||
private final String dimensionKey;
|
||||
private final IrisBiomeCustom biome;
|
||||
private final int hash;
|
||||
|
||||
private CustomBiomeKey(String dimensionKey, IrisBiomeCustom biome) {
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.biome = biome;
|
||||
hash = 31 * Objects.hashCode(dimensionKey) + System.identityHashCode(biome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof CustomBiomeKey key && key.biome == biome && Objects.equals(key.dimensionKey, dimensionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ package art.arcane.iris.platform.bukkit.nms;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeSectionVolume;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.BukkitTerrainBuffer;
|
||||
|
||||
@@ -217,7 +217,7 @@ public interface INMSBinding extends NativeTerrainAccess {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, NativeBlockVolume data) {
|
||||
default boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, NativeSectionVolume data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,8 @@ import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.iris.generation.chunk.ColumnBiomeHunk;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBiome;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataOutput;
|
||||
import art.arcane.iris.generation.chunk.SectionBuffer;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
@@ -1344,7 +1345,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (selected != null) {
|
||||
selected.generateChunk(engine, TerrainChunk.create(d), x, z);
|
||||
} else {
|
||||
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
|
||||
SectionBuffer blocks = new SectionBuffer(d.getMaxHeight() - d.getMinHeight());
|
||||
Hunk<NativeBiome> biomes = new ColumnBiomeHunk(d.getMaxHeight() - d.getMinHeight(), () -> BukkitBiome.of(Biome.PLAINS));
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||
openGenerationHistoryCoordinateScope(engine, x << 4, z << 4);
|
||||
@@ -1352,7 +1353,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
engine.generate(x << 4, z << 4, blocks, biomes,
|
||||
initialEntryPending || studioEntryBootstrapActive.get() || engine.shouldGenerateMulticore());
|
||||
blocks.apply();
|
||||
ChunkDataOutput.apply(d, blocks);
|
||||
if (historyScope != null) {
|
||||
historyScope.claimGeneratedSemantics();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.volmlib.util.parallel.MultiBurstSupport;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -45,7 +46,7 @@ public final class PregenPerformanceProfile {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(Runtime.getRuntime().availableProcessors()))) {
|
||||
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(MultiBurstSupport.performanceProcessors()))) {
|
||||
PARALLELISM_HOLDERS.incrementAndGet();
|
||||
changed = true;
|
||||
}
|
||||
@@ -88,8 +89,8 @@ public final class PregenPerformanceProfile {
|
||||
apply(generator == null ? null : generator.getEngine());
|
||||
}
|
||||
|
||||
static int pregenBurstParallelism(int availableProcessors) {
|
||||
return Math.max(4, availableProcessors * 2);
|
||||
static int pregenBurstParallelism(int performanceProcessors) {
|
||||
return Math.max(4, performanceProcessors * 2);
|
||||
}
|
||||
|
||||
private static void prepareNoiseCacheInPlace(Engine engine) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import art.arcane.iris.generation.chunk.SectionBuffer;
|
||||
import org.junit.After;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
@@ -37,7 +36,7 @@ public class PlatformStateRebindTest {
|
||||
"art.arcane.iris.generation.biome.IrisBiomeLayerGenerator",
|
||||
"art.arcane.iris.studio.generation.BiomeBuffetGenerator",
|
||||
"art.arcane.iris.generation.mantle.EngineMantle",
|
||||
"art.arcane.iris.generation.chunk.ChunkDataHunkHolder");
|
||||
"art.arcane.iris.generation.chunk.SectionBuffer");
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
@@ -47,10 +46,7 @@ public class PlatformStateRebindTest {
|
||||
@Test
|
||||
public void chunkDataAirIsResolvedAgainstTheCurrentlyBoundPlatform() {
|
||||
NativeBlockState first = bindPlatform("first");
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(-64);
|
||||
when(chunk.getMaxHeight()).thenReturn(320);
|
||||
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
|
||||
SectionBuffer holder = new SectionBuffer(384);
|
||||
|
||||
assertSame(first, holder.getRaw(0, -1, 0));
|
||||
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
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.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ChunkDataHunkHolderTest {
|
||||
@Test
|
||||
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
|
||||
ChunkDataHunkHolder holder = holder(64);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
|
||||
assertEquals(-1, holder.highestStoredY(3, 7));
|
||||
holder.setRaw(3, 40, 7, null);
|
||||
assertEquals(-1, holder.highestStoredY(3, 7));
|
||||
holder.setRaw(3, 12, 7, stone);
|
||||
holder.setRaw(3, 30, 7, stone);
|
||||
holder.setRaw(3, 5, 7, stone);
|
||||
|
||||
assertEquals(30, holder.highestStoredY(3, 7));
|
||||
assertEquals(-1, holder.highestStoredY(7, 3));
|
||||
assertSame(stone, holder.getStoredRaw(3, 30, 7));
|
||||
assertNull(holder.getStoredRaw(3, 31, 7));
|
||||
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(holder, (x, y, z, state) -> {
|
||||
}), 3, 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cellsKeepTheZMajorLayout() {
|
||||
ChunkDataHunkHolder holder = holder(32);
|
||||
List<NativeBlockState> states = new ArrayList<>();
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = 0; y < 32; y++) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
states.add(state);
|
||||
holder.setRaw(x, y, z, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
int index = 0;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = 0; y < 32; y++) {
|
||||
assertSame(states.get(index++), holder.getStoredRaw(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
|
||||
for (int trial = 0; trial < 20; trial++) {
|
||||
ChunkDataHunkHolder holder = holder(768);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
int threads = 8;
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
List<Thread> writers = new ArrayList<>();
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
int offset = thread;
|
||||
Thread writer = new Thread(() -> {
|
||||
try {
|
||||
start.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
for (int y = offset; y < 768; y += threads) {
|
||||
holder.setRaw(9, y, 4, stone);
|
||||
}
|
||||
});
|
||||
writer.start();
|
||||
writers.add(writer);
|
||||
}
|
||||
start.countDown();
|
||||
for (Thread writer : writers) {
|
||||
writer.join();
|
||||
}
|
||||
assertEquals(767, holder.highestStoredY(9, 4));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(-256);
|
||||
when(chunk.getMaxHeight()).thenReturn(512);
|
||||
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
|
||||
BlockData dirt = mock(BlockData.class);
|
||||
BlockData coal = mock(BlockData.class);
|
||||
BlockData caveAir = mock(BlockData.class);
|
||||
NativeBlockState dirtState = state(dirt);
|
||||
NativeBlockState coalState = state(coal);
|
||||
NativeBlockState caveAirState = state(caveAir);
|
||||
BlockData[] applied = new BlockData[768];
|
||||
Arrays.fill(applied, dirt);
|
||||
for (int y = 0; y <= 75; y++) {
|
||||
holder.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
|
||||
}
|
||||
doAnswer(call -> {
|
||||
int y = call.getArgument(1);
|
||||
applied[y + 256] = call.getArgument(3);
|
||||
return null;
|
||||
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
doAnswer(call -> {
|
||||
int from = call.getArgument(1);
|
||||
int to = call.getArgument(4);
|
||||
BlockData block = call.getArgument(6);
|
||||
Arrays.fill(applied, from + 256, to + 256, block);
|
||||
return null;
|
||||
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
|
||||
holder.apply();
|
||||
}
|
||||
|
||||
for (int y = -256; y < 512; y++) {
|
||||
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
|
||||
assertSame("column at " + y, expected, applied[y + 256]);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeBlockState state(BlockData data) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.nativeHandle()).thenReturn(data);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static ChunkDataHunkHolder holder(int height) {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(0);
|
||||
when(chunk.getMaxHeight()).thenReturn(height);
|
||||
return new ChunkDataHunkHolder(chunk);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
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.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ChunkDataOutputTest {
|
||||
@Test
|
||||
public void nativeWriterReceivesTheSectionBuffer() {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
SectionBuffer blocks = new SectionBuffer(64);
|
||||
INMSBinding binding = mock(INMSBinding.class);
|
||||
when(binding.applyChunkDataBlocks(chunk, blocks)).thenReturn(true);
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(binding);
|
||||
ChunkDataOutput.apply(chunk, blocks);
|
||||
}
|
||||
|
||||
verify(binding).applyChunkDataBlocks(chunk, blocks);
|
||||
verify(chunk, never()).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(-256);
|
||||
when(chunk.getMaxHeight()).thenReturn(512);
|
||||
SectionBuffer blocks = new SectionBuffer(768);
|
||||
BlockData dirt = mock(BlockData.class);
|
||||
BlockData coal = mock(BlockData.class);
|
||||
BlockData caveAir = mock(BlockData.class);
|
||||
NativeBlockState dirtState = state(dirt);
|
||||
NativeBlockState coalState = state(coal);
|
||||
NativeBlockState caveAirState = state(caveAir);
|
||||
BlockData[] applied = new BlockData[768];
|
||||
Arrays.fill(applied, dirt);
|
||||
for (int y = 0; y <= 75; y++) {
|
||||
blocks.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
|
||||
}
|
||||
doAnswer(call -> {
|
||||
int y = call.getArgument(1);
|
||||
applied[y + 256] = call.getArgument(3);
|
||||
return null;
|
||||
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
doAnswer(call -> {
|
||||
int from = call.getArgument(1);
|
||||
int to = call.getArgument(4);
|
||||
BlockData block = call.getArgument(6);
|
||||
Arrays.fill(applied, from + 256, to + 256, block);
|
||||
return null;
|
||||
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
|
||||
ChunkDataOutput.apply(chunk, blocks);
|
||||
}
|
||||
|
||||
for (int y = -256; y < 512; y++) {
|
||||
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
|
||||
assertSame("column at " + y, expected, applied[y + 256]);
|
||||
}
|
||||
assertTrue(blocks.highestStoredY(14, 6) == 331);
|
||||
}
|
||||
|
||||
private static NativeBlockState state(BlockData data) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.nativeHandle()).thenReturn(data);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class SectionBufferTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private NativeBlockState air;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
air = mock(NativeBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block(anyString())).thenReturn(air);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
|
||||
SectionBuffer buffer = new SectionBuffer(64);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
|
||||
assertEquals(-1, buffer.highestStoredY(3, 7));
|
||||
buffer.setRaw(3, 40, 7, null);
|
||||
assertEquals(-1, buffer.highestStoredY(3, 7));
|
||||
buffer.setRaw(3, 12, 7, stone);
|
||||
buffer.setRaw(3, 30, 7, stone);
|
||||
buffer.setRaw(3, 5, 7, stone);
|
||||
|
||||
assertEquals(30, buffer.highestStoredY(3, 7));
|
||||
assertEquals(-1, buffer.highestStoredY(7, 3));
|
||||
assertSame(stone, buffer.getStoredRaw(3, 30, 7));
|
||||
assertNull(buffer.getStoredRaw(3, 31, 7));
|
||||
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(buffer, (x, y, z, state) -> {
|
||||
}), 3, 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unwrittenAndOutOfRangeCellsReadAsAir() {
|
||||
SectionBuffer buffer = new SectionBuffer(48);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
buffer.setRaw(2, 20, 9, stone);
|
||||
|
||||
assertSame(air, buffer.getRaw(2, 21, 9));
|
||||
assertSame(air, buffer.getRaw(2, 3, 9));
|
||||
assertSame(air, buffer.getRaw(2, -1, 9));
|
||||
assertSame(air, buffer.getRaw(2, 48, 9));
|
||||
assertSame(stone, buffer.getRaw(2, 20, 9));
|
||||
assertNull(buffer.getStoredRaw(2, 21, 9));
|
||||
assertNull(buffer.getStoredRaw(2, 3, 9));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void paletteKeepsEveryDistinctInstance() {
|
||||
SectionBuffer buffer = new SectionBuffer(32);
|
||||
NativeBlockState first = mock(NativeBlockState.class);
|
||||
NativeBlockState second = mock(NativeBlockState.class);
|
||||
when(first.matches(second)).thenReturn(true);
|
||||
when(second.matches(first)).thenReturn(true);
|
||||
|
||||
buffer.setRaw(0, 0, 0, first);
|
||||
buffer.setRaw(1, 0, 0, second);
|
||||
buffer.setRaw(2, 0, 0, first);
|
||||
|
||||
assertSame(first, buffer.getRaw(0, 0, 0));
|
||||
assertSame(second, buffer.getRaw(1, 0, 0));
|
||||
assertSame(first, buffer.getRaw(2, 0, 0));
|
||||
assertEquals(buffer.paletteId(first), buffer.paletteId(first));
|
||||
assertNotEquals(buffer.paletteId(first), buffer.paletteId(second));
|
||||
assertEquals(0, buffer.paletteId(null));
|
||||
NativeBlockState[] palette = buffer.palette();
|
||||
assertEquals(3, palette.length);
|
||||
assertNull(palette[0]);
|
||||
assertSame(first, palette[buffer.paletteId(first)]);
|
||||
assertSame(second, palette[buffer.paletteId(second)]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sectionsUseTheYzxLayoutAndStayUnallocatedUntilWritten() {
|
||||
SectionBuffer buffer = new SectionBuffer(64);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
NativeBlockState dirt = mock(NativeBlockState.class);
|
||||
buffer.setRaw(5, 37, 11, stone);
|
||||
buffer.setRaw(15, 47, 15, dirt);
|
||||
|
||||
assertEquals(4, buffer.sectionCount());
|
||||
assertNull(buffer.section(0));
|
||||
assertNull(buffer.section(1));
|
||||
assertNull(buffer.section(3));
|
||||
short[] section = buffer.section(2);
|
||||
assertEquals(4096, section.length);
|
||||
assertEquals(buffer.paletteId(stone), section[(5 << 8) | (11 << 4) | 5]);
|
||||
assertEquals(buffer.paletteId(dirt), section[(15 << 8) | (15 << 4) | 15]);
|
||||
int written = 0;
|
||||
for (short id : section) {
|
||||
written += id == 0 ? 0 : 1;
|
||||
}
|
||||
assertEquals(2, written);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnRunsMatchRepeatedSingleWrites() {
|
||||
Random random = new Random(41L);
|
||||
List<NativeBlockState> pool = new ArrayList<>();
|
||||
for (int index = 0; index < 9; index++) {
|
||||
pool.add(mock(NativeBlockState.class));
|
||||
}
|
||||
pool.add(null);
|
||||
for (int trial = 0; trial < 40; trial++) {
|
||||
int height = 16 * (1 + random.nextInt(12));
|
||||
SectionBuffer runs = new SectionBuffer(height);
|
||||
SectionBuffer cells = new SectionBuffer(height);
|
||||
for (int write = 0; write < 300; write++) {
|
||||
int x = random.nextInt(16);
|
||||
int z = random.nextInt(16);
|
||||
int from = random.nextInt(height);
|
||||
int to = Math.min(height - 1, from + random.nextInt(70));
|
||||
NativeBlockState state = pool.get(random.nextInt(pool.size()));
|
||||
switch (random.nextInt(3)) {
|
||||
case 0 -> runs.fillColumnRun(x, z, from, to, state);
|
||||
case 1 -> runs.fillColumnRun(x, z, from, to, runs.paletteId(state));
|
||||
default -> ColumnRunHunk.fillColumnRun(runs, x, z, from, to, state);
|
||||
}
|
||||
for (int y = to; y >= from; y--) {
|
||||
cells.setRaw(x, y, z, state);
|
||||
}
|
||||
}
|
||||
assertSameContents(cells, runs);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runsWithAnEmptyRangeOrNoStateWriteNothing() {
|
||||
SectionBuffer buffer = new SectionBuffer(32);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
buffer.fillColumnRun(1, 1, 10, 9, stone);
|
||||
buffer.fillColumnRun(1, 1, 3, 9, (NativeBlockState) null);
|
||||
buffer.fillColumnRun(1, 1, 3, 9, 0);
|
||||
|
||||
assertEquals(-1, buffer.highestStoredY(1, 1));
|
||||
assertNull(buffer.section(0));
|
||||
assertEquals(1, buffer.palette().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
|
||||
for (int trial = 0; trial < 20; trial++) {
|
||||
SectionBuffer buffer = new SectionBuffer(768);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
int threads = 8;
|
||||
runConcurrently(threads, thread -> {
|
||||
for (int y = thread; y < 768; y += threads) {
|
||||
buffer.setRaw(9, y, 4, stone);
|
||||
}
|
||||
});
|
||||
assertEquals(767, buffer.highestStoredY(9, 4));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentPaletteGrowthKeepsOneIdPerInstance() throws InterruptedException {
|
||||
int threads = 8;
|
||||
int statesPerThread = 300;
|
||||
NativeBlockState[][] states = new NativeBlockState[threads][statesPerThread];
|
||||
NativeBlockState[] shared = new NativeBlockState[40];
|
||||
for (int index = 0; index < shared.length; index++) {
|
||||
shared[index] = mock(NativeBlockState.class);
|
||||
}
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
for (int index = 0; index < statesPerThread; index++) {
|
||||
states[thread][index] = mock(NativeBlockState.class);
|
||||
}
|
||||
}
|
||||
for (int trial = 0; trial < 10; trial++) {
|
||||
SectionBuffer buffer = new SectionBuffer(384);
|
||||
runConcurrently(threads, thread -> {
|
||||
for (int index = 0; index < statesPerThread; index++) {
|
||||
int x = thread * 2 + (index & 1);
|
||||
int z = (index >> 1) & 15;
|
||||
int y = index;
|
||||
buffer.setRaw(x, y, z, states[thread][index]);
|
||||
buffer.setRaw(x, y + 1, z, shared[index % shared.length]);
|
||||
}
|
||||
});
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
for (int index = 0; index < statesPerThread; index++) {
|
||||
int x = thread * 2 + (index & 1);
|
||||
int z = (index >> 1) & 15;
|
||||
assertSame(states[thread][index], buffer.getStoredRaw(x, index, z));
|
||||
assertSame(shared[index % shared.length], buffer.getRaw(x, index + 1, z));
|
||||
}
|
||||
}
|
||||
assertEquals(1 + threads * statesPerThread + shared.length, buffer.palette().length);
|
||||
for (NativeBlockState state : shared) {
|
||||
assertSame(state, buffer.palette()[buffer.paletteId(state)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listeningHunksForwardRunsInTopDownOrder() {
|
||||
SectionBuffer buffer = new SectionBuffer(32);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
List<Integer> heard = new ArrayList<>();
|
||||
ColumnExtentListeningHunk<NativeBlockState> listening = new ColumnExtentListeningHunk<>(buffer,
|
||||
(x, y, z, state) -> heard.add(y));
|
||||
|
||||
ColumnRunHunk.fillColumnRun(listening, 4, 6, 10, 13, stone);
|
||||
|
||||
assertEquals(List.of(13, 12, 11, 10), heard);
|
||||
for (int y = 10; y <= 13; y++) {
|
||||
assertSame(stone, buffer.getStoredRaw(4, y, 6));
|
||||
}
|
||||
assertNull(buffer.getStoredRaw(4, 9, 6));
|
||||
assertEquals(13, buffer.highestStoredY(4, 6));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plainHunksReceiveRunsAsTopDownSingleWrites() {
|
||||
Hunk<NativeBlockState> array = Hunk.newArrayHunk(16, 32, 16);
|
||||
NativeBlockState stone = mock(NativeBlockState.class);
|
||||
array.setRaw(2, 5, 2, stone);
|
||||
List<Integer> order = new ArrayList<>();
|
||||
Hunk<NativeBlockState> recording = new ColumnExtentListeningHunk<>(array, (x, y, z, state) -> order.add(y));
|
||||
|
||||
ColumnRunHunk.fillColumnRun(Hunk.newArrayHunk(16, 32, 16), 0, 0, 0, 3, stone);
|
||||
ColumnRunHunk.fillColumnRun(array, 2, 2, 4, 6, null);
|
||||
ColumnRunHunk.fillColumnRun(recording, 1, 1, 7, 9, stone);
|
||||
|
||||
assertNull(array.getRaw(2, 5, 2));
|
||||
assertSame(stone, array.getRaw(1, 8, 1));
|
||||
assertEquals(List.of(9, 8, 7), order);
|
||||
}
|
||||
|
||||
private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) {
|
||||
assertEquals(expected.sectionCount(), actual.sectionCount());
|
||||
NativeBlockState[] expectedPalette = expected.palette();
|
||||
NativeBlockState[] actualPalette = actual.palette();
|
||||
for (int sectionIndex = 0; sectionIndex < expected.sectionCount(); sectionIndex++) {
|
||||
short[] expectedSection = expected.section(sectionIndex);
|
||||
short[] actualSection = actual.section(sectionIndex);
|
||||
if (expectedSection == null) {
|
||||
assertNull(actualSection);
|
||||
continue;
|
||||
}
|
||||
NativeBlockState[] expectedStates = new NativeBlockState[4096];
|
||||
NativeBlockState[] actualStates = new NativeBlockState[4096];
|
||||
for (int index = 0; index < 4096; index++) {
|
||||
expectedStates[index] = expectedPalette[expectedSection[index] & 0xFFFF];
|
||||
actualStates[index] = actualPalette[actualSection[index] & 0xFFFF];
|
||||
}
|
||||
assertArrayEquals(expectedStates, actualStates);
|
||||
}
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
assertEquals(expected.highestStoredY(x, z), actual.highestStoredY(x, z));
|
||||
}
|
||||
}
|
||||
assertEquals(Arrays.stream(expectedPalette).filter(state -> state != null).distinct().count(),
|
||||
Arrays.stream(actualPalette).filter(state -> state != null).distinct().count());
|
||||
}
|
||||
|
||||
private static void runConcurrently(int threads, ThreadTask task) throws InterruptedException {
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
List<Thread> workers = new ArrayList<>();
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
int index = thread;
|
||||
Thread worker = new Thread(() -> {
|
||||
try {
|
||||
start.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
task.run(index);
|
||||
});
|
||||
worker.start();
|
||||
workers.add(worker);
|
||||
}
|
||||
start.countDown();
|
||||
for (Thread worker : workers) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ThreadTask {
|
||||
void run(int thread);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -60,6 +61,13 @@ public class HydrologyBurstTest {
|
||||
assertTrue(pool.isTerminated());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerPoolFollowsThePerformanceCores() {
|
||||
assertEquals(12, MultiBurst.HydrologyBurst.plannerParallelism(12));
|
||||
assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(1));
|
||||
assertEquals(32, MultiBurst.HydrologyBurst.plannerParallelism(32));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closedHydrologyWorkersTerminateAndReopenCreatesANewPool() throws Exception {
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
|
||||
@@ -696,6 +696,29 @@ public class HydrologyTileCacheTest {
|
||||
cache.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenSpeculationStaysWithinTheGenerationReachOfTheArea() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyPlannerSettings settings = stalePrefetchSettings();
|
||||
when(planner.settings()).thenReturn(settings);
|
||||
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
planned.add(invocation.getArgument(0));
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
|
||||
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
|
||||
new HydrologyTileCache.PregenerationArea(1024, 1024, 736, 736, 1311, 1311))) {
|
||||
assertEquals(prefetchRectangle(0, 1, 0, 1), new HashSet<>(planned));
|
||||
cache.prepareChunkColumns(800, 800);
|
||||
cache.prepareChunkColumns(1300, 1300);
|
||||
assertEquals(prefetchRectangle(0, 1, 0, 1), new HashSet<>(planned));
|
||||
assertEquals(4, planned.size());
|
||||
}
|
||||
cache.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenScopesRestoreStudioPrefetchAndIgnoreSupersededClose() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CaveCellKernelTest {
|
||||
private static final int TRIALS = 200_000;
|
||||
|
||||
@Test
|
||||
public void decidedCellsAgreeWithEveryInterpolatedColumn() {
|
||||
SplittableRandom random = new SplittableRandom(91_337L);
|
||||
long decided = 0L;
|
||||
for (int trial = 0; trial < TRIALS; trial++) {
|
||||
double scale = Math.scalb(1D, random.nextInt(-30, 4));
|
||||
double base = (random.nextDouble() * 2D - 1D) * scale;
|
||||
double spread = random.nextInt(4) == 0 ? 0D : random.nextDouble() * scale * Math.scalb(1D, -random.nextInt(0, 40));
|
||||
double d00 = base + spread * (random.nextDouble() * 2D - 1D);
|
||||
double d01 = random.nextInt(8) == 0 ? d00 : base + spread * (random.nextDouble() * 2D - 1D);
|
||||
double d10 = base + spread * (random.nextDouble() * 2D - 1D);
|
||||
double d11 = random.nextInt(8) == 0 ? d10 : base + spread * (random.nextDouble() * 2D - 1D);
|
||||
double low = Math.min(Math.min(d00, d01), Math.min(d10, d11));
|
||||
double high = Math.max(Math.max(d00, d01), Math.max(d10, d11));
|
||||
int mode = random.nextInt(6);
|
||||
double[] thresholds = new double[64];
|
||||
for (int column = 0; column < 64; column++) {
|
||||
thresholds[column] = pickThreshold(random, low, high, mode);
|
||||
}
|
||||
double thresholdLow = Double.POSITIVE_INFINITY;
|
||||
double thresholdHigh = Double.NEGATIVE_INFINITY;
|
||||
for (double threshold : thresholds) {
|
||||
thresholdLow = Math.min(thresholdLow, threshold);
|
||||
thresholdHigh = Math.max(thresholdHigh, threshold);
|
||||
}
|
||||
int decision = CaveCellKernel.decide(d00, d01, d10, d11, thresholdLow, thresholdHigh);
|
||||
if (decision == CaveCellKernel.MIXED) {
|
||||
continue;
|
||||
}
|
||||
decided++;
|
||||
for (int column = 0; column < 64; column++) {
|
||||
double tx = (column >> 3) / 8D;
|
||||
double tz = (column & 7) / 8D;
|
||||
boolean carved = bilinear(d00, d01, d10, d11, tx, tz) <= thresholds[column];
|
||||
assertEquals("trial " + trial + " column " + column, decision == CaveCellKernel.ALL, carved);
|
||||
}
|
||||
}
|
||||
assertTrue("decided " + decided, decided > TRIALS / 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void thresholdsAtTheCornerExtremesStayMixed() {
|
||||
double d00 = 0.1D;
|
||||
double d01 = -0.2D;
|
||||
double d10 = 0.3D;
|
||||
double d11 = 0.05D;
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D, 0.3D));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, Math.nextUp(0.3D), 0.4D));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, -0.3D, Math.nextDown(-0.2D)));
|
||||
assertEquals(CaveCellKernel.ALL, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D + 1.0E-12D, 0.5D));
|
||||
assertEquals(CaveCellKernel.NONE, CaveCellKernel.decide(d00, d01, d10, d11, -0.9D, -0.2D - 1.0E-12D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonFiniteCornersAreNeverDecided() {
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(Double.NaN, 0D, 0D, 0D, 5D, 5D));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, Double.POSITIVE_INFINITY, 0D, 0D, -5D, -5D));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, Double.NEGATIVE_INFINITY, 0D,
|
||||
Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY));
|
||||
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, 0D, 0D, Double.NaN, 1D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void monotoneThresholdMappingPreservesBounds() {
|
||||
SplittableRandom random = new SplittableRandom(7_771L);
|
||||
for (int trial = 0; trial < TRIALS; trial++) {
|
||||
double normalization = 0.25D + random.nextDouble() * 3D;
|
||||
double inverse = 1D / normalization;
|
||||
double a = (random.nextDouble() * 2D - 1D) * Math.scalb(1D, random.nextInt(-20, 3));
|
||||
double b = random.nextInt(4) == 0 ? Math.nextUp(a) : a + random.nextDouble() * Math.scalb(1D, random.nextInt(-50, 0));
|
||||
assertTrue(CaveCellKernel.threshold(a, normalization, inverse) <= CaveCellKernel.threshold(b, normalization, inverse));
|
||||
assertEquals(Double.doubleToRawLongBits((a * normalization) * inverse),
|
||||
Double.doubleToRawLongBits(CaveCellKernel.threshold(a, normalization, inverse)));
|
||||
}
|
||||
}
|
||||
|
||||
private static double pickThreshold(SplittableRandom random, double low, double high, int mode) {
|
||||
double width = Math.max(high - low, Math.ulp(Math.max(Math.abs(low), Math.abs(high))));
|
||||
return switch (mode) {
|
||||
case 0 -> high + Math.ulp(high) * random.nextInt(0, 64);
|
||||
case 1 -> low - Math.ulp(low) * random.nextInt(0, 64);
|
||||
case 2 -> high + width * random.nextDouble() * 0.5D;
|
||||
case 3 -> low - width * random.nextDouble() * 0.5D;
|
||||
case 4 -> high + width * (1D + random.nextDouble() * 4D);
|
||||
default -> low - width * (1D + random.nextDouble() * 4D);
|
||||
};
|
||||
}
|
||||
|
||||
private static double bilinear(double d00, double d01, double d10, double d11, double tx, double tz) {
|
||||
double dx0 = d00 + ((d10 - d00) * tx);
|
||||
double dx1 = d01 + ((d11 - d01) * tx);
|
||||
return dx0 + ((dx1 - dx0) * tz);
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
|
||||
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import art.arcane.volmlib.util.matter.slices.CavernMatter;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class IrisCaveCarver3DCellKernelTest {
|
||||
private static final long CARVE_SEED = -54_777_863_784_492_918L;
|
||||
private static final int WORLD_HEIGHT = 160;
|
||||
private static final int CHUNK_SPAN = 4;
|
||||
|
||||
@Test
|
||||
public void warpModulesProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.WARP_MODULES, 0xdf617c77d3c3cc5eL, 67798);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void warpOnlyTerrainProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.WARP_TERRAIN, 0x7e0b63cab5bb3904L, 67036);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void styledThresholdBlendProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.STYLED_BLEND, 0x25792d49143774faL, 30387);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fluidBoundaryProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.FLUID_BOUNDARY, 0xb102145856119afeL, 17584);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noWarpModulesProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.NO_WARP_MODULES, 0x5a83dc20c89c6398L, 47534);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalStepProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.VERTICAL_STEP, 0xac0413031ed65b61L, 48011);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void latticeProfileMatchesGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.LATTICE, 0xc7ffab155e6bdef9L, 72714);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floorFluidProfilesSharingChunksMatchGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.FLOOR_FLUID_LAYERED, 0xb4c0fb23e071617dL, 68747);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packStyleProfilesSharingFieldsMatchGoldenWriteSequence() throws Exception {
|
||||
assertScenario(Scenario.PACK_SHARED, 0xdbe20a1ca87f9cc7L, 269035);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cellKernelDecidesCellsWithoutColumnEvaluation() throws Exception {
|
||||
IrisCaveCarver3D.CellCounters before = IrisCaveCarver3D.cellCounters();
|
||||
run(Scenario.WARP_MODULES, false);
|
||||
IrisCaveCarver3D.CellCounters after = IrisCaveCarver3D.cellCounters();
|
||||
long decided = after.decided() - before.decided();
|
||||
long evaluated = after.evaluated() - before.evaluated();
|
||||
assertTrue("decided=" + decided + " evaluated=" + evaluated, decided > 0L && evaluated > 0L);
|
||||
}
|
||||
|
||||
private void assertScenario(Scenario scenario, long expectedHash, int expectedWrites) throws Exception {
|
||||
Result serial = run(scenario, false);
|
||||
Result parallel = run(scenario, true);
|
||||
assertTrue(serial.carved > 0);
|
||||
assertEquals(serial.hash, parallel.hash);
|
||||
assertEquals(serial.writes, parallel.writes);
|
||||
assertEquals(serial.carved, parallel.carved);
|
||||
assertEquals(expectedHash, serial.hash);
|
||||
assertEquals(expectedWrites, serial.writes);
|
||||
}
|
||||
|
||||
private Result run(Scenario scenario, boolean parallel) throws Exception {
|
||||
if (!parallel) {
|
||||
return carveArea(scenario);
|
||||
}
|
||||
ForkJoinPool pool = new ForkJoinPool(4);
|
||||
try {
|
||||
return pool.submit(() -> carveArea(scenario)).get(120, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private Result carveArea(Scenario scenario) {
|
||||
Engine engine = createEngine(scenario);
|
||||
CaveDensityGrid grid = new CaveDensityGrid(4096);
|
||||
IrisCaveProfile[] profiles = scenario.profiles();
|
||||
IrisCaveCarver3D[] carvers = new IrisCaveCarver3D[profiles.length];
|
||||
for (int index = 0; index < profiles.length; index++) {
|
||||
carvers[index] = new IrisCaveCarver3D(engine, profiles[index], grid);
|
||||
}
|
||||
Capture capture = new Capture();
|
||||
int carved = 0;
|
||||
for (int chunkX = -2; chunkX < CHUNK_SPAN - 2; chunkX++) {
|
||||
for (int chunkZ = -1; chunkZ < CHUNK_SPAN - 1; chunkZ++) {
|
||||
MantleWriter writer = capture.writer(chunkX, chunkZ);
|
||||
CaveFluidSupportPlan supportPlan = new CaveFluidSupportPlan();
|
||||
for (int index = 0; index < carvers.length; index++) {
|
||||
carved += carvers[index].carve(writer, chunkX, chunkZ, scenario.weights(index, chunkX, chunkZ),
|
||||
0.08D, 0.24D, scenario.worldYRange(), scenario.surfaceHeights(chunkX, chunkZ),
|
||||
scenario.boundaries(chunkX, chunkZ), null, supportPlan);
|
||||
}
|
||||
capture.resolving = true;
|
||||
supportPlan.resolve(writer.acquireChunk(chunkX, chunkZ));
|
||||
capture.finishResolve();
|
||||
}
|
||||
}
|
||||
return new Result(capture.hash, capture.writes, carved);
|
||||
}
|
||||
|
||||
private Engine createEngine(Scenario scenario) {
|
||||
Engine engine = mock(Engine.class, withSettings().stubOnly());
|
||||
IrisData data = mock(IrisData.class, withSettings().stubOnly());
|
||||
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
|
||||
SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
|
||||
doReturn(data).when(engine).getData();
|
||||
doReturn(dimension).when(engine).getDimension();
|
||||
doReturn(seedManager).when(engine).getSeedManager();
|
||||
doReturn(CARVE_SEED).when(seedManager).getCarve();
|
||||
doReturn(new EngineMetrics(16)).when(engine).getMetrics();
|
||||
doReturn(IrisWorld.builder().minHeight(-32).maxHeight(WORLD_HEIGHT - 32).build()).when(engine).getWorld();
|
||||
doReturn(WORLD_HEIGHT).when(engine).getHeight();
|
||||
doReturn(110).when(engine).getHeight(anyInt(), anyInt());
|
||||
doReturn(24).when(dimension).getCaveLavaHeight();
|
||||
doReturn(70).when(dimension).getFluidHeight();
|
||||
doReturn(scenario == Scenario.VERTICAL_STEP ? 4 : 1).when(dimension).getCaveDensitySamplingStep();
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static IrisCaveProfile baseProfile(boolean warp, boolean modules) {
|
||||
IrisCaveProfile profile = new IrisCaveProfile();
|
||||
profile.setEnabled(true);
|
||||
profile.setVerticalRange(new IrisRange(4D, 150D));
|
||||
profile.setVerticalEdgeFade(14);
|
||||
profile.setVerticalEdgeFadeStrength(0.21D);
|
||||
profile.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.21D));
|
||||
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.43D));
|
||||
profile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.37D));
|
||||
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.140625D));
|
||||
profile.setBaseWeight(1D);
|
||||
profile.setDetailWeight(0.42D);
|
||||
profile.setWarpStrength(warp ? 3.7D : 0D);
|
||||
profile.setDensityThreshold(new IrisStyledRange(-0.18D, -0.18D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
|
||||
profile.setThresholdBias(0D);
|
||||
profile.setSampleStep(1);
|
||||
profile.setSurfaceClearance(5);
|
||||
profile.setAllowSurfaceBreak(true);
|
||||
profile.setSurfaceBreakNoiseThreshold(0.3D);
|
||||
profile.setSurfaceBreakDepth(14);
|
||||
profile.setSurfaceBreakThresholdBoost(0.21D);
|
||||
profile.setAllowFluid(false);
|
||||
profile.setAllowLava(true);
|
||||
KList<IrisCaveFieldModule> caveModules = new KList<>();
|
||||
if (modules) {
|
||||
caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.33D),
|
||||
0.27D, 0.05D, new IrisRange(0D, 90D), false));
|
||||
caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.51D),
|
||||
0.19D, -0.07D, new IrisRange(40D, 150D), true));
|
||||
}
|
||||
profile.setModules(caveModules);
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static IrisCaveProfile packProfile(NoiseStyle warp, double warpZoom, double warpStrength, NoiseStyle base,
|
||||
double baseZoom, double threshold, IrisCaveFieldModule... modules) {
|
||||
IrisCaveProfile profile = baseProfile(true, false);
|
||||
profile.setWarpStyle(new IrisGeneratorStyle(warp).zoomed(warpZoom));
|
||||
profile.setWarpStrength(warpStrength);
|
||||
profile.setBaseDensityStyle(new IrisGeneratorStyle(base).zoomed(baseZoom));
|
||||
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.5D));
|
||||
profile.setBaseWeight(0.82D);
|
||||
profile.setDetailWeight(0.15D);
|
||||
profile.setDensityThreshold(new IrisStyledRange(threshold - 0.06D, threshold + 0.05D,
|
||||
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.78D)));
|
||||
profile.setThresholdBias(0.12D);
|
||||
profile.setSampleStep(2);
|
||||
profile.setModules(new KList<>(modules));
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static IrisCaveFieldModule packModule(NoiseStyle style, double zoom, double weight, double threshold,
|
||||
double minY, double maxY, boolean invert) {
|
||||
return new IrisCaveFieldModule(new IrisGeneratorStyle(style).zoomed(zoom), weight, threshold,
|
||||
new IrisRange(minY, maxY), invert);
|
||||
}
|
||||
|
||||
private enum Scenario {
|
||||
PACK_SHARED,
|
||||
WARP_MODULES,
|
||||
WARP_TERRAIN,
|
||||
STYLED_BLEND,
|
||||
FLUID_BOUNDARY,
|
||||
NO_WARP_MODULES,
|
||||
VERTICAL_STEP,
|
||||
LATTICE,
|
||||
FLOOR_FLUID_LAYERED;
|
||||
|
||||
IrisCaveProfile[] profiles() {
|
||||
if (this == PACK_SHARED) {
|
||||
return new IrisCaveProfile[]{
|
||||
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D, -0.02D,
|
||||
packModule(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 0.08D, 0.03D, 10D, 140D, false),
|
||||
packModule(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 0.05D, 0.07D, 4D, 90D, true)),
|
||||
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D, 0.01D,
|
||||
packModule(NoiseStyle.CELLULAR_BILINEAR, 1.32D, 0.06D, 0.02D, 20D, 150D, false)),
|
||||
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D, 0.04D,
|
||||
packModule(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 0.08D, 0.03D, 10D, 140D, false),
|
||||
packModule(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 0.05D, 0.07D, 4D, 90D, true)),
|
||||
packProfile(NoiseStyle.PERLIN_IRIS, 0.35D, 2.4D, NoiseStyle.IRIS_DOUBLE, 0.68D, -0.04D,
|
||||
packModule(NoiseStyle.VASCULAR_THIN, 1.28D, 0.04D, 0.05D, 8D, 120D, false))
|
||||
};
|
||||
}
|
||||
if (this == FLOOR_FLUID_LAYERED) {
|
||||
return new IrisCaveProfile[]{
|
||||
baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(4),
|
||||
baseProfile(true, false).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(2)
|
||||
.setAllowLava(false).setDensityThreshold(new IrisStyledRange(-0.24D, -0.08D,
|
||||
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.45D)))
|
||||
};
|
||||
}
|
||||
return new IrisCaveProfile[]{profile()};
|
||||
}
|
||||
|
||||
IrisCaveProfile profile() {
|
||||
return switch (this) {
|
||||
case WARP_MODULES, VERTICAL_STEP -> baseProfile(true, true);
|
||||
case WARP_TERRAIN -> baseProfile(true, false);
|
||||
case STYLED_BLEND -> baseProfile(false, false).setDensityThreshold(new IrisStyledRange(-0.32D, -0.04D,
|
||||
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.6D)));
|
||||
case FLUID_BOUNDARY -> baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(false)
|
||||
.setFluidMinDepthBelowSurface(6).setAllowLava(false);
|
||||
case NO_WARP_MODULES -> baseProfile(false, true);
|
||||
case LATTICE -> baseProfile(true, true).setSampleStep(3);
|
||||
case FLOOR_FLUID_LAYERED, PACK_SHARED -> baseProfile(true, true);
|
||||
};
|
||||
}
|
||||
|
||||
double[] weights(int layer, int chunkX, int chunkZ) {
|
||||
double[] weights = new double[256];
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int worldX = (chunkX << 4) + (index >> 4);
|
||||
int worldZ = (chunkZ << 4) + (index & 15);
|
||||
double blend = 0.5D + 0.6D * Math.sin(worldX * 0.11D) * Math.cos(worldZ * 0.07D);
|
||||
weights[index] = switch (this) {
|
||||
case PACK_SHARED -> 0.5D + 0.6D * Math.sin(worldX * 0.13D + layer * 1.7D) * Math.cos(worldZ * 0.09D - layer);
|
||||
case STYLED_BLEND, FLUID_BOUNDARY -> blend;
|
||||
case FLOOR_FLUID_LAYERED -> layer == 0 ? blend : 1D - blend;
|
||||
default -> 1D;
|
||||
};
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
|
||||
int[] surfaceHeights(int chunkX, int chunkZ) {
|
||||
int[] heights = new int[256];
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int worldX = (chunkX << 4) + (index >> 4);
|
||||
int worldZ = (chunkZ << 4) + (index & 15);
|
||||
heights[index] = this == WARP_MODULES || this == LATTICE || this == FLOOR_FLUID_LAYERED
|
||||
? 118
|
||||
: 96 + (int) Math.round(22D * Math.sin(worldX * 0.09D) + 15D * Math.cos(worldZ * 0.13D));
|
||||
}
|
||||
return heights;
|
||||
}
|
||||
|
||||
IrisRange worldYRange() {
|
||||
return this == FLUID_BOUNDARY || this == FLOOR_FLUID_LAYERED ? new IrisRange(-20D, 100D) : null;
|
||||
}
|
||||
|
||||
long[] boundaries(int chunkX, int chunkZ) {
|
||||
long[] boundaries = new long[256];
|
||||
Arrays.fill(boundaries, SurfaceFluidBoundaryPlan.boundary(SurfaceFluidBoundaryPlan.NO_BOUNDARY, Integer.MIN_VALUE));
|
||||
if (this != FLUID_BOUNDARY && this != WARP_TERRAIN) {
|
||||
return boundaries;
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int worldX = (chunkX << 4) + (index >> 4);
|
||||
if (Math.floorMod(worldX + chunkZ, 5) == 0) {
|
||||
boundaries[index] = SurfaceFluidBoundaryPlan.boundary(60 + Math.floorMod(index, 7), 82);
|
||||
}
|
||||
}
|
||||
return boundaries;
|
||||
}
|
||||
}
|
||||
|
||||
private record Result(long hash, int writes, int carved) {
|
||||
}
|
||||
|
||||
private static final class Capture {
|
||||
private final Map<Long, Map<Integer, Matter>> chunks = new HashMap<>();
|
||||
private final IdentityHashMap<MatterCavern, Integer> identities = new IdentityHashMap<>();
|
||||
private final List<Long> resolved = new ArrayList<>();
|
||||
private long hash = 0xcbf29ce484222325L;
|
||||
private int writes;
|
||||
private boolean resolving;
|
||||
|
||||
private MantleWriter writer(int chunkX, int chunkZ) {
|
||||
MantleWriter writer = mock(MantleWriter.class, withSettings().stubOnly());
|
||||
@SuppressWarnings("unchecked")
|
||||
Mantle<Matter> mantle = mock(Mantle.class, withSettings().stubOnly());
|
||||
@SuppressWarnings("unchecked")
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
|
||||
long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
|
||||
Map<Integer, Matter> sections = chunks.computeIfAbsent(key, ignored -> new HashMap<>());
|
||||
doReturn(mantle).when(writer).getMantle();
|
||||
doReturn(WORLD_HEIGHT).when(mantle).getWorldHeight();
|
||||
doReturn(chunk).when(writer).acquireChunk(anyInt(), anyInt());
|
||||
doAnswer(invocation -> sections.get(invocation.getArgument(0, Integer.class))).when(chunk).get(anyInt());
|
||||
doAnswer(invocation -> sections.computeIfAbsent(invocation.getArgument(0, Integer.class),
|
||||
sectionIndex -> createSection(chunkX, chunkZ, sectionIndex))).when(chunk).getOrCreate(anyInt());
|
||||
return writer;
|
||||
}
|
||||
|
||||
private Matter createSection(int chunkX, int chunkZ, int sectionIndex) {
|
||||
mix(0x5EC7L);
|
||||
mix(chunkX);
|
||||
mix(chunkZ);
|
||||
mix(sectionIndex);
|
||||
Matter matter = mock(Matter.class, withSettings().stubOnly());
|
||||
MatterSlice<MatterCavern> slice = new RecordingCavernMatter(this, sectionIndex);
|
||||
doReturn(slice).when(matter).slice(MatterCavern.class);
|
||||
doReturn(slice).when(matter).getSlice(MatterCavern.class);
|
||||
return matter;
|
||||
}
|
||||
|
||||
private void record(int sectionIndex, int x, int y, int z, MatterCavern value) {
|
||||
writes++;
|
||||
if (resolving) {
|
||||
resolved.add(((long) sectionIndex << 20) | ((long) x << 16) | ((long) y << 12) | ((long) z << 8) | value.getLiquid());
|
||||
return;
|
||||
}
|
||||
mix(sectionIndex);
|
||||
mix((x << 8) | (y << 4) | z);
|
||||
mix(value == null ? -1 : value.getLiquid());
|
||||
Integer identity = identities.get(value);
|
||||
if (identity == null) {
|
||||
identity = identities.size();
|
||||
identities.put(value, identity);
|
||||
}
|
||||
mix(identity);
|
||||
}
|
||||
|
||||
private void finishResolve() {
|
||||
resolving = false;
|
||||
resolved.sort(Long::compare);
|
||||
for (long entry : resolved) {
|
||||
mix(entry);
|
||||
}
|
||||
resolved.clear();
|
||||
}
|
||||
|
||||
private void mix(long value) {
|
||||
hash ^= value;
|
||||
hash *= 0x100000001b3L;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingCavernMatter extends CavernMatter {
|
||||
private final Capture capture;
|
||||
private final int sectionIndex;
|
||||
|
||||
private RecordingCavernMatter(Capture capture, int sectionIndex) {
|
||||
super(16, 16, 16);
|
||||
this.capture = capture;
|
||||
this.sectionIndex = sectionIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, MatterCavern value) {
|
||||
super.set(x, y, z, value);
|
||||
capture.record(sectionIndex, x, y, z, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
|
||||
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class IrisCaveCarver3DLatticeTest {
|
||||
private static final long CARVE_SEED = 7_340_119_802_557L;
|
||||
private static final int WORLD_HEIGHT = 384;
|
||||
|
||||
@Test
|
||||
public void batchedLatticeColumnsMatchScalarPlaneDensityBitForBit() throws Exception {
|
||||
Engine engine = createEngine();
|
||||
CaveDensityGrid grid = new CaveDensityGrid(256);
|
||||
IrisCaveProfile[] profiles = {
|
||||
profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D,
|
||||
module(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 10D, 140D, false),
|
||||
module(NoiseStyle.CELLULAR_BILINEAR, 1.32D, 4D, 300D, true)),
|
||||
profile(NoiseStyle.PERLIN_IRIS, 0.35D, 2.4D, NoiseStyle.IRIS_DOUBLE, 0.68D,
|
||||
module(NoiseStyle.VASCULAR_THIN, 1.28D, 8D, 220D, false)),
|
||||
profile(NoiseStyle.SIMPLEX, 0.5D, 0D, NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D)
|
||||
};
|
||||
Method sampleLatticeColumn = IrisCaveCarver3D.class.getDeclaredMethod("sampleLatticeColumn",
|
||||
CaveCarveScratch.class, CaveDensityGrid.Column.class, int.class, int.class, int.class);
|
||||
sampleLatticeColumn.setAccessible(true);
|
||||
Method samplePlaneDensity = IrisCaveCarver3D.class.getDeclaredMethod("samplePlaneDensity",
|
||||
CaveCarveScratch.class, int.class, int.class, int.class);
|
||||
samplePlaneDensity.setAccessible(true);
|
||||
Field warpOwnerField = IrisCaveCarver3D.class.getDeclaredField("warpOwner");
|
||||
warpOwnerField.setAccessible(true);
|
||||
Random random = new Random(91_337L);
|
||||
for (IrisCaveProfile profile : profiles) {
|
||||
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile, grid);
|
||||
CaveDensityGrid.Owner warpOwner = (CaveDensityGrid.Owner) warpOwnerField.get(carver);
|
||||
CaveCarveScratch batch = new CaveCarveScratch();
|
||||
CaveCarveScratch reference = new CaveCarveScratch();
|
||||
for (int trial = 0; trial < 48; trial++) {
|
||||
int latticeX = random.nextInt(4001) - 2000;
|
||||
int latticeZ = random.nextInt(4001) - 2000;
|
||||
int x = latticeX << CaveDensityGrid.LATTICE_SHIFT;
|
||||
int z = latticeZ << CaveDensityGrid.LATTICE_SHIFT;
|
||||
int count = selectRows(random, batch);
|
||||
CaveDensityGrid.Column warpColumn = trial % 3 == 0 ? null
|
||||
: grid.warpColumn(warpOwner, WORLD_HEIGHT + 1, latticeX, latticeZ);
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
sampleLatticeColumn.invoke(carver, batch, warpColumn, x, z, count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
int y = batch.latticeY[index];
|
||||
double expected = (double) samplePlaneDensity.invoke(carver, reference, x, y, z);
|
||||
assertEquals("x=" + x + " y=" + y + " z=" + z + " pass=" + pass,
|
||||
Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(batch.latticeDensity[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void identicalFieldsShareGridOwnersAcrossProfiles() throws Exception {
|
||||
Engine engine = createEngine();
|
||||
CaveDensityGrid grid = new CaveDensityGrid(256);
|
||||
Field densityOwnerField = IrisCaveCarver3D.class.getDeclaredField("densityOwner");
|
||||
densityOwnerField.setAccessible(true);
|
||||
Field warpOwnerField = IrisCaveCarver3D.class.getDeclaredField("warpOwner");
|
||||
warpOwnerField.setAccessible(true);
|
||||
IrisCaveCarver3D first = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
|
||||
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true)), grid);
|
||||
IrisCaveCarver3D sameDensity = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
|
||||
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true))
|
||||
.setDensityThreshold(new IrisStyledRange(0.2D, 0.2D, new IrisGeneratorStyle(NoiseStyle.FLAT))), grid);
|
||||
IrisCaveCarver3D sameWarp = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 1.7D,
|
||||
NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D), grid);
|
||||
IrisCaveCarver3D otherWarp = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.46D, 3.2D,
|
||||
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true)), grid);
|
||||
IrisCaveCarver3D otherModule = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
|
||||
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 91D, true)), grid);
|
||||
assertSame(densityOwnerField.get(first), densityOwnerField.get(sameDensity));
|
||||
assertSame(warpOwnerField.get(first), warpOwnerField.get(sameDensity));
|
||||
assertSame(warpOwnerField.get(first), warpOwnerField.get(sameWarp));
|
||||
assertSame(warpOwnerField.get(first), warpOwnerField.get(otherModule));
|
||||
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(sameWarp));
|
||||
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(otherModule));
|
||||
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(otherWarp));
|
||||
assertNotSame(warpOwnerField.get(first), warpOwnerField.get(otherWarp));
|
||||
}
|
||||
|
||||
private static int selectRows(Random random, CaveCarveScratch scratch) {
|
||||
int start = random.nextInt(40);
|
||||
int end = start + 20 + random.nextInt(WORLD_HEIGHT - start - 20);
|
||||
scratch.ensureLatticeBatch(end - start + 1);
|
||||
int count = 0;
|
||||
for (int y = start; y <= end; y++) {
|
||||
if (random.nextInt(5) != 0) {
|
||||
scratch.latticeY[count++] = y;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static IrisCaveProfile profile(NoiseStyle warp, double warpZoom, double warpStrength, NoiseStyle base,
|
||||
double baseZoom, IrisCaveFieldModule... modules) {
|
||||
IrisCaveProfile profile = new IrisCaveProfile();
|
||||
profile.setEnabled(true);
|
||||
profile.setVerticalRange(new IrisRange(4D, 360D));
|
||||
profile.setWarpStyle(new IrisGeneratorStyle(warp).zoomed(warpZoom));
|
||||
profile.setWarpStrength(warpStrength);
|
||||
profile.setBaseDensityStyle(new IrisGeneratorStyle(base).zoomed(baseZoom));
|
||||
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.5D));
|
||||
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.82D));
|
||||
profile.setBaseWeight(0.82D);
|
||||
profile.setDetailWeight(0.15D);
|
||||
profile.setDensityThreshold(new IrisStyledRange(-0.1D, 0.02D, new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.78D)));
|
||||
profile.setSampleStep(2);
|
||||
profile.setModules(new KList<>(modules));
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static IrisCaveFieldModule module(NoiseStyle style, double zoom, double minY, double maxY, boolean invert) {
|
||||
return new IrisCaveFieldModule(new IrisGeneratorStyle(style).zoomed(zoom), 0.07D, 0.04D, new IrisRange(minY, maxY), invert);
|
||||
}
|
||||
|
||||
private static Engine createEngine() {
|
||||
Engine engine = mock(Engine.class, withSettings().stubOnly());
|
||||
IrisData data = mock(IrisData.class, withSettings().stubOnly());
|
||||
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
|
||||
SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
|
||||
doReturn(data).when(engine).getData();
|
||||
doReturn(dimension).when(engine).getDimension();
|
||||
doReturn(seedManager).when(engine).getSeedManager();
|
||||
doReturn(CARVE_SEED).when(seedManager).getCarve();
|
||||
doReturn(new EngineMetrics(16)).when(engine).getMetrics();
|
||||
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(WORLD_HEIGHT - 64).build()).when(engine).getWorld();
|
||||
doReturn(WORLD_HEIGHT).when(engine).getHeight();
|
||||
doReturn(200).when(engine).getHeight(anyInt(), anyInt());
|
||||
doReturn(20).when(dimension).getCaveLavaHeight();
|
||||
doReturn(64).when(dimension).getFluidHeight();
|
||||
doReturn(1).when(dimension).getCaveDensitySamplingStep();
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
+27
-20
@@ -13,6 +13,7 @@ import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.noise.program.NoiseProgram;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
@@ -148,7 +149,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
sampledY.add((int) y);
|
||||
return x * 0.0001D + y * 0.01D + z * 0.0002D;
|
||||
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
baseDensityField.set(carver, density);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
|
||||
Method prepare = IrisCaveCarver3D.class.getDeclaredMethod("prepareVerticalDensityPlane",
|
||||
CaveCarveScratch.class, int.class, int.class, int.class);
|
||||
prepare.setAccessible(true);
|
||||
@@ -168,7 +169,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
doReturn(1).when(dimension).getCaveDensitySamplingStep();
|
||||
IrisCaveCarver3D scalar = new IrisCaveCarver3D(engine,
|
||||
createProfile(false, false).setDetailWeight(0D).setAllowFluid(false));
|
||||
baseDensityField.set(scalar, density);
|
||||
baseDensityField.set(scalar, NoiseProgram.compileSigned3D(density));
|
||||
CaveCarveScratch scalarScratch = new CaveCarveScratch();
|
||||
for (int index = 0; index < 256; index++) {
|
||||
scalarScratch.planeColumnIndices[index] = index;
|
||||
@@ -292,10 +293,10 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
|
||||
CNG base = mock(CNG.class, withSettings().stubOnly());
|
||||
doReturn(1D).when(base).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
baseDensityField.set(carver, base);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(base));
|
||||
CNG module = mock(CNG.class, withSettings().stubOnly());
|
||||
doReturn(-1D).when(module).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], module);
|
||||
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], NoiseProgram.compileSigned3D(module));
|
||||
WriterCapture capture = createWriterCapture(128);
|
||||
carver.carve(capture.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
|
||||
assertEquals(512, capture.carvedCells.size());
|
||||
@@ -364,13 +365,13 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}
|
||||
return -1D;
|
||||
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
baseDensityField.set(carver, density);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
|
||||
assertThrows(IllegalStateException.class, () -> carver.carve(capture.writer, 0, 0,
|
||||
fullWeights(), 0D, 0D, null, filledHeights(110)));
|
||||
} else {
|
||||
carver.carve(capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(110));
|
||||
assertFalse(capture.carvedCells.isEmpty());
|
||||
}
|
||||
assertFalse(capture.carvedCells.isEmpty());
|
||||
Field scratchField = IrisCaveCarver3D.class.getDeclaredField("scratchCache");
|
||||
scratchField.setAccessible(true);
|
||||
ThreadLocal<?> scratchCache = (ThreadLocal<?>) scratchField.get(carver);
|
||||
@@ -385,6 +386,12 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
for (CaveDensityGrid.Column column : scratch.densityColumns) {
|
||||
assertNull(column);
|
||||
}
|
||||
for (CaveDensityGrid.Column column : scratch.cellLattice.densityColumns) {
|
||||
assertNull(column);
|
||||
}
|
||||
for (CaveDensityGrid.Column column : scratch.cellLattice.warpColumns) {
|
||||
assertNull(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +446,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}).when(detail).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
Field detailField = IrisCaveCarver3D.class.getDeclaredField("detailDensity");
|
||||
detailField.setAccessible(true);
|
||||
detailField.set(carver, detail);
|
||||
detailField.set(carver, NoiseProgram.compileSigned3D(detail));
|
||||
WriterCapture capture = createWriterCapture(128);
|
||||
try (ForkJoinPool pool = new ForkJoinPool(4)) {
|
||||
assertEquals(0, pool.submit(() -> carver.carve(capture.writer, 0, 0, fullWeights(),
|
||||
@@ -772,7 +779,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
rejectedDensity.put("11:10:10", -1D);
|
||||
rejectedDensity.put("9:10:10", -1D);
|
||||
CoordinateDensityCNG rejectedNoise = new CoordinateDensityCNG(rejectedDensity);
|
||||
baseDensityField.set(carver, rejectedNoise);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(rejectedNoise));
|
||||
|
||||
boolean rejected = (boolean) aquiferCupSupportMethod.invoke(
|
||||
carver, new CaveCarveScratch(), 10, 10, 10, 0D);
|
||||
@@ -781,7 +788,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
assertEquals(List.of("10:9:10", "10:8:10", "11:10:10", "9:10:10"), rejectedNoise.sampledCoordinates);
|
||||
|
||||
CoordinateDensityCNG acceptedNoise = new CoordinateDensityCNG(Map.of());
|
||||
baseDensityField.set(carver, acceptedNoise);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(acceptedNoise));
|
||||
|
||||
boolean accepted = (boolean) aquiferCupSupportMethod.invoke(
|
||||
carver, new CaveCarveScratch(), 10, 10, 10, 0D);
|
||||
@@ -1100,7 +1107,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
AtomicLong isolatedSamples = new AtomicLong();
|
||||
IrisCaveCarver3D shared = new IrisCaveCarver3D(engine,
|
||||
createProfile(warp, modules).setAdaptiveSampling(true), new CaveDensityGrid(4096));
|
||||
baseDensityField.set(shared, new CountingCNG((CNG) baseDensityField.get(shared), sharedSamples));
|
||||
baseDensityField.set(shared, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) baseDensityField.get(shared), sharedSamples)));
|
||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
|
||||
WriterCapture sharedCapture = createWriterCapture(96);
|
||||
@@ -1108,7 +1115,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
null, filledHeights(90));
|
||||
IrisCaveCarver3D isolated = new IrisCaveCarver3D(engine,
|
||||
createProfile(warp, modules).setAdaptiveSampling(true));
|
||||
baseDensityField.set(isolated, new CountingCNG((CNG) baseDensityField.get(isolated), isolatedSamples));
|
||||
baseDensityField.set(isolated, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) baseDensityField.get(isolated), isolatedSamples)));
|
||||
WriterCapture isolatedCapture = createWriterCapture(96);
|
||||
int isolatedCarved = isolated.carve(isolatedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D,
|
||||
null, filledHeights(90));
|
||||
@@ -1241,7 +1248,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
new IrisCaveCarver3D(engine, profile).carve(expected.writer, -1, 2,
|
||||
fullWeights(), 0D, 0D, null, filledHeights(110));
|
||||
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
|
||||
CNG original = (CNG) baseDensityField.get(carver);
|
||||
NoiseProgram original = (NoiseProgram) baseDensityField.get(carver);
|
||||
CNG density = mock(CNG.class, withSettings().stubOnly());
|
||||
AtomicBoolean entered = new AtomicBoolean();
|
||||
WriterCapture nested = createWriterCapture(128);
|
||||
@@ -1252,9 +1259,9 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
double x = invocation.getArgument(0);
|
||||
double y = invocation.getArgument(1);
|
||||
double z = invocation.getArgument(2);
|
||||
return original.noiseFastSigned3D(x, y, z);
|
||||
return original.sampleSigned3D(x, y, z);
|
||||
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
|
||||
baseDensityField.set(carver, density);
|
||||
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
|
||||
WriterCapture actual = createWriterCapture(128);
|
||||
carver.carve(actual.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
|
||||
assertTrue(entered.get());
|
||||
@@ -1268,7 +1275,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
doAnswer(invocation -> {
|
||||
lookups.incrementAndGet();
|
||||
return invocation.callRealMethod();
|
||||
}).when(grid).column(any(), anyInt(), anyInt(), anyInt(), anyInt());
|
||||
}).when(grid).column(any(), anyInt(), anyInt(), anyInt());
|
||||
return grid;
|
||||
}
|
||||
|
||||
@@ -1292,10 +1299,10 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}
|
||||
|
||||
private void countSkippableSamples(IrisCaveCarver3D carver, AtomicLong samples) throws Exception {
|
||||
detailDensityField.set(carver, new CountingCNG((CNG) detailDensityField.get(carver), samples));
|
||||
detailDensityField.set(carver, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) detailDensityField.get(carver), samples)));
|
||||
CaveFieldModuleState[] modules = (CaveFieldModuleState[]) modulesField.get(carver);
|
||||
for (CaveFieldModuleState module : modules) {
|
||||
moduleDensityField.set(module, new CountingCNG((CNG) moduleDensityField.get(module), samples));
|
||||
moduleDensityField.set(module, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) moduleDensityField.get(module), samples)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1893,10 +1900,10 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
}
|
||||
|
||||
private static final class CountingCNG extends CNG {
|
||||
private final CNG delegate;
|
||||
private final NoiseProgram delegate;
|
||||
private final AtomicLong samples;
|
||||
|
||||
private CountingCNG(CNG delegate, AtomicLong samples) {
|
||||
private CountingCNG(NoiseProgram delegate, AtomicLong samples) {
|
||||
super(new RNG(41_117L));
|
||||
this.delegate = delegate;
|
||||
this.samples = samples;
|
||||
@@ -1905,7 +1912,7 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
@Override
|
||||
public double noiseFastSigned3D(double x, double y, double z) {
|
||||
samples.incrementAndGet();
|
||||
return delegate.noiseFastSigned3D(x, y, z);
|
||||
return delegate.sampleSigned3D(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-20
@@ -31,11 +31,6 @@ import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -45,22 +40,12 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
||||
@Rule
|
||||
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void transformedFloatingPlacementExpandsOwnerRadius() throws Exception {
|
||||
File objectFile = temporaryFolder.newFile("floating.iob");
|
||||
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
||||
output.writeInt(1);
|
||||
output.writeInt(1);
|
||||
output.writeInt(1);
|
||||
}
|
||||
|
||||
public void transformedFloatingPlacementExpandsOwnerRadius() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
.setPlace(new KList<>("test/floating"))
|
||||
.setTranslate(new IrisObjectTranslate().setX(32));
|
||||
.setTranslate(new IrisObjectTranslate().setX(-32));
|
||||
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
|
||||
.setFloatingObjects(new KList<>(placement));
|
||||
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
|
||||
@@ -69,7 +54,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
||||
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
|
||||
|
||||
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
||||
when(objectLoader.findFile("test/floating")).thenReturn(objectFile);
|
||||
when(objectLoader.load("test/floating")).thenReturn(new IrisObject(1, 1, 1));
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getObjectLoader()).thenReturn(objectLoader);
|
||||
|
||||
@@ -79,8 +64,8 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
|
||||
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||
assertEquals(32, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
|
||||
assertEquals(34, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
|
||||
}
|
||||
}
|
||||
+32
-52
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -33,13 +32,8 @@ import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -60,9 +54,6 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
@@ -92,7 +83,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
||||
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
||||
|
||||
assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement, 1D));
|
||||
assertEquals(27, PlacementReach.of(new IrisObject(5, 48, 7), placement, 1D).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,7 +94,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
.setMode(ObjectPlaceMode.VACUUM)
|
||||
.setVacuumSettings(new IrisVacuumSettings().setRadius(12));
|
||||
|
||||
assertEquals(39, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement, placement.getMaximumScale(null)));
|
||||
assertEquals(22, PlacementReach.of(new IrisObject(5, 4, 3), placement, placement.getMaximumScale(null)).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -126,15 +117,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
public void inheritedRadiusMatchesScaledFrameAndExplicitScaleOverridesIt() {
|
||||
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
|
||||
IrisObjectPlacement inherited = new IrisObjectPlacement();
|
||||
IrisBlockVector size = new IrisBlockVector(8, 8, 8);
|
||||
assertEquals(16, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
||||
inherited.getMaximumScale(dimension)));
|
||||
IrisObject object = new IrisObject(8, 8, 8);
|
||||
assertEquals(8, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||
dimension.setAllObjectScaleFactor(0.5D);
|
||||
assertEquals(4, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
||||
inherited.getMaximumScale(dimension)));
|
||||
assertEquals(2, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||
inherited.setScale(new IrisObjectScale().setSize(1D));
|
||||
assertEquals(8, MantleObjectComponent.calculatePlacementReach(size, inherited,
|
||||
inherited.getMaximumScale(dimension)));
|
||||
assertEquals(4, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -183,10 +171,11 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
@Test
|
||||
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
|
||||
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, false));
|
||||
assertEquals(130, MantleObjectComponent.calculateInputRadius(33, true));
|
||||
assertEquals(84, MantleObjectComponent.calculateInputRadius(33, 35, false));
|
||||
assertEquals(132, MantleObjectComponent.calculateInputRadius(33, 35, true));
|
||||
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, 0, false));
|
||||
assertEquals(Integer.MAX_VALUE,
|
||||
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
|
||||
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, Integer.MAX_VALUE, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -199,7 +188,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
for (int radius = 0; radius < 300; radius++) {
|
||||
for (boolean collisions : new boolean[]{false, true}) {
|
||||
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
|
||||
<= MantleObjectComponent.calculateInputRadius(radius, collisions));
|
||||
<= MantleObjectComponent.calculateInputRadius(radius, radius, collisions));
|
||||
}
|
||||
}
|
||||
assertEquals(Integer.MAX_VALUE,
|
||||
@@ -231,27 +220,25 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
|
||||
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
|
||||
assertEquals(27, new MantleObjectComponent(engineMantle).getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
.setPlace(new KList<>("upper/tree"))
|
||||
.setRotation(IrisObjectRotation.of(90, 0, 0))
|
||||
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
|
||||
placement.getForbiddenCollisions().add("upper/blocker");
|
||||
IrisData baseData = mock(IrisData.class);
|
||||
IrisData upperData = mock(IrisData.class);
|
||||
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
|
||||
when(proceduralPlacement.getVariantObjects(upperData))
|
||||
.thenReturn(new KList<>(new IrisObject(5, 48, 7)));
|
||||
when(proceduralPlacement.asPlacement()).thenReturn(placement);
|
||||
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
|
||||
when(proceduralObjects.isEmpty()).thenReturn(false);
|
||||
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
|
||||
ResourceLoader<IrisObject> upperObjects = mock(ResourceLoader.class);
|
||||
when(upperObjects.load("upper/tree")).thenReturn(new IrisObject(5, 48, 7));
|
||||
when(upperData.getObjectLoader()).thenReturn(upperObjects);
|
||||
IrisRegion upperRegion = mock(IrisRegion.class);
|
||||
when(upperRegion.getObjects()).thenReturn(new KList<>());
|
||||
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
|
||||
when(upperRegion.getObjects()).thenReturn(new KList<>(placement));
|
||||
when(upperRegion.getSurfaceObjects()).thenReturn(new KList<>(placement));
|
||||
IrisDimension baseDimension = mock(IrisDimension.class);
|
||||
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
|
||||
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
|
||||
@@ -273,20 +260,13 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
|
||||
|
||||
assertEquals(53, component.getRadius());
|
||||
assertEquals(182, component.getInputRadius());
|
||||
assertEquals(5, component.getRadius());
|
||||
assertEquals(41, component.getInputRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
|
||||
File objectFile = temporaryFolder.newFile("tree.iob");
|
||||
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
||||
output.writeInt(1);
|
||||
output.writeInt(1);
|
||||
output.writeInt(1);
|
||||
}
|
||||
|
||||
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement()
|
||||
.setPlace(new KList<>("test/tree"))
|
||||
.setTranslate(new IrisObjectTranslate().setX(32));
|
||||
@@ -298,7 +278,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
|
||||
|
||||
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
||||
when(objectLoader.findFile("test/tree")).thenReturn(objectFile);
|
||||
when(objectLoader.load("test/tree")).thenReturn(new IrisObject(1, 1, 1));
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getObjectLoader()).thenReturn(objectLoader);
|
||||
|
||||
@@ -310,9 +290,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
|
||||
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
|
||||
assertEquals(33, component.getRadius());
|
||||
assertEquals(32, component.getRadius());
|
||||
assertEquals(0, component.getOutputRadius());
|
||||
assertEquals(82, component.getInputRadius());
|
||||
assertEquals(68, component.getInputRadius());
|
||||
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
@@ -341,9 +321,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||
|
||||
assertEquals(49, directOrigins.get());
|
||||
assertEquals(25, directOrigins.get());
|
||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||
assertEquals(49, directOrigins.get());
|
||||
assertEquals(25, directOrigins.get());
|
||||
|
||||
List<String> generatedChunks = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
@@ -370,14 +350,14 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
component.getRadius(),
|
||||
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
|
||||
);
|
||||
assertEquals(49, replayedSources.size());
|
||||
assertEquals("-3,-3", replayedSources.getFirst());
|
||||
assertEquals("-3,-2", replayedSources.get(1));
|
||||
assertEquals("3,3", replayedSources.getLast());
|
||||
assertEquals(25, replayedSources.size());
|
||||
assertEquals("-2,-2", replayedSources.getFirst());
|
||||
assertEquals("-2,-1", replayedSources.get(1));
|
||||
assertEquals("2,2", replayedSources.getLast());
|
||||
|
||||
placement.getForbiddenCollisions().add("test/tree");
|
||||
component.hotload();
|
||||
assertEquals(130, component.getInputRadius());
|
||||
assertEquals(100, component.getInputRadius());
|
||||
}
|
||||
|
||||
private static final class TestMatterGenerator implements MatterGenerator {
|
||||
|
||||
+90
-7
@@ -76,7 +76,7 @@ public class MatterGeneratorConcurrencyTest {
|
||||
@Test
|
||||
public void externalWindowsBoundManagedBlockedComponentSubmissions() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE;
|
||||
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_LIMIT;
|
||||
CountDownLatch admitted = new CountDownLatch(maximum);
|
||||
CountDownLatch excess = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
@@ -115,10 +115,10 @@ public class MatterGeneratorConcurrencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void externalWindowDefersSharedClaimsUntilItsOwnBatchesComplete() throws Exception {
|
||||
public void externalWindowDefersSharedClaimsUntilItsOwnSubmissionsComplete() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
List<MatterGenerator.MatterComponentTask> shared = new ArrayList<>();
|
||||
for (int z = -4; z < -4 + MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE; z++) {
|
||||
for (int z = -4; z < -4 + MatterGenerator.COMPONENT_SUBMISSION_LIMIT; z++) {
|
||||
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(
|
||||
fixture.mantle, -4, z, ReservedFlag.OBJECT);
|
||||
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
|
||||
@@ -137,7 +137,7 @@ public class MatterGeneratorConcurrencyTest {
|
||||
CompletableFuture<Void> generation = CompletableFuture.runAsync(
|
||||
() -> generator.generateMatter(0, 0, true, fixture.context));
|
||||
try {
|
||||
Await.until("all privately owned component batches", Duration.ofSeconds(10L),
|
||||
Await.until("all privately owned component submissions", Duration.ofSeconds(10L),
|
||||
() -> completed.get() == 81 - shared.size());
|
||||
assertFalse(generation.isDone());
|
||||
} finally {
|
||||
@@ -149,16 +149,17 @@ public class MatterGeneratorConcurrencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedSubmissionBatchDrainsItsWritersBeforeReturning() throws Exception {
|
||||
public void failedSubmissionDrainsItsWritersBeforeReturning() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
CountDownLatch failed = new CountDownLatch(1);
|
||||
CountDownLatch started = new CountDownLatch(MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE - 1);
|
||||
CountDownLatch started = new CountDownLatch(MatterGenerator.COMPONENT_SUBMISSION_LIMIT - 1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
IllegalStateException failure = new IllegalStateException("Component rejected its input");
|
||||
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
if (x == -4 && z == -4) {
|
||||
await(started);
|
||||
failed.countDown();
|
||||
throw failure;
|
||||
}
|
||||
@@ -172,7 +173,7 @@ public class MatterGeneratorConcurrencyTest {
|
||||
try {
|
||||
await(failed);
|
||||
await(started);
|
||||
if (MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE > 1) {
|
||||
if (MatterGenerator.COMPONENT_SUBMISSION_LIMIT > 1) {
|
||||
assertFalse(generation.isDone());
|
||||
verify(fixture.mantle.getChunk(-4, -4), never()).release();
|
||||
}
|
||||
@@ -186,6 +187,69 @@ public class MatterGeneratorConcurrencyTest {
|
||||
assertEquals(0, activeClaims(fixture, 0, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void externalWindowKeepsSubmittingPastAStragglingComponent() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicInteger completed = new AtomicInteger();
|
||||
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
if (x == -4 && z == -4) {
|
||||
awaitManaged(release);
|
||||
}
|
||||
completed.incrementAndGet();
|
||||
}
|
||||
};
|
||||
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 4, 0)));
|
||||
CompletableFuture<Void> generation = CompletableFuture.runAsync(
|
||||
() -> generator.generateMatter(0, 0, true, fixture.context));
|
||||
try {
|
||||
Await.until("every chunk but the straggler", Duration.ofSeconds(10L), () -> completed.get() == 80);
|
||||
assertFalse(generation.isDone());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
generation.get(10L, TimeUnit.SECONDS);
|
||||
assertEquals(81, completed.get());
|
||||
assertEquals(0, activeClaims(fixture, 0, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inlineWindowFinishesItsOwnChunksBeforeJoiningAClaimAnotherGenerationHolds() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
MantleChunk<Matter> held = fixture.mantle.getChunk(1, 0);
|
||||
CountDownLatch claimed = new CountDownLatch(1);
|
||||
CountDownLatch releaseClaim = new CountDownLatch(1);
|
||||
Set<Long> completed = ConcurrentHashMap.newKeySet();
|
||||
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
completed.add((((long) x) << 32) ^ (z & 0xffffffffL));
|
||||
}
|
||||
};
|
||||
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 1, 0)));
|
||||
CompletableFuture<Void> holder = CompletableFuture.runAsync(() -> {
|
||||
assertTrue(held.tryClaim(ReservedFlag.OBJECT));
|
||||
claimed.countDown();
|
||||
await(releaseClaim);
|
||||
held.completeClaim(ReservedFlag.OBJECT);
|
||||
});
|
||||
await(claimed);
|
||||
CompletableFuture<Void> generation = CompletableFuture.runAsync(
|
||||
() -> generator.generateMatter(0, 0, false, fixture.context));
|
||||
try {
|
||||
Await.until("the eight unclaimed chunks", Duration.ofSeconds(10L), () -> completed.size() == 8);
|
||||
assertFalse(generation.isDone());
|
||||
} finally {
|
||||
releaseClaim.countDown();
|
||||
}
|
||||
holder.get(10L, TimeUnit.SECONDS);
|
||||
generation.get(10L, TimeUnit.SECONDS);
|
||||
assertEquals(8, completed.size());
|
||||
assertTrue(held.isFlagged(ReservedFlag.OBJECT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundedAndReentrantGenerationMatchSerialComponentResults() throws Exception {
|
||||
Map<Long, Long> serial = componentResults(false, false);
|
||||
@@ -812,6 +876,25 @@ public class MatterGeneratorConcurrencyTest {
|
||||
}
|
||||
return null;
|
||||
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
|
||||
when(chunk.tryClaim(any())).thenAnswer(invocation -> {
|
||||
if (flagged.contains(invocation.getArgument(0)) || !lock.tryLock()) {
|
||||
return false;
|
||||
}
|
||||
if (flagged.contains(invocation.getArgument(0))) {
|
||||
lock.unlock();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
doAnswer(invocation -> {
|
||||
flagged.add(invocation.getArgument(0));
|
||||
lock.unlock();
|
||||
return null;
|
||||
}).when(chunk).completeClaim(any());
|
||||
doAnswer(invocation -> {
|
||||
lock.unlock();
|
||||
return null;
|
||||
}).when(chunk).abandonClaim(any());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.MeteredCache;
|
||||
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.structure.object.FloatingObjectFootprint;
|
||||
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.structure.object.IrisObjectScale;
|
||||
import art.arcane.iris.structure.object.IrisObjectTranslate;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.Vector3i;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import org.junit.After;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Answers;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class PlacementReachPackTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final int SPIN_PERIOD = 360;
|
||||
private static final int MAXIMUM_CHECKED_KERNELS = 4096;
|
||||
private static final String V2_HEADER = "Iris V2 IOB;";
|
||||
|
||||
private File pack;
|
||||
private IrisData data;
|
||||
private IrisDimension dimension;
|
||||
|
||||
@Before
|
||||
public void loadPack() throws IOException {
|
||||
String configured = System.getenv("IRIS_TEST_PACK");
|
||||
Assume.assumeTrue(configured != null && !configured.isBlank());
|
||||
pack = new File(configured).getAbsoluteFile();
|
||||
Assume.assumeTrue(new File(pack, "objects").isDirectory());
|
||||
File[] dimensions = new File(pack, "dimensions").listFiles((folder, name) -> name.endsWith(".json"));
|
||||
Assume.assumeTrue(dimensions != null && dimensions.length > 0);
|
||||
bindPlatform(Files.createTempDirectory("iris-placement-reach").toFile());
|
||||
data = IrisData.get(pack);
|
||||
String dimensionKey = System.getenv().getOrDefault("IRIS_TEST_PACK_DIMENSION",
|
||||
dimensions[0].getName().substring(0, dimensions[0].getName().length() - 5));
|
||||
dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
assertTrue("dimension " + dimensionKey + " must load", dimension != null);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisServices.remove(PreservationRegistry.class);
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyObjectFileStaysInsideItsReachUnderRightAngleRotations() throws IOException {
|
||||
List<IrisObjectPlacement> placements = List.of(
|
||||
new IrisObjectPlacement(),
|
||||
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp())),
|
||||
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D))),
|
||||
new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D)),
|
||||
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()))
|
||||
.setTranslate(new IrisObjectTranslate().setX(7).setZ(-3)));
|
||||
List<Path> files;
|
||||
try (Stream<Path> walk = Files.walk(new File(pack, "objects").toPath())) {
|
||||
files = walk.filter(path -> path.toString().endsWith(".iob")).sorted().toList();
|
||||
}
|
||||
assertTrue(!files.isEmpty());
|
||||
Statistics statistics = new Statistics();
|
||||
NativeBlockState marker = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||
for (Path file : files) {
|
||||
IrisObject object = readPositions(file, marker);
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
verifyCentered(file.toString(), object, placement, PlacementReach.of(object, placement, 1D), statistics);
|
||||
}
|
||||
}
|
||||
System.out.println("PLACEMENT_REACH_FILES objects=" + files.size() + " " + statistics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyPackPlacementStaysInsideItsReach() {
|
||||
Statistics statistics = new Statistics();
|
||||
Set<IrisObjectPlacement> placements = new LinkedHashSet<>();
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
placements.addAll(region.getObjects());
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||
placements.addAll(biome.getObjects());
|
||||
}
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
for (String key : placement.getPlace()) {
|
||||
IrisObject object = data.getObjectLoader().load(key);
|
||||
if (object == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(dimension));
|
||||
forEachVariant(object, placement, 1D, variant -> verifyCentered(key, variant, placement, reach, statistics));
|
||||
}
|
||||
}
|
||||
for (IrisProceduralPlacement procedural : proceduralPlacements()) {
|
||||
KList<IrisObject> variants = procedural.getVariantObjects(data);
|
||||
if (variants == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObjectPlacement placement = procedural.asPlacement();
|
||||
for (IrisObject variant : variants) {
|
||||
PlacementReach reach = PlacementReach.of(variant, placement, placement.getMaximumScale(dimension));
|
||||
forEachVariant(variant, placement, 1D,
|
||||
scaled -> verifyCentered(variant.getLoadKey(), scaled, placement, reach, statistics));
|
||||
}
|
||||
}
|
||||
assertTrue(statistics.checked > 0);
|
||||
System.out.println("PLACEMENT_REACH_PLACEMENTS placements=" + placements.size() + " " + statistics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floatingPlacementsStayInsideTheirReach() {
|
||||
Statistics statistics = new Statistics();
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
|
||||
if (entries == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisFloatingChildBiomes entry : entries) {
|
||||
double shrink = entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D;
|
||||
IrisBiome target = entry.getRealBiome(biome, data);
|
||||
for (IrisObjectPlacement placement : listOrEmpty(entry.getFloatingObjects())) {
|
||||
verifyFloating(placement, shrink, false, statistics);
|
||||
}
|
||||
List<IrisObjectPlacement> anchored = new ArrayList<>();
|
||||
anchored.addAll(listOrEmpty(entry.getExtraObjects()));
|
||||
anchored.addAll(listOrEmpty(entry.getTopObjectOverrides()));
|
||||
anchored.addAll(listOrEmpty(entry.getBottomObjectOverrides()));
|
||||
if (target != null) {
|
||||
anchored.addAll(listOrEmpty(entry.resolveTopObjects(target)));
|
||||
anchored.addAll(listOrEmpty(entry.resolveBottomObjects(target)));
|
||||
}
|
||||
for (IrisObjectPlacement placement : anchored) {
|
||||
verifyFloating(placement, shrink, true, statistics);
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("PLACEMENT_REACH_FLOATING " + statistics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reportsTheMantleWindow() {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
MantleObjectComponent objects = new MantleObjectComponent(engineMantle);
|
||||
MantleFloatingObjectComponent floating = new MantleFloatingObjectComponent(engineMantle);
|
||||
int objectRadius = objects.getRadius();
|
||||
int objectInput = objects.getInputRadius();
|
||||
int objectEager = objects.getEagerInputRadius(objectInput);
|
||||
int floatingRadius = floating.getRadius();
|
||||
int floatingPassRadius = chunkRadius(floatingRadius);
|
||||
int objectPassRadius = chunkRadius(floatingRadius);
|
||||
int terrainRadius = chunkRadius(floatingRadius + objectEager + 1);
|
||||
int writerRadius = 2 * chunkRadius(floatingRadius + objectInput + 1);
|
||||
System.out.println("PLACEMENT_REACH_WINDOW objectReach=" + objectRadius
|
||||
+ " objectInput=" + objectInput
|
||||
+ " objectEager=" + objectEager
|
||||
+ " sourceChunkRadius=" + MantleObjectComponent.sourceChunkRadius(objectRadius)
|
||||
+ " floatingReach=" + floatingRadius
|
||||
+ " floatingPassRadius=" + floatingPassRadius
|
||||
+ " objectPassRadius=" + objectPassRadius
|
||||
+ " terrainRadius=" + terrainRadius
|
||||
+ " terrainWindow=" + ((2 * terrainRadius) + 1) + "x" + ((2 * terrainRadius) + 1)
|
||||
+ " writerRadius=" + writerRadius);
|
||||
assertTrue(objectRadius > 0);
|
||||
}
|
||||
|
||||
private void verifyFloating(IrisObjectPlacement placement, double shrink, boolean anchored, Statistics statistics) {
|
||||
if (placement == null || placement.getPlace() == null) {
|
||||
return;
|
||||
}
|
||||
for (String key : placement.getPlace()) {
|
||||
IrisObject object = data.getObjectLoader().load(key);
|
||||
if (object == null) {
|
||||
continue;
|
||||
}
|
||||
PlacementReach.Extent extent = PlacementReach.Extent.of(object)
|
||||
.scaled(placement.getMaximumScale(dimension)).scaled(shrink);
|
||||
if (!anchored) {
|
||||
PlacementReach reach = PlacementReach.of(extent, placement);
|
||||
forEachVariant(object, placement, shrink, variant -> verifyCentered(key, variant, placement, reach, statistics));
|
||||
continue;
|
||||
}
|
||||
PlacementReach reach = PlacementReach.anchored(extent, placement);
|
||||
forEachVariant(object, placement, shrink, variant -> verifyAnchored(key, variant, reach, statistics));
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyAnchored(String key, IrisObject variant, PlacementReach reach, Statistics statistics) {
|
||||
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(variant);
|
||||
List<IrisBlockVector> positions = positions(variant);
|
||||
List<IrisObjectRotation> rotations = List.of(IrisObjectRotation.of(0D, 0D, 0D),
|
||||
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
|
||||
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D));
|
||||
for (IrisObjectRotation rotation : rotations) {
|
||||
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
|
||||
int actual = 0;
|
||||
for (IrisBlockVector position : positions) {
|
||||
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
|
||||
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()));
|
||||
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()));
|
||||
}
|
||||
record(key, actual, reach.horizontal(), statistics);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyCentered(String key, IrisObject object, IrisObjectPlacement placement, PlacementReach reach,
|
||||
Statistics statistics) {
|
||||
IrisObjectRotation rotation = placement.getRotation();
|
||||
IrisObjectTranslate translate = placement.getTranslate();
|
||||
boolean translating = translate != null && translate.canTranslate();
|
||||
int[] spinsX = spins(rotation, 0);
|
||||
int[] spinsY = spins(rotation, 1);
|
||||
int[] spinsZ = spins(rotation, 2);
|
||||
List<IrisBlockVector> positions = positions(object);
|
||||
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
|
||||
if (kernels > MAXIMUM_CHECKED_KERNELS) {
|
||||
fail("rotation for " + key + " has " + kernels + " kernels");
|
||||
}
|
||||
for (int spinX : spinsX) {
|
||||
for (int spinY : spinsY) {
|
||||
for (int spinZ : spinsZ) {
|
||||
IrisBlockVector offset = translating
|
||||
? rotation.rotate(new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ()), spinX, spinY, spinZ)
|
||||
: null;
|
||||
int actual = 0;
|
||||
for (IrisBlockVector position : positions) {
|
||||
IrisBlockVector moved = rotation.rotate(position, spinX, spinY, spinZ).clone();
|
||||
if (offset != null) {
|
||||
moved.add(offset);
|
||||
}
|
||||
actual = Math.max(actual, Math.abs((int) Math.round(moved.getX())));
|
||||
actual = Math.max(actual, Math.abs((int) Math.round(moved.getZ())));
|
||||
}
|
||||
record(key, actual, reach.horizontal(), statistics);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisObject readPositions(Path file, NativeBlockState marker) throws IOException {
|
||||
try (DataInputStream input = new DataInputStream(new BufferedInputStream(Files.newInputStream(file)))) {
|
||||
IrisObject object = new IrisObject(input.readInt(), input.readInt(), input.readInt());
|
||||
input.mark(Short.BYTES + V2_HEADER.length());
|
||||
int headerLength = input.readUnsignedShort();
|
||||
boolean v2 = headerLength == V2_HEADER.length()
|
||||
&& V2_HEADER.equals(new String(input.readNBytes(headerLength), StandardCharsets.US_ASCII));
|
||||
if (!v2) {
|
||||
input.reset();
|
||||
}
|
||||
int paletteSize = v2 ? input.readShort() : 0;
|
||||
for (int index = 0; index < paletteSize; index++) {
|
||||
input.readUTF();
|
||||
}
|
||||
int blocks = input.readInt();
|
||||
for (int index = 0; index < blocks; index++) {
|
||||
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
|
||||
if (v2) {
|
||||
input.readShort();
|
||||
} else {
|
||||
input.readUTF();
|
||||
}
|
||||
object.getBlocks().put(position, marker);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
private static void record(String key, int actual, int reach, Statistics statistics) {
|
||||
statistics.checked++;
|
||||
statistics.maximumActual = Math.max(statistics.maximumActual, actual);
|
||||
statistics.maximumReach = Math.max(statistics.maximumReach, reach);
|
||||
statistics.slack += reach - actual;
|
||||
if (actual > reach) {
|
||||
fail(key + " places a block " + actual + " blocks from its origin, beyond its reach " + reach);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<IrisBlockVector> positions(IrisObject object) {
|
||||
List<IrisBlockVector> positions = new ArrayList<>(object.getBlocks().size() + 8);
|
||||
object.getBlocks().forEach((position, state) -> positions.add(position));
|
||||
Vector3i center = object.getCenter();
|
||||
int[] xs = {-center.getBlockX(), object.getW() - center.getBlockX() - 1};
|
||||
int[] ys = {-center.getBlockY(), object.getH() - center.getBlockY() - 1};
|
||||
int[] zs = {-center.getBlockZ(), object.getD() - center.getBlockZ() - 1};
|
||||
for (int x : xs) {
|
||||
for (int y : ys) {
|
||||
for (int z : zs) {
|
||||
positions.add(new IrisBlockVector(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
private void forEachVariant(IrisObject object, IrisObjectPlacement placement, double shrink, Consumer<IrisObject> consumer) {
|
||||
IrisObjectScale scale = placement.getScale();
|
||||
IrisObjectPlacementScaleInterpolator interpolation = scale == null || scale.getInterpolation() == null
|
||||
? IrisObjectPlacementScaleInterpolator.NONE : scale.getInterpolation();
|
||||
for (double factor : scales(placement)) {
|
||||
IrisObject variant = factor == 1D && (scale == null || !scale.shouldScale())
|
||||
? object
|
||||
: scale == null ? IrisObjectScale.getFixed(object, factor) : object.scaled(factor, interpolation);
|
||||
consumer.accept(shrink < 1D ? variant.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE) : variant);
|
||||
}
|
||||
}
|
||||
|
||||
private double[] scales(IrisObjectPlacement placement) {
|
||||
IrisObjectScale scale = placement.getScale();
|
||||
if (scale == null) {
|
||||
return new double[]{dimension.getAllObjectScaleFactor()};
|
||||
}
|
||||
if (!scale.shouldScale()) {
|
||||
return new double[]{1D};
|
||||
}
|
||||
if (scale.getSize() != 1D) {
|
||||
return new double[]{scale.getSize()};
|
||||
}
|
||||
if (scale.getMinimumScale() == scale.getMaximumScale()) {
|
||||
return new double[]{scale.getMinimumScale()};
|
||||
}
|
||||
int count = Math.max(1, Math.min(scale.getVariations(), 32));
|
||||
double[] scales = new double[count];
|
||||
for (int index = 0; index < count; index++) {
|
||||
scales[index] = scale.getMinimumScale() + (((scale.getMaximumScale() - scale.getMinimumScale()) / count) * index);
|
||||
}
|
||||
return scales;
|
||||
}
|
||||
|
||||
private List<IrisProceduralPlacement> proceduralPlacements() {
|
||||
List<IrisProceduralPlacement> placements = new ArrayList<>();
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
addProcedural(placements, region.getProceduralObjects());
|
||||
}
|
||||
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||
addProcedural(placements, biome.getProceduralObjects());
|
||||
}
|
||||
return placements;
|
||||
}
|
||||
|
||||
private static void addProcedural(List<IrisProceduralPlacement> placements, IrisProceduralObjects procedural) {
|
||||
if (procedural != null && !procedural.isEmpty()) {
|
||||
placements.addAll(procedural.getAllPlacements());
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] spins(IrisObjectRotation rotation, int axis) {
|
||||
if (rotation == null || !rotation.canRotate()) {
|
||||
return new int[]{0};
|
||||
}
|
||||
boolean rotates = switch (axis) {
|
||||
case 0 -> rotation.canRotateX();
|
||||
case 1 -> rotation.canRotateY();
|
||||
default -> rotation.canRotateZ();
|
||||
};
|
||||
if (!rotates) {
|
||||
return new int[]{0};
|
||||
}
|
||||
LongOpenHashSet angles = new LongOpenHashSet();
|
||||
IntArrayList spins = new IntArrayList();
|
||||
for (int spin = -SPIN_PERIOD + 1; spin < SPIN_PERIOD; spin++) {
|
||||
double angle = switch (axis) {
|
||||
case 0 -> rotation.getXRotation(spin);
|
||||
case 1 -> rotation.getYRotation(spin);
|
||||
default -> rotation.getZRotation(spin);
|
||||
};
|
||||
if (angles.add(Double.doubleToLongBits(angle))) {
|
||||
spins.add(spin);
|
||||
}
|
||||
}
|
||||
return spins.toIntArray();
|
||||
}
|
||||
|
||||
private static <T> List<T> listOrEmpty(KList<T> list) {
|
||||
return list == null ? List.of() : list;
|
||||
}
|
||||
|
||||
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
|
||||
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
rotation.setEnabled(true);
|
||||
rotation.setXAxis(x);
|
||||
rotation.setYAxis(y);
|
||||
rotation.setZAxis(z);
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private static int chunkRadius(int blockRadius) {
|
||||
return blockRadius > 0 ? Math.ceilDiv(blockRadius, 16) : 0;
|
||||
}
|
||||
|
||||
private static void bindPlatform(File dataFolder) {
|
||||
IrisPlatforms.unbind();
|
||||
Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
|
||||
when(registries.block(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
|
||||
when(registries.air()).thenAnswer(invocation -> state(states, "minecraft:air"));
|
||||
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
|
||||
when(platform.dataFolder()).thenReturn(dataFolder);
|
||||
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
|
||||
Object[] path = invocation.getRawArguments();
|
||||
return new File(dataFolder, String.join(File.separator, (String[]) path[0]));
|
||||
});
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
IrisServices.register(PreservationRegistry.class, new IgnoredPreservation());
|
||||
}
|
||||
|
||||
private static NativeBlockState state(Map<String, NativeBlockState> states, String key) {
|
||||
return states.computeIfAbsent(key, ignored -> {
|
||||
String material = materialOf(key);
|
||||
boolean air = material.endsWith(":air") || material.endsWith("cave_air") || material.endsWith("void_air");
|
||||
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||
when(state.key()).thenReturn(key);
|
||||
when(state.materialKey()).thenReturn(material);
|
||||
when(state.isAir()).thenReturn(air);
|
||||
when(state.isSolid()).thenReturn(!air);
|
||||
when(state.isOccluding()).thenReturn(!air);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
private static String materialOf(String key) {
|
||||
String trimmed = key.trim().toLowerCase(Locale.ROOT);
|
||||
int properties = trimmed.indexOf('[');
|
||||
String head = properties < 0 ? trimmed : trimmed.substring(0, properties);
|
||||
return head.indexOf(':') < 0 ? "minecraft:" + head : head;
|
||||
}
|
||||
|
||||
private static final class IgnoredPreservation implements PreservationRegistry {
|
||||
@Override
|
||||
public void register(Thread thread) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ExecutorService service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCache(MeteredCache cache) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dereference() {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Statistics {
|
||||
private long checked;
|
||||
private long slack;
|
||||
private int maximumActual;
|
||||
private int maximumReach;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "checked=" + checked + " maxActual=" + maximumActual + " maxReach=" + maximumReach
|
||||
+ " meanSlack=" + (checked == 0 ? 0D : (double) slack / checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.structure.object.FloatingObjectFootprint;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.structure.object.IrisObjectScale;
|
||||
import art.arcane.iris.structure.object.IrisObjectTranslate;
|
||||
import art.arcane.iris.structure.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class PlacementReachTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final int ORIGIN_X = 1000;
|
||||
private static final int ORIGIN_Y = 90;
|
||||
private static final int ORIGIN_Z = -2000;
|
||||
private static final int GROUND_Y = 64;
|
||||
|
||||
private final Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||
private IrisData data;
|
||||
private IrisData objectData;
|
||||
private Engine engine;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
|
||||
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.air()).thenAnswer(invocation -> state("minecraft:air"));
|
||||
IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
|
||||
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
|
||||
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
|
||||
when(complex.getRockStream()).thenReturn(rock);
|
||||
engine = mock(Engine.class, withSettings().stubOnly());
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(engine.getMinHeight()).thenReturn(0);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
data = mock(IrisData.class, withSettings().stubOnly());
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
objectData = mock(IrisData.class, withSettings().stubOnly());
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void halfExtentReachIsTheCenteredFrameNotTheFullWidth() {
|
||||
IrisObject object = singleBlockObject(103, 75, 73);
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
|
||||
assertEquals(51, PlacementReach.of(object, placement, 1D).horizontal());
|
||||
assertEquals(54, PlacementReach.of(object, placement, 1D).input());
|
||||
assertEquals(51, PlacementReach.of(object, placement.setRotation(IrisObjectRotation.of(0D, 0D, 0D)), 1D).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arbitraryAngleRotationUsesTheRotatedFrame() {
|
||||
IrisObject object = singleBlockObject(21, 5, 21);
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()));
|
||||
|
||||
assertEquals(14, PlacementReach.of(object, placement, 1D).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void slopeRotationIsBoundedEvenWhenTheConfiguredRotationIsDisabled() {
|
||||
IrisObject object = singleBlockObject(21, 9, 21);
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D));
|
||||
|
||||
assertEquals(10, PlacementReach.of(object, placement, 1D).horizontal());
|
||||
assertEquals(15, PlacementReach.of(object, placement.setRotateTowardsSlope(true), 1D).horizontal());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unionKeepsTheLargestReachAndTheLargestInput() {
|
||||
PlacementReach union = new PlacementReach(10, 2).union(new PlacementReach(4, 12));
|
||||
|
||||
assertEquals(10, union.horizontal());
|
||||
assertEquals(16, union.input());
|
||||
assertEquals(19, union.withReadMargin(9).input());
|
||||
assertEquals(16, union.withReadMargin(1).input());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scaledSpanBoundsEveryScaledVariant() {
|
||||
Random random = new Random(91L);
|
||||
double[] scales = {0.01D, 0.25D, 0.45D, 0.5D, 0.75D, 0.95D, 1D, 1.3D, 1.5D, 2D, 2.6D, 3.7D, 4D};
|
||||
for (int trial = 0; trial < 120; trial++) {
|
||||
int outside = trial % 3 == 0 ? 1 + random.nextInt(3) : 0;
|
||||
IrisObject object = randomObject(random, outside);
|
||||
PlacementReach.Extent extent = PlacementReach.Extent.of(object);
|
||||
for (double scale : scales) {
|
||||
for (IrisObjectPlacementScaleInterpolator interpolation : List.of(IrisObjectPlacementScaleInterpolator.NONE,
|
||||
IrisObjectPlacementScaleInterpolator.TRILINEAR)) {
|
||||
if (interpolation != IrisObjectPlacementScaleInterpolator.NONE && scale > 2D) {
|
||||
continue;
|
||||
}
|
||||
IrisObject scaled = object.scaled(scale, interpolation);
|
||||
for (double maximum : new double[]{scale, Math.min(IrisObjectScale.MAXIMUM_FACTOR, scale * 1.4D)}) {
|
||||
assertContains(extent.scaled(maximum), scaled, "scale " + scale + " bounded at " + maximum);
|
||||
for (double shrink : new double[]{0.45D, 0.8D}) {
|
||||
assertContains(extent.scaled(maximum).scaled(shrink),
|
||||
scaled.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE),
|
||||
"scale " + scale + " shrink " + shrink);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runnerWritesAndReadsStayInsideTheReach() {
|
||||
Random random = new Random(1337L);
|
||||
for (int trial = 0; trial < 260; trial++) {
|
||||
IrisObject object = randomObject(random, trial % 5 == 0 ? 2 : 0);
|
||||
object.setLoader(objectData);
|
||||
IrisObjectPlacement placement = randomPlacement(random);
|
||||
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(null));
|
||||
for (int seed = 0; seed < 12; seed++) {
|
||||
RNG rng = new RNG((trial * 7919L) + seed);
|
||||
IrisObject variant = placement.getScale() == null ? object : placement.getScale().get(rng, object);
|
||||
RecordingPlacer placer = new RecordingPlacer(engine);
|
||||
variant.place(ORIGIN_X, ORIGIN_Y, ORIGIN_Z, placer, placement, rng, null, null, data);
|
||||
String context = "trial " + trial + " seed " + seed + " mode " + placement.getMode()
|
||||
+ " rotation " + placement.getRotation() + " translate " + placement.getTranslate()
|
||||
+ " scale " + placement.getScale() + " warp " + placement.getWarp().getMultiplier();
|
||||
assertTrue(context + " writes " + placer.writeReach + " > " + reach.horizontal(),
|
||||
placer.writeReach <= reach.horizontal());
|
||||
assertTrue(context + " reads " + placer.readReach + " > " + reach.input(),
|
||||
placer.readReach <= reach.input());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anchoredReachCoversTheFloatingFootprintAnchor() {
|
||||
Random random = new Random(7L);
|
||||
for (int trial = 0; trial < 80; trial++) {
|
||||
IrisObject object = randomObject(random, trial % 4 == 0 ? 1 : 0);
|
||||
PlacementReach reach = PlacementReach.anchored(PlacementReach.Extent.of(object), new IrisObjectPlacement());
|
||||
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(object);
|
||||
for (IrisObjectRotation rotation : List.of(IrisObjectRotation.of(0D, 0D, 0D),
|
||||
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
|
||||
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D))) {
|
||||
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
|
||||
object.getBlocks().forEach((position, state) -> {
|
||||
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
|
||||
assertTrue(Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()) <= reach.horizontal());
|
||||
assertTrue(Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()) <= reach.horizontal());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IrisObjectPlacement randomPlacement(Random random) {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setRotation(randomRotation(random));
|
||||
if (random.nextInt(3) > 0) {
|
||||
placement.setTranslate(new IrisObjectTranslate()
|
||||
.setX(random.nextInt(41) - 20)
|
||||
.setY(random.nextInt(21) - 10)
|
||||
.setZ(random.nextInt(41) - 20));
|
||||
}
|
||||
if (random.nextInt(3) == 0) {
|
||||
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(9)));
|
||||
}
|
||||
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
|
||||
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT, ObjectPlaceMode.CEILING_HANG,
|
||||
ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_HIGH, ObjectPlaceMode.VACUUM_FAST, ObjectPlaceMode.VACUUM_ORGANIC,
|
||||
ObjectPlaceMode.VACUUM_WAVY, ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
placement.setMode(modes.get(random.nextInt(modes.size())));
|
||||
if (random.nextInt(4) == 0) {
|
||||
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(16)));
|
||||
}
|
||||
if (random.nextInt(4) == 0) {
|
||||
placement.setStiltSettings(new IrisStiltSettings());
|
||||
}
|
||||
placement.setBore(random.nextInt(4) == 0);
|
||||
placement.setBoreExtendMaxY(random.nextInt(3));
|
||||
placement.setSnow(random.nextInt(4) == 0 ? 0.6D : 0D);
|
||||
placement.setSurfaceSupportBuffer(random.nextInt(5));
|
||||
switch (random.nextInt(5)) {
|
||||
case 0 -> placement.setScale(new IrisObjectScale().setSize(0.3D + (random.nextDouble() * 1.6D)));
|
||||
case 1 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.5D).setMaximumScale(2.3D)
|
||||
.setVariations(5).setInterpolation(IrisObjectPlacementScaleInterpolator.TRILINEAR));
|
||||
case 2 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.4D).setMaximumScale(0.9D).setVariations(3));
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
return placement;
|
||||
}
|
||||
|
||||
private static IrisObjectRotation randomRotation(Random random) {
|
||||
return switch (random.nextInt(9)) {
|
||||
case 0 -> new IrisObjectRotation();
|
||||
case 1 -> IrisObjectRotation.of(0D, 0D, 0D);
|
||||
case 2 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp());
|
||||
case 3 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp());
|
||||
case 4 -> IrisObjectRotation.of(90D, 0D, 0D);
|
||||
case 5 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
|
||||
case 6 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 0D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp(true, false, 0D, 0D, 30D));
|
||||
case 7 -> IrisObjectRotation.of(30D, 135D, 0D);
|
||||
default -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 20D, 110D, 15D),
|
||||
new IrisAxisRotationClamp(true, true, 180D, 180D, 0D));
|
||||
};
|
||||
}
|
||||
|
||||
private IrisObject randomObject(Random random, int outside) {
|
||||
int width = 1 + random.nextInt(18);
|
||||
int height = 1 + random.nextInt(14);
|
||||
int depth = 1 + random.nextInt(18);
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 3));
|
||||
for (int index = 0; index < count; index++) {
|
||||
int x = random.nextInt(width) - (width / 2);
|
||||
int y = random.nextInt(height) - (height / 2);
|
||||
int z = random.nextInt(depth) - (depth / 2);
|
||||
object.getBlocks().put(new IrisBlockVector(x, y, z), state(random.nextInt(5) == 0 ? "minecraft:oak_leaves" : "minecraft:stone"));
|
||||
}
|
||||
for (int index = 0; index < outside; index++) {
|
||||
int x = random.nextInt(width + 10) - ((width + 10) / 2);
|
||||
int y = random.nextInt(height + 6) - ((height + 6) / 2);
|
||||
int z = random.nextInt(depth + 10) - ((depth + 10) / 2);
|
||||
object.getBlocks().put(new IrisBlockVector(x, y, z), state("minecraft:stone"));
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
private IrisObject singleBlockObject(int width, int height, int depth) {
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
object.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:stone"));
|
||||
return object;
|
||||
}
|
||||
|
||||
private static void assertContains(PlacementReach.Extent extent, IrisObject object, String context) {
|
||||
object.getBlocks().forEach((position, state) -> {
|
||||
assertTrue(context + " x " + position.getBlockX() + " outside " + extent.x(),
|
||||
position.getBlockX() >= extent.x().minimum() && position.getBlockX() <= extent.x().maximum());
|
||||
assertTrue(context + " y " + position.getBlockY() + " outside " + extent.y(),
|
||||
position.getBlockY() >= extent.y().minimum() && position.getBlockY() <= extent.y().maximum());
|
||||
assertTrue(context + " z " + position.getBlockZ() + " outside " + extent.z(),
|
||||
position.getBlockZ() >= extent.z().minimum() && position.getBlockZ() <= extent.z().maximum());
|
||||
});
|
||||
int centerX = object.getCenter().getBlockX();
|
||||
int centerZ = object.getCenter().getBlockZ();
|
||||
assertTrue(context + " frame x", -centerX >= extent.x().minimum() && object.getW() - 1 - centerX <= extent.x().maximum());
|
||||
assertTrue(context + " frame z", -centerZ >= extent.z().minimum() && object.getD() - 1 - centerZ <= extent.z().maximum());
|
||||
}
|
||||
|
||||
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
|
||||
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
rotation.setEnabled(true);
|
||||
rotation.setXAxis(x);
|
||||
rotation.setYAxis(y);
|
||||
rotation.setZAxis(z);
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private NativeBlockState state(String key) {
|
||||
return states.computeIfAbsent(key, ignored -> {
|
||||
String material = key.toLowerCase(Locale.ROOT);
|
||||
boolean air = material.endsWith("air");
|
||||
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
|
||||
when(state.key()).thenReturn(key);
|
||||
when(state.materialKey()).thenReturn(material);
|
||||
when(state.isAir()).thenReturn(air);
|
||||
when(state.isSolid()).thenReturn(!air);
|
||||
when(state.isOccluding()).thenReturn(!air);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
private static final class RecordingPlacer implements IObjectPlacer {
|
||||
private final Engine engine;
|
||||
private int writeReach;
|
||||
private int readReach;
|
||||
|
||||
private RecordingPlacer(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
private void write(int x, int z) {
|
||||
writeReach = Math.max(writeReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
|
||||
read(x, z);
|
||||
}
|
||||
|
||||
private void read(int x, int z) {
|
||||
readReach = Math.max(readReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
read(x, z);
|
||||
return GROUND_Y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
read(x, z);
|
||||
return GROUND_Y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, NativeBlockState d) {
|
||||
write(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
read(x, z);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
read(x, z);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
read(x, z);
|
||||
return y < GROUND_Y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
read(x, z);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return GROUND_Y - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
write(xx, zz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int xx, int yy, int zz, T data) {
|
||||
write(xx, zz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
|
||||
read(xx, zz);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class EngineMetricsStageTotalsTest {
|
||||
@Test
|
||||
public void stageTotalsAccumulatePastTheRollingWindow() {
|
||||
EngineMetrics metrics = new EngineMetrics(2);
|
||||
metrics.getTerrain().put(1.5D);
|
||||
metrics.getTerrain().put(2.5D);
|
||||
metrics.getTerrain().put(4D);
|
||||
|
||||
EngineMetrics.StageTotal terrain = metrics.stageTotals().get("terrain");
|
||||
|
||||
assertEquals(8D, terrain.millis(), 0D);
|
||||
assertEquals(3L, terrain.samples());
|
||||
assertEquals(3.25D, metrics.getTerrain().getAverage(), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stageTotalsSinceAnEarlierSnapshotCoverOnlyTheLaterPuts() {
|
||||
EngineMetrics metrics = new EngineMetrics(4);
|
||||
metrics.getCarveResolve().put(3D);
|
||||
EngineMetrics.StageTotal before = metrics.stageTotals().get("carve.resolve");
|
||||
metrics.getCarveResolve().put(5D);
|
||||
metrics.getCarveResolve().put(7D);
|
||||
|
||||
EngineMetrics.StageTotal window = metrics.stageTotals().get("carve.resolve").since(before);
|
||||
|
||||
assertEquals(12D, window.millis(), 0D);
|
||||
assertEquals(2L, window.samples());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stageTotalsNameEveryPulledTimer() {
|
||||
EngineMetrics metrics = new EngineMetrics(1);
|
||||
Map<String, EngineMetrics.StageTotal> totals = metrics.stageTotals();
|
||||
|
||||
assertEquals(Set.copyOf(metrics.pull().keySet()), Set.copyOf(totals.keySet()));
|
||||
assertEquals(metrics.pull().size(), totals.size());
|
||||
}
|
||||
}
|
||||
+15
-43
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.GenerationRuntime.BiomeMaxes;
|
||||
import art.arcane.iris.world.history.SavedBiomeRuntime;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.testsupport.Await;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -37,13 +36,11 @@ import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
@@ -680,30 +677,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingRouterReadWaitsForAttachmentPublication() throws Exception {
|
||||
public void missingRouterReadsDoNotWaitForTheAttachmentMonitor() throws Exception {
|
||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||
GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
|
||||
when(router.openCoordinateScope(-17, 32)).thenReturn(scope);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
|
||||
started.countDown();
|
||||
return engine.openGenerationHistoryCoordinateScope(-17, 32);
|
||||
});
|
||||
Thread reader = new Thread(read, "Pending history attachment");
|
||||
|
||||
ExecutorService reader = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
synchronized (engine.generationHistoryRuntimeRouterLock) {
|
||||
reader.start();
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
awaitBlocked(reader);
|
||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||
assertNull(reader.submit(() -> engine.openGenerationHistoryCoordinateScope(-17, 32))
|
||||
.get(5, TimeUnit.SECONDS));
|
||||
assertTrue(reader.submit(engine::getGenerationHistoryRuntimeRouter).get(5, TimeUnit.SECONDS).isEmpty());
|
||||
}
|
||||
assertSame(scope, read.get(5, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
reader.join(5_000L);
|
||||
assertTrue("Pending router read did not finish", !reader.isAlive());
|
||||
reader.shutdownNow();
|
||||
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,27 +699,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
|
||||
started.countDown();
|
||||
return engine.openGenerationHistoryCoordinateScope(-17, 32);
|
||||
});
|
||||
Thread reader = new Thread(read, "Detached history read");
|
||||
|
||||
engine.detachGenerationHistoryRuntimeRouter(router);
|
||||
ExecutorService reader = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
synchronized (engine.generationHistoryRuntimeRouterLock) {
|
||||
engine.detachGenerationHistoryRuntimeRouter(router);
|
||||
reader.start();
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
awaitBlocked(reader);
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> reader.submit(
|
||||
() -> engine.openGenerationHistoryCoordinateScope(-17, 32)).get(5, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
|
||||
}
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> read.get(5, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
|
||||
assertTrue(engine.getGenerationHistoryRuntimeRouter().isEmpty());
|
||||
} finally {
|
||||
reader.join(5_000L);
|
||||
assertTrue("Detached router read did not finish", !reader.isAlive());
|
||||
reader.shutdownNow();
|
||||
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
|
||||
}
|
||||
@@ -909,12 +887,6 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
return router;
|
||||
}
|
||||
|
||||
private static void awaitBlocked(Thread thread) {
|
||||
Await.reached("the reader to block on the engine lifecycle lock", Duration.ofSeconds(5L),
|
||||
() -> thread.getState() == Thread.State.BLOCKED || !thread.isAlive());
|
||||
assertEquals(Thread.State.BLOCKED, thread.getState());
|
||||
}
|
||||
|
||||
private static IrisEngine engine(
|
||||
GenerationRuntime generationRuntime,
|
||||
EngineEffects effects,
|
||||
|
||||
+3
@@ -132,6 +132,9 @@ public class IrisCarveModifierBiomeBlendTest {
|
||||
Field field = IrisCarveModifier.class.getDeclaredField("rng");
|
||||
field.setAccessible(true);
|
||||
field.set(modifier, rng);
|
||||
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
|
||||
seed.setAccessible(true);
|
||||
seed.setLong(modifier, SEED);
|
||||
return modifier;
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -56,6 +56,9 @@ public class IrisCarveModifierBoundarySupportTest {
|
||||
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
|
||||
rng.setAccessible(true);
|
||||
rng.set(modifier, new RNG(71L));
|
||||
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
|
||||
seed.setAccessible(true);
|
||||
seed.setLong(modifier, 71L);
|
||||
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
Matter floorMatter = new IrisMatter(16, 16, 16);
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IrisCarveModifierCaveBlendRollTest {
|
||||
private static final int SAMPLES = 1_000_000;
|
||||
|
||||
@Test
|
||||
public void statelessBlendRandomMatchesParallelRngForOneMillionPoints() {
|
||||
SplittableRandom random = new SplittableRandom(0x5eed_cafe_f00dL);
|
||||
long[] seeds = {0L, 1L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE, 0x5DEECE66DL, random.nextLong()};
|
||||
for (int sample = 0; sample < SAMPLES; sample++) {
|
||||
long seed = sample < seeds.length * 64 ? seeds[sample % seeds.length] : random.nextLong();
|
||||
int x = coordinate(random, sample);
|
||||
int y = sample % 7 == 0 ? random.nextInt() : random.nextInt(-2048, 2048);
|
||||
int z = coordinate(random, sample + 3);
|
||||
assertEquals(seed + " @ " + x + "," + y + "," + z,
|
||||
new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
|
||||
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statelessBlendRandomMatchesParallelRngAtCoordinateExtremes() {
|
||||
int[] edges = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, -33_554_433, -33_554_432, -1, 0, 1,
|
||||
33_554_431, 33_554_432, Integer.MAX_VALUE - 1, Integer.MAX_VALUE};
|
||||
long[] seeds = {0L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE};
|
||||
for (long seed : seeds) {
|
||||
for (int x : edges) {
|
||||
for (int y : edges) {
|
||||
for (int z : edges) {
|
||||
assertEquals(new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
|
||||
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int coordinate(SplittableRandom random, int sample) {
|
||||
return switch (sample % 4) {
|
||||
case 0 -> random.nextInt();
|
||||
case 1 -> random.nextInt(-30_000_000, 30_000_000);
|
||||
default -> random.nextInt(-65_536, 65_536);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -49,6 +50,7 @@ public class IrisCarveScratchTest {
|
||||
scratch.boundaryMasks[0].add(13);
|
||||
scratch.walls.put(1, 12, 2, cavern);
|
||||
scratch.customBiomeCache.put("cave", null);
|
||||
scratch.caveBiomeCache.put(42L, new IrisBiome());
|
||||
scratch.customCaveBiomePresent = true;
|
||||
|
||||
scratch.reset();
|
||||
@@ -56,6 +58,7 @@ public class IrisCarveScratchTest {
|
||||
assertTrue(scratch.columnMasks[0].isEmpty());
|
||||
assertTrue(scratch.boundaryMasks[0].isEmpty());
|
||||
assertTrue(scratch.customBiomeCache.isEmpty());
|
||||
assertTrue(scratch.caveBiomeCache.isEmpty());
|
||||
assertFalse(scratch.customCaveBiomePresent);
|
||||
int[] wallCount = new int[1];
|
||||
scratch.walls.forEach((x, y, z, value) -> wallCount[0]++);
|
||||
|
||||
@@ -19,11 +19,10 @@ import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.generation.chunk.SectionBuffer;
|
||||
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
@@ -165,10 +164,7 @@ public class IrisNaturalCaveScanTest {
|
||||
fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key())), null);
|
||||
|
||||
Fixture bounded = new Fixture(true);
|
||||
ChunkGenerator.ChunkData chunkData = mock(ChunkGenerator.ChunkData.class);
|
||||
doReturn(0).when(chunkData).getMinHeight();
|
||||
doReturn(32).when(chunkData).getMaxHeight();
|
||||
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunkData);
|
||||
SectionBuffer holder = new SectionBuffer(32);
|
||||
bounded.sparseOutput(holder);
|
||||
List<String> boundedWrites = new ArrayList<>();
|
||||
bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder,
|
||||
|
||||
+3
-11
@@ -1,7 +1,7 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.generation.chunk.SectionBuffer;
|
||||
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
|
||||
import art.arcane.iris.generation.concurrent.BurstExecutor;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
@@ -15,7 +15,6 @@ 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.bukkit.generator.ChunkGenerator;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -65,8 +64,8 @@ public class IrisPerfectionModifierColumnTest {
|
||||
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++) {
|
||||
ChunkDataHunkHolder bounded = holder(64);
|
||||
ChunkDataHunkHolder full = holder(64);
|
||||
SectionBuffer bounded = new SectionBuffer(64);
|
||||
SectionBuffer full = new SectionBuffer(64);
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int top = random.nextInt(64);
|
||||
@@ -91,13 +90,6 @@ public class IrisPerfectionModifierColumnTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static ChunkDataHunkHolder holder(int height) {
|
||||
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
|
||||
when(chunk.getMinHeight()).thenReturn(0);
|
||||
when(chunk.getMaxHeight()).thenReturn(height);
|
||||
return new ChunkDataHunkHolder(chunk);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedTallColumnsAreScannedOnceDuringRepeatedCleanup() {
|
||||
Fixture fixture = new Fixture();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.subterrain;
|
||||
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.junit.Test;
|
||||
@@ -10,6 +11,11 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -441,6 +447,74 @@ public class SubterrainPlannerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void alternatingChunkSamplingResolvesEachCandidateCellOnce() {
|
||||
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setAllowedRegions(new KList<>("test-region"));
|
||||
AtomicInteger regionQueries = new AtomicInteger();
|
||||
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97,
|
||||
(x, z) -> {
|
||||
regionQueries.incrementAndGet();
|
||||
return "test-region";
|
||||
}));
|
||||
for (int round = 0; round < 500; round++) {
|
||||
planner.sample(7, 10, 7);
|
||||
planner.sample(23, 10, 7);
|
||||
planner.sample(7, 10, 23);
|
||||
planner.sample(-9, 10, -9);
|
||||
}
|
||||
int firstRound = regionQueries.get();
|
||||
assertTrue("candidate cells resolved " + firstRound + " times", firstRound <= 16);
|
||||
for (int round = 0; round < 500; round++) {
|
||||
planner.plansForBounds(-1, -1, 16, 16);
|
||||
planner.plansForBounds(15, 15, 32, 32);
|
||||
}
|
||||
assertEquals(firstRound, regionQueries.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentCrossChunkSamplingMatchesAFreshPlanner() throws Exception {
|
||||
List<IrisSubterrainFeature> features = List.of(compactFeature(IrisSubterrainFamily.CENOTE).setSpacing(96),
|
||||
compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setSpacing(160));
|
||||
SubterrainPlanner shared = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
|
||||
SubterrainPlanner reference = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
|
||||
int threads = 8;
|
||||
ExecutorService pool = Executors.newFixedThreadPool(threads);
|
||||
try {
|
||||
List<Future<Integer>> results = new ArrayList<>();
|
||||
for (int thread = 0; thread < threads; thread++) {
|
||||
int offset = thread;
|
||||
results.add(pool.submit(() -> {
|
||||
int occupied = 0;
|
||||
for (int step = 0; step < 6000; step++) {
|
||||
int x = Math.floorMod(step * 37 + offset * 101, 2400) - 1200;
|
||||
int z = Math.floorMod(step * 53 + offset * 211, 2400) - 1200;
|
||||
int y = Math.floorMod(step * 11 + offset, 150) - 60;
|
||||
int chunkX = Math.floorDiv(x, 16) * 16;
|
||||
int chunkZ = Math.floorDiv(z, 16) * 16;
|
||||
SubterrainCell expected;
|
||||
synchronized (reference) {
|
||||
expected = reference.sample(reference.plansForBounds(chunkX - 1, chunkZ - 1, chunkX + 16, chunkZ + 16), x, y, z);
|
||||
}
|
||||
SubterrainCell actual = shared.sample(x, y, z);
|
||||
assertEquals(expected, actual);
|
||||
if (actual.occupied()) {
|
||||
occupied++;
|
||||
}
|
||||
}
|
||||
return occupied;
|
||||
}));
|
||||
}
|
||||
int occupied = 0;
|
||||
for (Future<Integer> result : results) {
|
||||
occupied += result.get(60, TimeUnit.SECONDS);
|
||||
}
|
||||
assertTrue(occupied > 0);
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void immutableSnapshotsAndSeedsControlPlans() {
|
||||
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package art.arcane.iris.pack.loading;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.testsupport.DurabilityMode;
|
||||
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisDataCustomBiomeKeyTest {
|
||||
@ClassRule
|
||||
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void repeatedConcurrentLookupsMatchTheContentAddressedKeyPerDimension() throws Exception {
|
||||
Path pack = temporaryFolder.newFolder("custom-keys").toPath().resolve("iris/pack");
|
||||
writePack(pack);
|
||||
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
|
||||
ExecutorService pool = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
IrisDimension overworld = data.getDimensionLoader().load("overworld");
|
||||
IrisDimension second = data.getDimensionLoader().load("second");
|
||||
List<IrisBiomeCustom> customs = data.getBiomeLoader().load("test").getCustomDerivitives();
|
||||
assertEquals(2, customs.size());
|
||||
List<String> expected = new ArrayList<>();
|
||||
for (IrisDimension dimension : List.of(overworld, second)) {
|
||||
for (IrisBiomeCustom custom : customs) {
|
||||
expected.add(GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||
dimension.getLoadKey(), custom, data.getContentGate()));
|
||||
}
|
||||
}
|
||||
assertEquals(4, expected.stream().distinct().count());
|
||||
List<Future<List<String>>> results = new ArrayList<>();
|
||||
for (int thread = 0; thread < 8; thread++) {
|
||||
results.add(pool.submit(() -> {
|
||||
List<String> seen = new ArrayList<>();
|
||||
for (int round = 0; round < 200; round++) {
|
||||
seen.clear();
|
||||
for (IrisDimension dimension : List.of(overworld, second)) {
|
||||
for (IrisBiomeCustom custom : customs) {
|
||||
seen.add(data.customBiomeResourceKey(dimension, custom));
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(seen);
|
||||
}));
|
||||
}
|
||||
for (Future<List<String>> result : results) {
|
||||
assertEquals(expected, result.get(60, TimeUnit.SECONDS));
|
||||
}
|
||||
IrisBiomeCustom equalCopy = new IrisBiomeCustom().setId(customs.getFirst().getId());
|
||||
assertEquals(GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||
overworld.getLoadKey(), equalCopy, data.getContentGate()),
|
||||
data.customBiomeResourceKey(overworld, equalCopy));
|
||||
assertNotEquals(data.customBiomeResourceKey(overworld, customs.getFirst()),
|
||||
data.customBiomeResourceKey(second, customs.getFirst()));
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static void writePack(Path root) throws Exception {
|
||||
Files.createDirectories(root.resolve("dimensions"));
|
||||
Files.createDirectories(root.resolve("biomes"));
|
||||
for (String dimension : List.of("overworld", "second")) {
|
||||
Files.writeString(root.resolve("dimensions/" + dimension + ".json"), """
|
||||
{
|
||||
"name": "Test Dimension",
|
||||
"environment": "NORMAL",
|
||||
"logicalHeight": 256,
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
}
|
||||
}
|
||||
""", StandardCharsets.UTF_8);
|
||||
}
|
||||
Files.writeString(root.resolve("biomes/test.json"), """
|
||||
{
|
||||
"name": "Test Biome",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"customDerivitives": [
|
||||
{
|
||||
"id": "first_custom"
|
||||
},
|
||||
{
|
||||
"id": "second_custom",
|
||||
"temperature": 1.5
|
||||
}
|
||||
]
|
||||
}
|
||||
""", StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public class PregenPerformanceProfileTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenerationWantsTwoBurstWorkersPerCore() {
|
||||
public void pregenerationWantsTwoBurstWorkersPerPerformanceCore() {
|
||||
assertEquals(32, PregenPerformanceProfile.pregenBurstParallelism(16));
|
||||
assertEquals(4, PregenPerformanceProfile.pregenBurstParallelism(1));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user