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:
Brian Neumann-Fopiano committed 2026-10-22 04:00:16 -04:00
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);