mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 06:50:02 +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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user