diff --git a/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java b/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java index 43f5c241d1..e1dbbca6f4 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java +++ b/chunky/src/java/se/llbit/chunky/chunk/BlockPalette.java @@ -16,6 +16,8 @@ */ package se.llbit.chunky.chunk; +import it.unimi.dsi.fastutil.longs.Long2IntMap; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import se.llbit.chunky.block.*; import se.llbit.chunky.plugin.PluginApi; import se.llbit.math.Octree; @@ -98,6 +100,19 @@ public void unsynchronize() { }; } + private final Long2IntMap waterIds = new Long2IntOpenHashMap(); + private final Long2IntMap lavaIds = new Long2IntOpenHashMap(); + + /** + * This method should be called after the chunks were loaded and nothing will be changed about this palette anymore. + * + * It removes intermediate structures. + */ + public void compact() { + waterIds.clear(); + lavaIds.clear(); + } + /** * Adds a new block to the palette and returns the palette index. * @@ -163,12 +178,14 @@ public int getWaterId(int level, int data) { if (level < 0 || level > 15) { throw new IllegalArgumentException("Invalid water level " + level); } - CompoundTag tag = new CompoundTag(); - tag.add("Name", new StringTag("minecraft:water$chunky")); - tag.add("level", new IntTag(level)); - tag.add("data", new IntTag(data)); - BlockSpec spec = new BlockSpec(tag); - return put(spec); + long id = (level & 0xF) | ((data & 0xFFFFFFFFL) << 4); + return waterIds.computeIfAbsent(id, key -> { + CompoundTag tag = new CompoundTag(); + tag.add("Name", new StringTag("minecraft:water$chunky")); + tag.add("level", new IntTag(level)); + tag.add("data", new IntTag(data)); + return put(new BlockSpec(tag)); + }); } /** @@ -184,12 +201,14 @@ public int getLavaId(int level, int data) { if (level < 0 || level > 15) { throw new IllegalArgumentException("Invalid lava level " + level); } - CompoundTag tag = new CompoundTag(); - tag.add("Name", new StringTag("minecraft:lava$chunky")); - tag.add("level", new IntTag(level)); - tag.add("data", new IntTag(data)); - BlockSpec spec = new BlockSpec(tag); - return put(spec); + long id = (level & 0xFFFFFFFFL) | (data & 0xFFFFFFFFL) << 32; + return lavaIds.computeIfAbsent(id, key -> { + CompoundTag tag = new CompoundTag(); + tag.add("Name", new StringTag("minecraft:lava$chunky")); + tag.add("level", new IntTag(level)); + tag.add("data", new IntTag(data)); + return put(new BlockSpec(tag)); + }); } /** diff --git a/chunky/src/java/se/llbit/chunky/chunk/GenericChunkData.java b/chunky/src/java/se/llbit/chunky/chunk/GenericChunkData.java index 17a6ae945f..23a0accb04 100644 --- a/chunky/src/java/se/llbit/chunky/chunk/GenericChunkData.java +++ b/chunky/src/java/se/llbit/chunky/chunk/GenericChunkData.java @@ -51,6 +51,7 @@ public class GenericChunkData implements ChunkData { return; SectionData sectionData = sections.computeIfAbsent(sectionY, SectionData::new); sectionData.blocks[chunkIndex(x & (X_MAX - 1), y & (SECTION_Y_MAX - 1), z & (Z_MAX - 1))] = block; + sectionData.isEmpty = false; } @Override public boolean isBlockOnEdge(int x, int y, int z) { @@ -80,7 +81,9 @@ public void addEntity(CompoundTag entity) { @Override public void clear() { minSectionY = Integer.MAX_VALUE; maxSectionY = Integer.MIN_VALUE; - sections.clear(); + for (SectionData section : sections.values()) { + section.clear(); + } biomeData.clear(); tileEntities.clear(); entities.clear(); @@ -98,7 +101,7 @@ public void setBiomeData(BiomeData biomeData) { @Override public boolean isEmpty() { - return sections.isEmpty() && entities.isEmpty() && tileEntities.isEmpty(); + return entities.isEmpty() && tileEntities.isEmpty() && (sections.isEmpty() || sections.values().stream().allMatch(section -> section.isEmpty)); } @Override public boolean equals(Object o) { @@ -117,6 +120,7 @@ public boolean isEmpty() { private static class SectionData { public final int sectionY; public final int[] blocks; + private boolean isEmpty = true; public SectionData(int sectionY) { this(sectionY, new int[X_MAX * SECTION_Y_MAX * Z_MAX]); @@ -139,5 +143,10 @@ public SectionData(int sectionY, int[] blocks) { result = 31 * result + Arrays.hashCode(blocks); return result; } + + public void clear() { + Arrays.fill(blocks, 0); + isEmpty = true; + } } } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index bac09a6039..3e729fcbe4 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -1535,6 +1535,8 @@ public synchronized void loadChunks(TaskTracker taskTracker, World world, Collec } Log.info(String.format("Loaded %d chunks", numChunks)); + palette.compact(); + isLoading = false; } diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/biome/WorldTexture2dBiomeStructure.java b/chunky/src/java/se/llbit/chunky/renderer/scene/biome/WorldTexture2dBiomeStructure.java index ac8072a7cf..c19cb1b38f 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/biome/WorldTexture2dBiomeStructure.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/biome/WorldTexture2dBiomeStructure.java @@ -1,6 +1,9 @@ package se.llbit.chunky.renderer.scene.biome; +import it.unimi.dsi.fastutil.longs.Long2IntMap; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import se.llbit.chunky.world.WorldTexture; +import se.llbit.math.structures.Position2IntStructure; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -19,6 +22,11 @@ public BiomeStructure load(DataInputStream in) throws IOException { return new Impl(WorldTexture.load(in)); } + @Override + public Position2IntStructure createIndexStructure() { + return new StructureImpl(); + } + @Override public boolean is3d() { return false; @@ -71,4 +79,20 @@ public float[] get(int x, int y, int z) { return texture.get(x, z); } } + + private static class StructureImpl implements Position2IntStructure { + private final Long2IntMap biomeMap = new Long2IntOpenHashMap(); + + @Override + public void set(int x, int y, int z, int data) { + long key = ((long) x >> 4) << 32 | ((z >> 4) & 0xffffffffL); + biomeMap.put(key, data); + } + + @Override + public int get(int x, int y, int z) { + long key = ((long) x >> 4) << 32 | ((z >> 4) & 0xffffffffL); + return biomeMap.get(key); + } + } } diff --git a/chunky/src/java/se/llbit/chunky/world/ChunkPosition.java b/chunky/src/java/se/llbit/chunky/world/ChunkPosition.java index 518b4ff365..87173498cd 100644 --- a/chunky/src/java/se/llbit/chunky/world/ChunkPosition.java +++ b/chunky/src/java/se/llbit/chunky/world/ChunkPosition.java @@ -17,7 +17,6 @@ package se.llbit.chunky.world; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** * A chunk position consists of two integer coordinates x and z. @@ -35,6 +34,19 @@ private ChunkPosition(int x, int z) { this.z = z; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChunkPosition that = (ChunkPosition) o; + return x == that.x && z == that.z; + } + + @Override + public int hashCode() { + return z * 31 + x; + } + @Override public String toString() { return "[" + x + ", " + z + "]"; @@ -44,11 +56,8 @@ public ChunkPosition regionPosition() { return get(x >> 5, z >> 5); } - //not using synchronized Long2ReferenceOpenHashMap due to using one lock. ConcurrentHashMap locks on buckets - private static final Map positions = new ConcurrentHashMap<>(); - public static ChunkPosition get(int x, int z) { - return positions.computeIfAbsent(positionToLong(x, z), (position) -> new ChunkPosition(x, z)); + return new ChunkPosition(x, z); } public static long positionToLong(int x, int z) { diff --git a/chunky/src/java/se/llbit/math/PackedOctree.java b/chunky/src/java/se/llbit/math/PackedOctree.java index 058f6d6e27..7f3d34235f 100644 --- a/chunky/src/java/se/llbit/math/PackedOctree.java +++ b/chunky/src/java/se/llbit/math/PackedOctree.java @@ -289,6 +289,8 @@ private boolean nodeEquals(int firstNodeIndex, int secondNodeIndex) { return (!firstIsBranch && !secondIsBranch && treeData[firstNodeIndex] == treeData[secondNodeIndex]); // compare types } + private int[] parents = new int[depth]; + /** * Sets a specified block within the octree to a specific palette value, subdividing and merging as needed. * @@ -296,7 +298,10 @@ private boolean nodeEquals(int firstNodeIndex, int secondNodeIndex) { */ @Override public void set(int type, int x, int y, int z) { - int[] parents = new int[depth]; // better to put as a field to prevent allocation at each invocation? + if (this.parents.length < depth) { + this.parents = new int[depth]; + } + int[] parents = this.parents; int nodeIndex = 0; // start at root int position; @@ -449,7 +454,10 @@ else if(firstType != tempTree.get(curDepth+1)[childrenIdx+childNo] && tempTree.g int type = tempTree.get(0)[0]; - int[] parents = new int[depth]; // better to put as a field to prevent allocation at each invocation? + if (this.parents.length < depth) { + this.parents = new int[depth]; + } + int[] parents = this.parents; int nodeIndex = 0; // start at root int position;