Initial palette rework

Signed-off-by: TheMode <themode@outlook.fr>
This commit is contained in:
TheMode 2021-11-05 15:58:35 +01:00
parent 41563ba973
commit 5f0c4aedbe
4 changed files with 273 additions and 356 deletions

View File

@ -59,7 +59,7 @@ public class DynamicChunk extends Chunk {
columnarOcclusionFieldList.onBlockChanged(x, y, z, blockDescription, 0);
}
Section section = getSection(ChunkUtils.getSectionAt(y));
section.setBlockAt(x, y, z, block.stateId());
section.blockPalette().set(toChunkRelativeCoordinate(x), y, toChunkRelativeCoordinate(z), block.stateId());
final int index = ChunkUtils.getBlockIndex(x, y, z);
// Handler
@ -113,9 +113,9 @@ public class DynamicChunk extends Chunk {
// Retrieve the block from state id
final Section section = getOptionalSection(y);
if (section == null) return Block.AIR; // Section is unloaded
final short blockStateId = section.getBlockAt(x, y, z);
final int blockStateId = section.blockPalette().get(toChunkRelativeCoordinate(x), y, toChunkRelativeCoordinate(z));
if (blockStateId == -1) return Block.AIR; // Section is empty
return Objects.requireNonNullElse(Block.fromStateId(blockStateId), Block.AIR);
return Objects.requireNonNullElse(Block.fromStateId((short) blockStateId), Block.AIR);
}
@Override
@ -176,10 +176,10 @@ public class DynamicChunk extends Chunk {
final BinaryWriter writer = new BinaryWriter();
for (int i = 0; i < 16; i++) { // TODO: variable section count
final Section section = Objects.requireNonNullElseGet(sectionMap.get(i), Section::new);
final Palette blockPalette = section.getPalette();
writer.writeShort(blockPalette.getBlockCount());
final Palette blockPalette = section.blockPalette();
writer.writeShort((short) blockPalette.count());
blockPalette.write(writer); // Blocks
new Palette(2, 2).write(writer); // Biomes
section.biomePalette().write(writer); // Biomes
}
return new ChunkDataPacket(chunkX, chunkZ,
new ChunkData(heightmapsNBT, writer.toByteArray(), entries),
@ -221,6 +221,14 @@ public class DynamicChunk extends Chunk {
skyLights, blockLights);
}
private static int toChunkRelativeCoordinate(int xz) {
xz %= 16;
if (xz < 0) {
xz += Chunk.CHUNK_SECTION_SIZE;
}
return xz;
}
private @Nullable Section getOptionalSection(int y) {
final int sectionIndex = ChunkUtils.getSectionAt(y);
return sectionMap.get(sectionIndex);

View File

@ -1,34 +1,33 @@
package net.minestom.server.instance;
import net.minestom.server.instance.palette.Palette;
import net.minestom.server.utils.clone.PublicCloneable;
import org.jetbrains.annotations.NotNull;
public class Section implements PublicCloneable<Section> {
public final class Section {
private Palette blockPalette;
private Palette biomePalette;
private byte[] skyLight;
private byte[] blockLight;
private final Palette palette;
private byte[] skyLight = new byte[0];
private byte[] blockLight = new byte[0];
private Section(Palette palette) {
this.palette = palette;
private Section(Palette blockPalette, Palette biomePalette,
byte[] skyLight, byte[] blockLight) {
this.blockPalette = blockPalette;
this.biomePalette = biomePalette;
this.skyLight = skyLight;
this.blockLight = blockLight;
}
public Section() {
this(new Palette(8, 2));
this(Palette.blocks(), Palette.biomes(),
new byte[0], new byte[0]);
}
public short getBlockAt(int x, int y, int z) {
x = toChunkRelativeCoordinate(x);
z = toChunkRelativeCoordinate(z);
return palette.getBlockAt(x, y, z);
public Palette blockPalette() {
return blockPalette;
}
public void setBlockAt(int x, int y, int z, short blockId) {
x = toChunkRelativeCoordinate(x);
z = toChunkRelativeCoordinate(z);
palette.setBlockAt(x, y, z, blockId);
public Palette biomePalette() {
return biomePalette;
}
public byte[] getSkyLight() {
@ -47,34 +46,16 @@ public class Section implements PublicCloneable<Section> {
this.blockLight = blockLight;
}
public void clean() {
palette.clean();
}
public void clear() {
palette.clear();
}
public Palette getPalette() {
return palette;
this.blockPalette = Palette.blocks();
this.biomePalette = Palette.biomes();
this.skyLight = new byte[0];
this.blockLight = new byte[0];
}
@Override
public @NotNull Section clone() {
return new Section(palette.clone());
}
/**
* Returns a coordinate that is relative to a chunk the provided coordinate is in.
*
* @param xz the world coordinate
* @return a coordinate relative to the closest chunk
*/
private static int toChunkRelativeCoordinate(int xz) {
xz %= 16;
if (xz < 0) {
xz += Chunk.CHUNK_SECTION_SIZE;
}
return xz;
return new Section(blockPalette.clone(), biomePalette.clone(),
skyLight.clone(), blockLight.clone());
}
}

View File

@ -1,325 +1,41 @@
package net.minestom.server.instance.palette;
import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.instance.Chunk;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.binary.Writeable;
import net.minestom.server.utils.clone.PublicCloneable;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_SIZE;
/**
* Represents a palette storing a complete chunk section.
* Represents a palette used to store blocks & biomes.
* <p>
* 0 is always interpreted as being air, reason being that the block array will be filled with it during initialization.
* 0 is the default value.
*/
@ApiStatus.Internal
public final class Palette implements Writeable, PublicCloneable<Palette> {
public interface Palette extends Writeable {
static Palette blocks() {
return new PaletteImpl(16 * 16 * 16, 8, 8, 2);
}
static Palette biomes() {
return new PaletteImpl(4 * 4 * 4, 2, 2, 1);
}
int get(int x, int y, int z);
void set(int x, int y, int z, int value);
int count();
/**
* The maximum bits per entry value.
* Returns the number of bits used per entry.
*/
public final static int MAXIMUM_BITS_PER_ENTRY = 15;
int bitsPerEntry();
int maxBitsPerEntry();
/**
* The minimum bits per entry value.
* Returns the number of entries in this palette.
*/
public final static int MINIMUM_BITS_PER_ENTRY = 4;
int size();
/**
* The maximum bits per entry value which allow for a data palette.
*/
public final static int PALETTE_MAXIMUM_BITS = 8;
long[] data();
/**
* The number of blocks that should be in one chunk section.
*/
public final static int BLOCK_COUNT = CHUNK_SECTION_SIZE * CHUNK_SECTION_SIZE * CHUNK_SECTION_SIZE;
// Magic values generated with "Integer.MAX_VALUE >> (31 - bitsPerIndex)" for bitsPerIndex between 4 and 15
private static final int[] MAGIC_MASKS =
{0, 0, 0, 0,
15, 31, 63, 127, 255,
511, 1023, 2047, 4095,
8191, 16383, 32767};
private static final Short2ShortOpenHashMap MAP_TEMPLATE = new Short2ShortOpenHashMap(CHUNK_SECTION_SIZE);
static {
MAP_TEMPLATE.put((short) 0, (short) 0);
}
private long[] blocks;
// palette index = block id
private short[] paletteBlockArray;
// block id = palette index
private Short2ShortOpenHashMap blockPaletteMap;
private int bitsPerEntry;
private final int bitsIncrement;
private int valuesPerLong;
private boolean hasPalette;
private int lastPaletteIndex;
private short blockCount = 0;
public Palette(int bitsPerEntry, int bitsIncrement) {
this.bitsPerEntry = bitsPerEntry;
this.bitsIncrement = bitsIncrement;
this.valuesPerLong = Long.SIZE / bitsPerEntry;
this.hasPalette = bitsPerEntry <= PALETTE_MAXIMUM_BITS;
clear();
}
public void setBlockAt(int x, int y, int z, short blockId) {
final boolean placedAir = blockId == 0;
if (blocks.length == 0) {
if (placedAir) {
// Section is empty and method is trying to place an air block, stop unnecessary computation
return;
}
// Initialize the section
this.blocks = new long[(BLOCK_COUNT + valuesPerLong - 1) / valuesPerLong];
}
// Change to palette value
blockId = getPaletteIndex(blockId);
final int sectionIndex = getSectionIndex(x, y, z);
final int index = sectionIndex / valuesPerLong;
final int bitIndex = (sectionIndex % valuesPerLong) * bitsPerEntry;
long block = blocks[index];
{
final long clear = MAGIC_MASKS[bitsPerEntry];
final long oldBlock = block >> bitIndex & clear;
if (oldBlock == blockId)
return; // Trying to place the same block
final boolean currentAir = oldBlock == 0;
final long indexClear = clear << bitIndex;
block |= indexClear;
block ^= indexClear;
block |= (long) blockId << bitIndex;
if (currentAir != placedAir) {
// Block count changed
this.blockCount += (short) (currentAir ? 1 : -1);
}
blocks[index] = block;
}
}
public short getBlockAt(int x, int y, int z) {
if (blocks.length == 0) {
// Section is not loaded, can only be air
return -1;
}
final int sectionIdentifier = getSectionIndex(x, y, z);
final int index = sectionIdentifier / valuesPerLong;
final int bitIndex = sectionIdentifier % valuesPerLong * bitsPerEntry;
final short value = (short) (blocks[index] >> bitIndex & MAGIC_MASKS[bitsPerEntry]);
// Change to palette value and return
return hasPalette ? paletteBlockArray[value] : value;
}
/**
* Resizes the array.
* <p>
* Will create a new palette storage to set all the current blocks, and the data will be transferred to 'this'.
*
* @param newBitsPerEntry the new bits per entry count
*/
public void resize(int newBitsPerEntry) {
newBitsPerEntry = fixBitsPerEntry(newBitsPerEntry);
Palette palette = new Palette(newBitsPerEntry, bitsIncrement);
for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) {
for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
palette.setBlockAt(x, y, z, getBlockAt(x, y, z));
}
}
}
this.paletteBlockArray = palette.paletteBlockArray;
this.lastPaletteIndex = palette.lastPaletteIndex;
this.bitsPerEntry = palette.bitsPerEntry;
this.valuesPerLong = palette.valuesPerLong;
this.hasPalette = palette.hasPalette;
this.blocks = palette.blocks;
this.blockCount = palette.blockCount;
}
/**
* Loops through all the sections and blocks to find unused array (empty chunk section)
* <p>
* Useful after clearing one or multiple sections of a chunk. Can be unnecessarily expensive if the chunk
* is composed of almost-empty sections since the loop will not stop until a non-air block is discovered.
*/
public synchronized void clean() {
if (blocks.length != 0) {
boolean canClear = true;
for (long blockGroup : blocks) {
if (blockGroup != 0) {
canClear = false;
break;
}
}
if (canClear) {
this.blocks = new long[0];
}
}
}
public void clear() {
this.blocks = new long[0];
this.paletteBlockArray = new short[1 << bitsPerEntry];
this.blockPaletteMap = MAP_TEMPLATE.clone();
this.blockCount = 0;
this.lastPaletteIndex = 1; // First index is air
}
public long[] getBlocks() {
return blocks;
}
public void setBlocks(long[] blocks) {
this.blocks = blocks;
}
/**
* Get the amount of non-air blocks in this section.
*
* @return The amount of blocks in this section.
*/
public short getBlockCount() {
return blockCount;
}
public void setBlockCount(short blockCount) {
this.blockCount = blockCount;
}
public short[] getPaletteBlockArray() {
return paletteBlockArray;
}
public int getLastPaletteIndex() {
return lastPaletteIndex;
}
public Short2ShortOpenHashMap getBlockPaletteMap() {
return blockPaletteMap;
}
public int getBitsPerEntry() {
return bitsPerEntry;
}
/**
* Retrieves the palette index for the specified block id.
* <p>
* Also responsible for resizing the palette when full.
*
* @param blockId the block id to convert
* @return the palette index of {@code blockId}
*/
private short getPaletteIndex(short blockId) {
if (!hasPalette) return blockId;
final short value = blockPaletteMap.getOrDefault(blockId, (short) -1);
if (value != -1) return value;
if (lastPaletteIndex >= paletteBlockArray.length) {
// Palette is full, must resize
resize(bitsPerEntry + bitsIncrement);
if (!hasPalette) return blockId;
}
final short paletteIndex = (short) lastPaletteIndex++;
this.paletteBlockArray[paletteIndex] = blockId;
this.blockPaletteMap.put(blockId, paletteIndex);
return paletteIndex;
}
/**
* Gets the index of the block on the section array based on the block position.
*
* @param x the chunk X
* @param y the chunk Y
* @param z the chunk Z
* @return the section index of the position
*/
public static int getSectionIndex(int x, int y, int z) {
y = Math.floorMod(y, CHUNK_SECTION_SIZE);
return y << 8 | z << 4 | x;
}
/**
* Fixes invalid bitsPerEntry values.
* <p>
* See https://wiki.vg/Chunk_Format#Direct
*
* @param bitsPerEntry the bits per entry value before fixing
* @return the fixed bits per entry value
*/
private static int fixBitsPerEntry(int bitsPerEntry) {
if (bitsPerEntry < MINIMUM_BITS_PER_ENTRY) {
return MINIMUM_BITS_PER_ENTRY;
} else if (MathUtils.isBetween(bitsPerEntry, 9, 14)) {
return MAXIMUM_BITS_PER_ENTRY;
}
return bitsPerEntry;
}
@Override
public void write(@NotNull BinaryWriter writer) {
writer.writeByte((byte) bitsPerEntry);
// Palette
if (bitsPerEntry < 9) {
// Palette has to exist
final short[] paletteBlockArray = getPaletteBlockArray();
final int paletteSize = getLastPaletteIndex() + 1;
writer.writeVarInt(paletteSize);
for (int i = 0; i < paletteSize; i++) {
writer.writeVarInt(paletteBlockArray[i]);
}
}
// Raw
writer.writeVarInt(blocks.length);
for (long datum : blocks) {
writer.writeLong(datum);
}
}
@Override
public @NotNull Palette clone() {
try {
Palette palette = (Palette) super.clone();
palette.blocks = blocks.clone();
palette.paletteBlockArray = paletteBlockArray.clone();
palette.blockPaletteMap = blockPaletteMap.clone();
palette.blockCount = blockCount;
return palette;
} catch (CloneNotSupportedException e) {
MinecraftServer.getExceptionManager().handleException(e);
throw new IllegalStateException("Weird thing happened");
}
}
@NotNull Palette clone();
}

View File

@ -0,0 +1,212 @@
package net.minestom.server.instance.palette;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.instance.Chunk;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.NotNull;
import static net.minestom.server.instance.Chunk.CHUNK_SECTION_SIZE;
final class PaletteImpl implements Palette {
// Magic values generated with "Integer.MAX_VALUE >> (31 - bitsPerIndex)" for bitsPerIndex between 4 and 15
private static final int[] MAGIC_MASKS =
{0, 0, 0, 0,
15, 31, 63, 127, 255,
511, 1023, 2047, 4095,
8191, 16383, 32767};
// Specific to this palette type
private final int size;
private final int maxBitsPerEntry;
private int bitsPerEntry;
private final int bitsIncrement;
private int valuesPerLong;
private boolean hasPalette;
private int lastPaletteIndex = 1; // First index is air
private int count = 0;
private long[] values = new long[0];
// palette index = value
private int[] paletteToValueArray;
// value = palette index
private Int2IntOpenHashMap valueToPaletteMap = new Int2IntOpenHashMap();
PaletteImpl(int size, int maxBitsPerEntry, int bitsPerEntry, int bitsIncrement) {
this.size = size;
this.maxBitsPerEntry = maxBitsPerEntry;
this.bitsPerEntry = bitsPerEntry;
this.bitsIncrement = bitsIncrement;
this.valuesPerLong = Long.SIZE / bitsPerEntry;
this.hasPalette = bitsPerEntry <= maxBitsPerEntry;
this.paletteToValueArray = new int[1 << bitsPerEntry];
this.valueToPaletteMap.put(0, 0);
}
@Override
public int get(int x, int y, int z) {
if (values.length == 0) {
// Section is not loaded, can only be air
return -1;
}
final int sectionIdentifier = getSectionIndex(x, y, z);
final int index = sectionIdentifier / valuesPerLong;
final int bitIndex = sectionIdentifier % valuesPerLong * bitsPerEntry;
final short value = (short) (values[index] >> bitIndex & MAGIC_MASKS[bitsPerEntry]);
// Change to palette value and return
return hasPalette ? paletteToValueArray[value] : value;
}
@Override
public void set(int x, int y, int z, int value) {
final boolean placedAir = value == 0;
if (values.length == 0) {
if (placedAir) {
// Section is empty and method is trying to place an air block, stop unnecessary computation
return;
}
// Initialize the section
this.values = new long[(size + valuesPerLong - 1) / valuesPerLong];
}
// Change to palette value
value = getPaletteIndex(value);
final int sectionIndex = getSectionIndex(x, y, z);
final int index = sectionIndex / valuesPerLong;
final int bitIndex = (sectionIndex % valuesPerLong) * bitsPerEntry;
long block = values[index];
{
final long clear = MAGIC_MASKS[bitsPerEntry];
final long oldBlock = block >> bitIndex & clear;
if (oldBlock == value)
return; // Trying to place the same block
final boolean currentAir = oldBlock == 0;
final long indexClear = clear << bitIndex;
block |= indexClear;
block ^= indexClear;
block |= (long) value << bitIndex;
if (currentAir != placedAir) {
// Block count changed
this.count += currentAir ? 1 : -1;
}
values[index] = block;
}
}
@Override
public int count() {
return count;
}
@Override
public int bitsPerEntry() {
return bitsPerEntry;
}
@Override
public int maxBitsPerEntry() {
return maxBitsPerEntry;
}
@Override
public int size() {
return size;
}
@Override
public long[] data() {
return values;
}
@Override
public @NotNull Palette clone() {
try {
PaletteImpl palette = (PaletteImpl) super.clone();
palette.values = values.clone();
palette.paletteToValueArray = paletteToValueArray.clone();
palette.valueToPaletteMap = valueToPaletteMap.clone();
palette.count = count;
return palette;
} catch (CloneNotSupportedException e) {
MinecraftServer.getExceptionManager().handleException(e);
throw new IllegalStateException("Weird thing happened");
}
}
@Override
public void write(@NotNull BinaryWriter writer) {
writer.writeByte((byte) bitsPerEntry);
// Palette
if (bitsPerEntry < 9) {
// Palette has to exist
final int paletteSize = lastPaletteIndex + 1;
writer.writeVarInt(paletteSize);
for (int i = 0; i < paletteSize; i++) {
writer.writeVarInt(paletteToValueArray[i]);
}
}
// Raw
writer.writeVarInt(values.length);
for (long datum : values) {
writer.writeLong(datum);
}
}
private int fixBitsPerEntry(int bitsPerEntry) {
return bitsPerEntry > maxBitsPerEntry ? 15 : bitsPerEntry;
}
private void resize(int newBitsPerEntry) {
newBitsPerEntry = fixBitsPerEntry(newBitsPerEntry);
PaletteImpl palette = new PaletteImpl(size, maxBitsPerEntry, newBitsPerEntry, bitsIncrement);
for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) {
for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) {
for (int z = 0; z < Chunk.CHUNK_SIZE_Z; z++) {
palette.set(x, y, z, get(x, y, z));
}
}
}
this.paletteToValueArray = palette.paletteToValueArray;
this.lastPaletteIndex = palette.lastPaletteIndex;
this.bitsPerEntry = palette.bitsPerEntry;
this.valuesPerLong = palette.valuesPerLong;
this.hasPalette = palette.hasPalette;
this.values = palette.values;
this.count = palette.count;
}
private int getPaletteIndex(int value) {
if (!hasPalette) return value;
final int lookup = valueToPaletteMap.getOrDefault(value, (short) -1);
if (lookup != -1) return lookup;
if (lastPaletteIndex >= paletteToValueArray.length) {
// Palette is full, must resize
resize(bitsPerEntry + bitsIncrement);
if (!hasPalette) return value;
}
final int paletteIndex = lastPaletteIndex++;
this.paletteToValueArray[paletteIndex] = value;
this.valueToPaletteMap.put(value, paletteIndex);
return paletteIndex;
}
static int getSectionIndex(int x, int y, int z) {
y = Math.floorMod(y, CHUNK_SECTION_SIZE);
return y << 8 | z << 4 | x;
}
}