mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Shared object placement layout
- Object placement bakes split into one per-object layout (palette, tiles, canopy) and per-variant offsets - Variants beyond the per-object limit are built transiently instead of clearing the variant map - High-variant golden and no-rebuild tests
This commit is contained in:
1 parent
d7cad729e4
commit
bf9e65252c
4 files changed
+244
-121
No files matched your search
@@ -146,6 +146,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
|
||||
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
|
||||
transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>();
|
||||
transient volatile ObjectPlacementBake.Layout placementLayout;
|
||||
@Getter
|
||||
@Setter
|
||||
private transient TreeGrounding treeGrounding;
|
||||
|
||||
@@ -15,140 +15,73 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
final class ObjectPlacementBake {
|
||||
private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32;
|
||||
|
||||
private final long blocksRevision;
|
||||
private final long statesRevision;
|
||||
private final TreeGrounding grounding;
|
||||
private final int count;
|
||||
private final Layout layout;
|
||||
private final Key key;
|
||||
private final int[] offsetX;
|
||||
private final int[] offsetY;
|
||||
private final int[] offsetZ;
|
||||
private final int[] layerY;
|
||||
private final short[] paletteIndexes;
|
||||
private final NativeBlockState[] palette;
|
||||
private final int[] tileBlocks;
|
||||
private final TileData[] tileValues;
|
||||
private final long[] canopy;
|
||||
private final Key key;
|
||||
private volatile double[] exact;
|
||||
|
||||
private ObjectPlacementBake(Key key, long blocksRevision, long statesRevision, TreeGrounding grounding, int count,
|
||||
int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes,
|
||||
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
|
||||
private ObjectPlacementBake(Layout layout, Key key, int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY) {
|
||||
this.layout = layout;
|
||||
this.key = key;
|
||||
this.blocksRevision = blocksRevision;
|
||||
this.statesRevision = statesRevision;
|
||||
this.grounding = grounding;
|
||||
this.count = count;
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
this.offsetZ = offsetZ;
|
||||
this.layerY = layerY;
|
||||
this.paletteIndexes = paletteIndexes;
|
||||
this.palette = palette;
|
||||
this.tileBlocks = tileBlocks;
|
||||
this.tileValues = tileValues;
|
||||
this.canopy = canopy;
|
||||
}
|
||||
|
||||
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||
Key key = Key.of(spin, ceilingHang, translateOffset);
|
||||
ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
|
||||
ObjectPlacementBake cached = variants.get(key);
|
||||
if (cached != null && cached.matches(object)) {
|
||||
return cached;
|
||||
}
|
||||
ObjectPlacementBake built = bake(object, key, spin, ceilingHang, translateOffset);
|
||||
if (built.matches(object)) {
|
||||
if (variants.size() >= MAXIMUM_VARIANTS_PER_OBJECT) {
|
||||
variants.clear();
|
||||
}
|
||||
variants.put(key, built);
|
||||
}
|
||||
return built;
|
||||
}
|
||||
|
||||
private static ObjectPlacementBake bake(IrisObject object, Key key, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
object.readLock.lock();
|
||||
try {
|
||||
VectorMap<NativeBlockState> blocks = object.blocks;
|
||||
VectorMap<TileData> states = object.states;
|
||||
long blocksRevision = blocks.modificationRevision();
|
||||
long statesRevision = states.modificationRevision();
|
||||
TreeGrounding grounding = object.getTreeGrounding();
|
||||
boolean hasStates = !states.isEmpty();
|
||||
IntArrayList x = new IntArrayList();
|
||||
IntArrayList y = new IntArrayList();
|
||||
IntArrayList z = new IntArrayList();
|
||||
IntArrayList layer = new IntArrayList();
|
||||
IntArrayList indexes = new IntArrayList();
|
||||
IntArrayList tileBlocks = new IntArrayList();
|
||||
List<TileData> tileValues = new ArrayList<>();
|
||||
List<NativeBlockState> palette = new ArrayList<>();
|
||||
IdentityHashMap<NativeBlockState, Integer> paletteSlots = new IdentityHashMap<>();
|
||||
IntArrayList canopyBlocks = new IntArrayList();
|
||||
boolean layerDiffers = false;
|
||||
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector source = cursor.key();
|
||||
NativeBlockState state = cursor.value();
|
||||
if (state == null) {
|
||||
state = IrisObject.States.air();
|
||||
}
|
||||
transform(source, rotated, spin, ceilingHang, translateOffset);
|
||||
int roundedY = (int) Math.round(rotated.getY());
|
||||
int floorY = rotated.getBlockY();
|
||||
x.add((int) Math.round(rotated.getX()));
|
||||
y.add(roundedY);
|
||||
z.add((int) Math.round(rotated.getZ()));
|
||||
layer.add(floorY);
|
||||
layerDiffers |= floorY != roundedY;
|
||||
Integer slot = paletteSlots.get(state);
|
||||
if (slot == null) {
|
||||
slot = palette.size();
|
||||
palette.add(state);
|
||||
paletteSlots.put(state, slot);
|
||||
}
|
||||
indexes.add(slot);
|
||||
if (hasStates) {
|
||||
TileData tile = states.get(source);
|
||||
if (tile != null) {
|
||||
tileBlocks.add(index);
|
||||
tileValues.add(tile);
|
||||
}
|
||||
}
|
||||
if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
|
||||
canopyBlocks.add(index);
|
||||
}
|
||||
index++;
|
||||
Layout layout = Layout.of(object);
|
||||
ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
|
||||
ObjectPlacementBake cached = variants.get(key);
|
||||
if (cached != null && cached.layout == layout) {
|
||||
return cached;
|
||||
}
|
||||
if (palette.size() > Short.MAX_VALUE) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than "
|
||||
+ Short.MAX_VALUE + " distinct block states");
|
||||
ObjectPlacementBake built = bake(object, layout, key, spin, ceilingHang, translateOffset);
|
||||
if (cached != null) {
|
||||
variants.replace(key, cached, built);
|
||||
} else if (variants.size() < MAXIMUM_VARIANTS_PER_OBJECT) {
|
||||
variants.putIfAbsent(key, built);
|
||||
}
|
||||
short[] paletteIndexes = new short[index];
|
||||
for (int block = 0; block < index; block++) {
|
||||
paletteIndexes[block] = (short) indexes.getInt(block);
|
||||
}
|
||||
long[] canopy = null;
|
||||
if (!canopyBlocks.isEmpty()) {
|
||||
canopy = new long[(index + 63) >>> 6];
|
||||
for (int block : canopyBlocks) {
|
||||
canopy[block >>> 6] |= 1L << (block & 63);
|
||||
}
|
||||
}
|
||||
return new ObjectPlacementBake(key, blocksRevision, statesRevision, grounding, index,
|
||||
x.toIntArray(), y.toIntArray(), z.toIntArray(), layerDiffers ? layer.toIntArray() : null,
|
||||
paletteIndexes, palette.toArray(new NativeBlockState[0]),
|
||||
tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(),
|
||||
tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy);
|
||||
return built;
|
||||
} finally {
|
||||
object.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectPlacementBake bake(IrisObject object, Layout layout, Key key, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
int count = layout.count;
|
||||
int[] x = new int[count];
|
||||
int[] y = new int[count];
|
||||
int[] z = new int[count];
|
||||
int[] layer = new int[count];
|
||||
boolean layerDiffers = false;
|
||||
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next() && index < count) {
|
||||
transform(cursor.key(), rotated, spin, ceilingHang, translateOffset);
|
||||
int roundedY = (int) Math.round(rotated.getY());
|
||||
int floorY = rotated.getBlockY();
|
||||
x[index] = (int) Math.round(rotated.getX());
|
||||
y[index] = roundedY;
|
||||
z[index] = (int) Math.round(rotated.getZ());
|
||||
layer[index] = floorY;
|
||||
layerDiffers |= floorY != roundedY;
|
||||
index++;
|
||||
}
|
||||
if (index != count) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was built");
|
||||
}
|
||||
return new ObjectPlacementBake(layout, key, x, y, z, layerDiffers ? layer : null);
|
||||
}
|
||||
|
||||
private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
target.setX(source.getX());
|
||||
@@ -163,14 +96,8 @@ final class ObjectPlacementBake {
|
||||
}
|
||||
}
|
||||
|
||||
boolean matches(IrisObject object) {
|
||||
return object.blocks.modificationRevision() == blocksRevision
|
||||
&& object.states.modificationRevision() == statesRevision
|
||||
&& object.getTreeGrounding() == grounding;
|
||||
}
|
||||
|
||||
int count() {
|
||||
return count;
|
||||
return layout.count;
|
||||
}
|
||||
|
||||
int offsetX(int index) {
|
||||
@@ -190,22 +117,23 @@ final class ObjectPlacementBake {
|
||||
}
|
||||
|
||||
int paletteIndex(int index) {
|
||||
return paletteIndexes[index];
|
||||
return layout.paletteIndexes[index];
|
||||
}
|
||||
|
||||
NativeBlockState[] palette() {
|
||||
return palette;
|
||||
return layout.palette;
|
||||
}
|
||||
|
||||
int[] tileBlocks() {
|
||||
return tileBlocks;
|
||||
return layout.tileBlocks;
|
||||
}
|
||||
|
||||
TileData[] tileValues() {
|
||||
return tileValues;
|
||||
return layout.tileValues;
|
||||
}
|
||||
|
||||
boolean canopy(int index) {
|
||||
long[] canopy = layout.canopy;
|
||||
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
|
||||
}
|
||||
|
||||
@@ -214,6 +142,7 @@ final class ObjectPlacementBake {
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
int count = layout.count;
|
||||
object.readLock.lock();
|
||||
try {
|
||||
resolved = new double[count * 3];
|
||||
@@ -237,6 +166,112 @@ final class ObjectPlacementBake {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
static final class Layout {
|
||||
private final long blocksRevision;
|
||||
private final long statesRevision;
|
||||
private final TreeGrounding grounding;
|
||||
private final int count;
|
||||
private final short[] paletteIndexes;
|
||||
private final NativeBlockState[] palette;
|
||||
private final int[] tileBlocks;
|
||||
private final TileData[] tileValues;
|
||||
private final long[] canopy;
|
||||
|
||||
private Layout(long blocksRevision, long statesRevision, TreeGrounding grounding, int count, short[] paletteIndexes,
|
||||
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
|
||||
this.blocksRevision = blocksRevision;
|
||||
this.statesRevision = statesRevision;
|
||||
this.grounding = grounding;
|
||||
this.count = count;
|
||||
this.paletteIndexes = paletteIndexes;
|
||||
this.palette = palette;
|
||||
this.tileBlocks = tileBlocks;
|
||||
this.tileValues = tileValues;
|
||||
this.canopy = canopy;
|
||||
}
|
||||
|
||||
private static Layout of(IrisObject object) {
|
||||
Layout current = object.placementLayout;
|
||||
if (current != null && current.matches(object)) {
|
||||
return current;
|
||||
}
|
||||
Layout built = build(object);
|
||||
if (!built.matches(object)) {
|
||||
return built;
|
||||
}
|
||||
object.placementLayout = built;
|
||||
if (current != null) {
|
||||
object.placementBakes.clear();
|
||||
}
|
||||
return built;
|
||||
}
|
||||
|
||||
private static Layout build(IrisObject object) {
|
||||
VectorMap<NativeBlockState> blocks = object.blocks;
|
||||
VectorMap<TileData> states = object.states;
|
||||
long blocksRevision = blocks.modificationRevision();
|
||||
long statesRevision = states.modificationRevision();
|
||||
TreeGrounding grounding = object.getTreeGrounding();
|
||||
boolean hasStates = !states.isEmpty();
|
||||
int size = blocks.size();
|
||||
short[] paletteIndexes = new short[size];
|
||||
IntArrayList tileBlocks = new IntArrayList();
|
||||
List<TileData> tileValues = new ArrayList<>();
|
||||
List<NativeBlockState> palette = new ArrayList<>();
|
||||
IdentityHashMap<NativeBlockState, Integer> paletteSlots = new IdentityHashMap<>();
|
||||
long[] canopy = null;
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector source = cursor.key();
|
||||
NativeBlockState state = cursor.value();
|
||||
if (state == null) {
|
||||
state = IrisObject.States.air();
|
||||
}
|
||||
Integer slot = paletteSlots.get(state);
|
||||
if (slot == null) {
|
||||
slot = palette.size();
|
||||
if (slot >= Short.MAX_VALUE) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than "
|
||||
+ Short.MAX_VALUE + " distinct block states");
|
||||
}
|
||||
palette.add(state);
|
||||
paletteSlots.put(state, slot);
|
||||
}
|
||||
if (index == paletteIndexes.length) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement layout was built");
|
||||
}
|
||||
paletteIndexes[index] = (short) slot.intValue();
|
||||
if (hasStates) {
|
||||
TileData tile = states.get(source);
|
||||
if (tile != null) {
|
||||
tileBlocks.add(index);
|
||||
tileValues.add(tile);
|
||||
}
|
||||
}
|
||||
if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
|
||||
if (canopy == null) {
|
||||
canopy = new long[(size + 63) >>> 6];
|
||||
}
|
||||
canopy[index >>> 6] |= 1L << (index & 63);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
if (index != size) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement layout was built");
|
||||
}
|
||||
return new Layout(blocksRevision, statesRevision, grounding, index, paletteIndexes,
|
||||
palette.toArray(new NativeBlockState[0]), tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(),
|
||||
tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy);
|
||||
}
|
||||
|
||||
private boolean matches(IrisObject object) {
|
||||
return object.blocks.modificationRevision() == blocksRevision
|
||||
&& object.states.modificationRevision() == statesRevision
|
||||
&& object.getTreeGrounding() == grounding;
|
||||
}
|
||||
}
|
||||
|
||||
record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin,
|
||||
long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY,
|
||||
long translateZ) {
|
||||
|
||||
@@ -4,9 +4,16 @@ import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.testsupport.KeyedBlockState;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ObjectPlacementBakeTest {
|
||||
@Test
|
||||
@@ -65,6 +72,57 @@ public class ObjectPlacementBakeTest {
|
||||
assertSame(after, ObjectPlacementBake.of(object, spin, false, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void highVariantPlacementsKeepCachedVariantsAndNeverRebuildTheLayout() {
|
||||
IrisObject object = object();
|
||||
IrisObjectRotation pine = new IrisObjectRotation();
|
||||
pine.setEnabled(true);
|
||||
pine.setXAxis(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
|
||||
pine.setYAxis(new IrisAxisRotationClamp(true, false, 0D, 270D, 90D));
|
||||
pine.setZAxis(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
|
||||
IrisBlockVector translate = new IrisBlockVector(0D, -6D, 0D);
|
||||
Random random = new Random(1444L);
|
||||
ObjectPlacementBake first = ObjectPlacementBake.of(object, new SpinKernel(pine, 0, 0, 0), false, translate);
|
||||
Map<ObjectPlacementBake.Key, ObjectPlacementBake> settled = null;
|
||||
Set<ObjectPlacementBake.Key> distinct = new HashSet<>();
|
||||
for (int placement = 0; placement < 4000; placement++) {
|
||||
SpinKernel spin = new SpinKernel(pine, random.nextInt(360), random.nextInt(360), random.nextInt(360));
|
||||
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, false, translate);
|
||||
distinct.add(ObjectPlacementBake.Key.of(spin, false, translate));
|
||||
assertSame(first.palette(), bake.palette());
|
||||
assertTrue(object.placementBakes.size() <= 32);
|
||||
assertTransformed(object, spin, translate, bake);
|
||||
if (placement == 2000) {
|
||||
settled = new HashMap<>(object.placementBakes);
|
||||
}
|
||||
}
|
||||
assertTrue(distinct.size() > 40);
|
||||
assertEquals(32, settled.size());
|
||||
assertEquals(settled.keySet(), object.placementBakes.keySet());
|
||||
for (Map.Entry<ObjectPlacementBake.Key, ObjectPlacementBake> entry : settled.entrySet()) {
|
||||
assertSame(entry.getValue(), object.placementBakes.get(entry.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertTransformed(IrisObject object, SpinKernel spin, IrisBlockVector translate, ObjectPlacementBake bake) {
|
||||
double[] exact = bake.exact(object, spin);
|
||||
int index = 0;
|
||||
for (IrisBlockVector source : object.getBlocks().keys()) {
|
||||
IrisBlockVector expected = source.clone();
|
||||
spin.rotate(expected);
|
||||
expected.add(translate);
|
||||
assertEquals((int) Math.round(expected.getX()), bake.offsetX(index));
|
||||
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
|
||||
assertEquals((int) Math.round(expected.getZ()), bake.offsetZ(index));
|
||||
assertEquals(expected.getBlockY(), bake.layerY(index));
|
||||
assertEquals(expected.getX(), exact[index * 3], 0D);
|
||||
assertEquals(expected.getY(), exact[index * 3 + 1], 0D);
|
||||
assertEquals(expected.getZ(), exact[index * 3 + 2], 0D);
|
||||
index++;
|
||||
}
|
||||
assertEquals(bake.count(), index);
|
||||
}
|
||||
|
||||
private static IrisObject object() {
|
||||
IrisObject object = new IrisObject(5, 5, 5);
|
||||
KeyedBlockState stone = new KeyedBlockState("minecraft:stone");
|
||||
|
||||
@@ -65,6 +65,7 @@ public class ObjectPlacementGoldenTest {
|
||||
private static final int SEEDS = 5;
|
||||
private static final long GOLDEN = 0x408f9d4113015897L;
|
||||
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
|
||||
private static final long HIGH_VARIANT_GOLDEN = 0xf017622340ecac42L;
|
||||
|
||||
private static final List<String> PALETTE = List.of(
|
||||
"minecraft:stone",
|
||||
@@ -229,6 +230,34 @@ public class ObjectPlacementGoldenTest {
|
||||
assertEquals(Long.toHexString(MARKER_GOLDEN), Long.toHexString(combined));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void highVariantPlacementsMatchTheRecordedBlockStream() {
|
||||
Random random = new Random(1444L);
|
||||
IrisObjectRotation pine = rotation(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 270D, 90D), new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
|
||||
long combined = 29L;
|
||||
int writes = 0;
|
||||
for (int index = 0; index < 16; index++) {
|
||||
IrisObject object = randomObject(random, index);
|
||||
object.setLoader(objectData);
|
||||
IrisObjectPlacement placement = randomPlacement(random).toPlacement("pine");
|
||||
placement.setRotation(pine);
|
||||
for (int seed = 0; seed < 150; seed++) {
|
||||
RNG rng = new RNG(index * 104_729L + seed);
|
||||
GoldenPlacer placer = new GoldenPlacer(engine, (seed & 1) == 0);
|
||||
int y = seed % 3 == 0 ? 90 + (seed & 7) : -1;
|
||||
int result = object.place(300 + seed, y, -400 + index * 7, placer, placement, rng, null, null, data);
|
||||
long hash = mix(placer.hash, result);
|
||||
hash = mix(hash, rng.nextLong());
|
||||
combined = mix(combined, hash);
|
||||
writes += placer.writes;
|
||||
}
|
||||
assertTrue(object.placementBakes.size() <= 32);
|
||||
}
|
||||
assertTrue(writes > 10_000);
|
||||
assertEquals(Long.toHexString(HIGH_VARIANT_GOLDEN), Long.toHexString(combined));
|
||||
}
|
||||
|
||||
private static long mix(long hash, long value) {
|
||||
long mixed = (hash ^ value) * 0x9E3779B97F4A7C15L;
|
||||
return mixed ^ (mixed >>> 29);
|
||||
|
||||
Reference in new issue
Block a user