Merged object placement improvements

- Native structure protection uses transformed object bounds
- Object copies share the volume mutation lock
- Upscaling writes expanded voxels without temporary lists
This commit is contained in:
Brian Neumann-Fopiano
2026-09-06 03:10:04 -04:00
6 changed files with 150 additions and 53 deletions
@@ -110,7 +110,7 @@ public class IrisObject extends IrisRegistrant {
this.d = d;
center = new Vector3i(w / 2, h / 2, d / 2);
shrinkOffset = new Vector3i(0, 0, 0);
var lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
readLock = lock.readLock();
writeLock = lock.writeLock();
}
@@ -135,17 +135,22 @@ public class IrisObject extends IrisRegistrant {
return aabb.aquire(() -> getAABBFor(new IrisBlockVector(w, h, d)));
}
public synchronized IrisObject copy() {
IrisObject o = new IrisObject(w, h, d);
o.setLoadKey(getLoadKey());
o.setLoader(getLoader());
o.setLoadFile(getLoadFile());
o.setCenter(getCenter().clone());
public IrisObject copy() {
readLock.lock();
try {
IrisObject copy = new IrisObject(w, h, d);
copy.setLoadKey(getLoadKey());
copy.setLoader(getLoader());
copy.setLoadFile(getLoadFile());
copy.setCenter(getCenter().clone());
blocks.forEach((i, v) -> o.blocks.put(i.clone(), v));
states.forEach((i, v) -> o.states.put(i.clone(), v.clone()));
blocks.forEach(copy.blocks::put);
states.forEach((position, tile) -> copy.states.put(position, tile.clone()));
return o;
return copy;
} finally {
readLock.unlock();
}
}
public void readLegacy(InputStream in) throws IOException {
@@ -350,7 +350,7 @@ final class IrisObjectPlacementRunner {
return -1;
}
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, translating, translateOffset, ceilingHang,
if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, placementBounds, translating, translateOffset, ceilingHang,
yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) {
return -1;
}
@@ -955,7 +955,7 @@ final class IrisObjectPlacementRunner {
* near this placement means no per block work at all. Only when the placement envelope meets a piece does the
* precise pass run, and the first solid block inside a piece rejects the whole object before any write.
*/
private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin,
private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin, TransformedBounds bounds,
boolean translating, IrisBlockVector translateOffset, boolean ceilingHang,
boolean paint, int warpMargin, int x, int y, int z) {
Engine engine = placer.getEngine();
@@ -963,23 +963,21 @@ final class IrisObjectPlacementRunner {
return false;
}
int margin = (Math.max(self.getW(), Math.max(self.getH(), self.getD())) / 2) + 1 + warpMargin;
if (translating) {
margin += Math.max(Math.abs(translateOffset.getBlockX()),
Math.max(Math.abs(translateOffset.getBlockY()), Math.abs(translateOffset.getBlockZ())));
}
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(x - margin, z - margin, x + margin, z + margin);
int minX = x + bounds.minX();
int maxX = x + bounds.maxX();
int minZ = z + bounds.minZ();
int maxZ = z + bounds.maxZ();
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(minX, minZ, maxX, maxZ);
if (volumes == null || volumes.isEmpty()) {
return false;
}
int worldY = y + engine.getMinHeight();
int envelopeMinY = paint ? Integer.MIN_VALUE : worldY - margin;
int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + margin;
int envelopeMinY = paint ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin;
int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin;
boolean envelopeMeetsPiece = false;
for (NativeStructureVolume volume : volumes) {
if (volume.intersects(x - margin, envelopeMinY, z - margin, x + margin, envelopeMaxY, z + margin)) {
if (volume.intersects(minX, envelopeMinY, minZ, maxX, envelopeMaxY, maxZ)) {
envelopeMeetsPiece = true;
break;
}
@@ -22,15 +22,12 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.VectorMap;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.math.IrisVector;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -289,23 +286,4 @@ final class IrisObjectShaping {
static String materialKey(PlatformBlockState state) {
return IrisProceduralBlocks.materialKey(state);
}
static List<IrisBlockVector> blocksBetweenTwoPoints(IrisVector loc1, IrisVector loc2) {
List<IrisBlockVector> locations = new ArrayList<>();
int topBlockX = Math.max(loc1.getBlockX(), loc2.getBlockX());
int bottomBlockX = Math.min(loc1.getBlockX(), loc2.getBlockX());
int topBlockY = Math.max(loc1.getBlockY(), loc2.getBlockY());
int bottomBlockY = Math.min(loc1.getBlockY(), loc2.getBlockY());
int topBlockZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ());
int bottomBlockZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ());
for (int x = bottomBlockX; x <= topBlockX; x++) {
for (int z = bottomBlockZ; z <= topBlockZ; z++) {
for (int y = bottomBlockY; y <= topBlockY; y++) {
locations.add(new IrisBlockVector(x, y, z));
}
}
}
return locations;
}
}
@@ -96,8 +96,6 @@ final class IrisObjectTransforms {
scale = scale - 0.0001;
}
IrisPosition l1 = self.getAABB().max();
IrisPosition l2 = self.getAABB().min();
VectorMap<PlatformBlockState> placeBlock = new VectorMap<>();
VectorMap<TileData> placeTile = new VectorMap<>();
VectorMap<IrisBlockVector> placeMax = savedOrigin && scale > 1 ? new VectorMap<>() : null;
@@ -153,13 +151,27 @@ final class IrisObjectTransforms {
if (scale > 1) {
IrisVector minimum = savedOrigin ? v : v.clone().add(center);
IrisVector maximum = savedOrigin ? placeMax.get(v) : v.clone().add(center).add(sm1);
for (IrisBlockVector vec : IrisObjectShaping.blocksBetweenTwoPoints(minimum, maximum)) {
oo.blocks.put(vec, entry.getValue());
if (hasTiles) {
if (tile == null) {
oo.states.remove(vec);
} else {
oo.states.put(vec, tile.clone());
int minX = Math.min(minimum.getBlockX(), maximum.getBlockX());
int maxX = Math.max(minimum.getBlockX(), maximum.getBlockX());
int minY = Math.min(minimum.getBlockY(), maximum.getBlockY());
int maxY = Math.max(minimum.getBlockY(), maximum.getBlockY());
int minZ = Math.min(minimum.getBlockZ(), maximum.getBlockZ());
int maxZ = Math.max(minimum.getBlockZ(), maximum.getBlockZ());
IrisBlockVector position = new IrisBlockVector(minX, minY, minZ);
for (int x = minX; x <= maxX; x++) {
position.setX(x);
for (int z = minZ; z <= maxZ; z++) {
position.setZ(z);
for (int y = minY; y <= maxY; y++) {
position.setY(y);
oo.blocks.put(position, entry.getValue());
if (hasTiles) {
if (tile == null) {
oo.states.remove(position);
} else {
oo.states.put(position, tile.clone());
}
}
}
}
}
@@ -0,0 +1,74 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.volmlib.util.collection.KMap;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisObjectCopyTest {
@Test
public void copyWaitsForVolumeMutationToComplete() throws Exception {
IrisObject source = new IrisObject(1, 1, 1);
PlatformBlockState block = mock(PlatformBlockState.class);
CountDownLatch started = new CountDownLatch(1);
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
Future<IrisObject> result;
source.writeLock.lock();
try {
result = executor.submit(() -> {
started.countDown();
return source.copy();
});
assertTrue(started.await(5, TimeUnit.SECONDS));
assertThrows(TimeoutException.class, () -> result.get(150, TimeUnit.MILLISECONDS));
source.setW(3);
source.setCenter(new Vector3i(1, 0, 0));
source.setUnsigned(2, 0, 0, block);
} finally {
source.writeLock.unlock();
}
IrisObject copy = result.get(5, TimeUnit.SECONDS);
assertEquals(3, copy.getW());
assertEquals(new Vector3i(1, 0, 0), copy.getCenter());
assertSame(block, copy.getBlocks().get(new IrisBlockVector(1, 0, 0)));
}
}
@Test
public void copyOwnsItsVolumeCoordinatesAndTiles() {
IrisObject source = new IrisObject(1, 1, 1);
PlatformBlockState block = mock(PlatformBlockState.class);
TileData tile = new TileData("minecraft:chest", new KMap<>());
tile.getProperties().put("name", "source");
source.setUnsigned(0, 0, 0, block);
source.setUnsignedTile(0, 0, 0, tile);
IrisObject copy = source.copy();
IrisBlockVector position = new IrisBlockVector(0, 0, 0);
TileData copiedTile = copy.getStates().get(position);
assertNotSame(source.getCenter(), copy.getCenter());
assertNotSame(tile, copiedTile);
assertNotSame(tile.getProperties(), copiedTile.getProperties());
copiedTile.getProperties().put("name", "copy");
copy.getBlocks().remove(position);
assertEquals("source", tile.getProperties().get("name"));
assertSame(block, source.getBlocks().get(position));
}
}
@@ -8,6 +8,7 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
@@ -101,6 +102,35 @@ public class NativeStructureObjectVetoTest {
assertTrue(placer.written().isEmpty());
}
@Test
public void obliqueRotationRejectsNativePiecesBeyondTheUnrotatedRadius() {
IrisObject object = new IrisObject(21, 1, 21);
object.setUnsigned(20, 0, 20, log);
IrisObjectPlacement placement = placement();
placement.setBottom(false);
placement.setRotation(IrisObjectRotation.of(0, 45, 0));
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(volume(14, SURFACE_Y, 0, 14, SURFACE_Y, 0));
assertEquals(-1, object.place(0, SURFACE_Y, 0, placer, placement, new RNG(1234L), data));
assertTrue(placer.written().isEmpty());
}
@Test
public void offCenterOriginRejectsNativePiecesAtTheFarEdge() {
IrisObject object = new IrisObject(9, 1, 1);
object.setCenter(new Vector3i(0, 0, 0));
object.setUnsigned(8, 0, 0, log);
IrisObjectPlacement placement = placement();
placement.setBottom(false);
placement.setRotation(IrisObjectRotation.of(0, 0, 0));
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(volume(8, SURFACE_Y, 0, 8, SURFACE_Y, 0));
assertEquals(-1, object.place(0, SURFACE_Y, 0, placer, placement, new RNG(1234L), data));
assertTrue(placer.written().isEmpty());
}
@Test
public void pieceOverlappingOnlyTheEnvelopeStillPlaces() {
RecordingPlacer placer = new RecordingPlacer(engine);