Skip to content

Commit

Permalink
fix: Improve and simplify chunk bucketing for light updates
Browse files Browse the repository at this point in the history
  • Loading branch information
jellysquid3 committed Apr 1, 2020
1 parent e577acd commit e9ac5e1
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public LightEngineBlockAccess(ChunkProvider provider) {
}

public BlockState getBlockState(int x, int y, int z) {
if (y < 0 || y >= 256) {
return DEFAULT_STATE;
}

ChunkSection[] sections = this.getCachedSection(x >> 4, z >> 4);

if (sections != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import me.jellysquid.mods.phosphor.common.chunk.level.PendingUpdateListener;
import me.jellysquid.mods.phosphor.common.util.cache.LightEngineBlockAccess;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.util.math.Direction;
Expand All @@ -26,14 +25,11 @@
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

import java.util.Arrays;
import java.util.BitSet;

@Mixin(ChunkLightProvider.class)
public abstract class MixinChunkLightProvider<M extends ChunkToNibbleArrayMap<M>, S extends LightStorage<M>>
extends LevelPropagator implements ChunkLightProviderExtended, PendingUpdateListener {
private static long GLOBAL_TO_CHUNK_MASK = ~BlockPos.asLong(0xF, 0xF, 0xF);

@Shadow
@Final
protected BlockPos.Mutable reusableBlockPos;
Expand All @@ -44,26 +40,18 @@ public abstract class MixinChunkLightProvider<M extends ChunkToNibbleArrayMap<M>

private LightEngineBlockAccess blockAccess;

private Long2ObjectOpenHashMap<BitSet> pendingUpdatesByChunk;
private final Long2ObjectOpenHashMap<BitSet> buckets = new Long2ObjectOpenHashMap<>();

private BitSet[] lastChunkUpdateSets;
private long[] lastChunkPos;
private long prevChunkBucketKey = Long.MIN_VALUE;
private BitSet prevChunkBucketSet;

protected MixinChunkLightProvider(int levelCount, int expectedLevelSize, int expectedTotalSize) {
super(levelCount, expectedLevelSize, expectedTotalSize);
}


@Inject(method = "<init>", at = @At("RETURN"))
private void onConstructed(ChunkProvider provider, LightType lightType, S storage, CallbackInfo ci) {
this.blockAccess = new LightEngineBlockAccess(provider);
this.pendingUpdatesByChunk = new Long2ObjectOpenHashMap<>(512, 0.25F);

this.lastChunkUpdateSets = new BitSet[2];
this.lastChunkPos = new long[2];

this.resetUpdateSetCache();

}

@Inject(method = "clearChunkCache", at = @At("RETURN"))
Expand All @@ -77,10 +65,6 @@ private void onCleanup(CallbackInfo ci) {
// [VanillaCopy] method_20479
@Override
public BlockState getBlockStateForLighting(int x, int y, int z) {
if (y < 0 || y >= 256) {
return Blocks.AIR.getDefaultState();
}

return this.blockAccess.getBlockState(x, y, z);
}

Expand Down Expand Up @@ -123,123 +107,106 @@ public VoxelShape getOpaqueShape(BlockState state, int x, int y, int z, Directio
* update (<8K checks) or every block position within a sub-chunk (16^3 checks). This is painfully slow and results
* in a tremendous amount of CPU time being spent here when chunks are unloaded on the client and server.
*
* To work around this, we maintain a list of queued updates by chunk position so we can simply select every light
* update within a chunk and drop them in one operation.
* To work around this, we maintain a bit-field of queued updates by chunk position so we can simply select every
* light update within a section without excessive iteration. The bit-field only requires 64 bytes of memory per
* section with queued updates, and does not require expensive hashing in order to track updates within it. In order
* to avoid as much overhead as possible when looking up a bit-field for a given chunk section, the previous lookup
* is cached and used where possible. The integer key for each bucket can be computed by performing a simple bit
* mask over the already-encoded block position value.
*/
@Override
public void cancelUpdatesForChunk(long sectionPos) {
int chunkX = ChunkSectionPos.getX(sectionPos);
int chunkY = ChunkSectionPos.getY(sectionPos);
int chunkZ = ChunkSectionPos.getZ(sectionPos);
long key = getBucketKeyForSection(sectionPos);
BitSet bits = this.removeChunkBucket(key);

long key = toChunkKey(BlockPos.asLong(chunkX << 4, chunkY << 4, chunkZ << 4));
if (bits != null && !bits.isEmpty()) {
int startX = ChunkSectionPos.getX(sectionPos) << 4;
int startY = ChunkSectionPos.getY(sectionPos) << 4;
int startZ = ChunkSectionPos.getZ(sectionPos) << 4;

BitSet set = this.pendingUpdatesByChunk.remove(key);
for (int i = bits.nextSetBit(0); i != -1; i = bits.nextSetBit(i + 1)) {
int x = (i >> 8) & 15;
int y = (i >> 4) & 15;
int z = i & 15;

if (set == null || set.isEmpty()) {
return;
this.removePendingUpdate(BlockPos.asLong(startX + x, startY + y, startZ + z));
}
}

this.resetUpdateSetCache();

int startX = chunkX << 4;
int startY = chunkY << 4;
int startZ = chunkZ << 4;

set.stream().forEach(i -> {
int x = (i >> 8) & 0xF;
int y = (i >> 4) & 0xF;
int z = i & 0xF;

this.removePendingUpdate(BlockPos.asLong(startX + x, startY + y, startZ + z));
});
}


@Override
public void onPendingUpdateRemoved(long blockPos) {
BitSet set = this.getUpdateSetFor(toChunkKey(blockPos));
long key = getBucketKeyForBlock(blockPos);

if (set != null) {
set.clear(toLocalKey(blockPos));
BitSet bits;

if (set.isEmpty()) {
this.pendingUpdatesByChunk.remove(toChunkKey(blockPos));
if (this.prevChunkBucketKey == key) {
bits = this.prevChunkBucketSet;
} else {
bits = this.buckets.get(key);

if (bits == null) {
return;
}
}

bits.clear(getLocalIndex(blockPos));

if (bits.isEmpty()) {
this.removeChunkBucket(key);
}
}

@Override
public void onPendingUpdateAdded(long blockPos) {
BitSet set = this.getOrCreateUpdateSetFor(toChunkKey(blockPos));
set.set(toLocalKey(blockPos));
}
long key = getBucketKeyForBlock(blockPos);

private BitSet getUpdateSetFor(long chunkPos) {
BitSet set = this.getCachedUpdateSet(chunkPos);
BitSet bits;

if (set == null) {
set = this.pendingUpdatesByChunk.get(chunkPos);
if (this.prevChunkBucketKey == key) {
bits = this.prevChunkBucketSet;
} else {
bits = this.buckets.get(key);

if (set != null) {
this.addUpdateSetToCache(chunkPos, set);
if (bits == null) {
this.buckets.put(key, bits = new BitSet(16 * 16 * 16));
}
}

return set;
}

private BitSet getOrCreateUpdateSetFor(long chunkPos) {
BitSet set = this.getCachedUpdateSet(chunkPos);

if (set == null) {
set = this.pendingUpdatesByChunk.get(chunkPos);

if (set == null) {
this.pendingUpdatesByChunk.put(chunkPos, set = new BitSet(4096));
this.addUpdateSetToCache(chunkPos, set);
}
this.prevChunkBucketKey = key;
this.prevChunkBucketSet = bits;
}

return set;
bits.set(getLocalIndex(blockPos));
}

private BitSet getCachedUpdateSet(long chunkPos) {
long[] lastChunkPos = this.lastChunkPos;
// Used to mask a long-encoded block position into a bucket key by dropping the first 4 bits of each component
private static final long BLOCK_TO_BUCKET_KEY_MASK = ~BlockPos.asLong(15, 15, 15);

for (int i = 0; i < lastChunkPos.length; i++) {
if (lastChunkPos[i] == chunkPos) {
return this.lastChunkUpdateSets[i];
}
}

return null;
private long getBucketKeyForBlock(long blockPos) {
return blockPos & BLOCK_TO_BUCKET_KEY_MASK;
}

private void addUpdateSetToCache(long chunkPos, BitSet set) {
long[] lastPos = this.lastChunkPos;
lastPos[1] = lastPos[0];
lastPos[0] = chunkPos;

BitSet[] lastSet = this.lastChunkUpdateSets;
lastSet[1] = lastSet[0];
lastSet[0] = set;
private long getBucketKeyForSection(long sectionPos) {
return BlockPos.asLong(ChunkSectionPos.getX(sectionPos) << 4, ChunkSectionPos.getY(sectionPos) << 4, ChunkSectionPos.getZ(sectionPos) << 4);
}

protected void resetUpdateSetCache() {
Arrays.fill(this.lastChunkPos, Long.MIN_VALUE);
Arrays.fill(this.lastChunkUpdateSets, null);
}
private BitSet removeChunkBucket(long key) {
BitSet set = this.buckets.remove(key);

if (this.prevChunkBucketSet == set) {
this.prevChunkBucketKey = Long.MIN_VALUE;
this.prevChunkBucketSet = null;
}

private static long toChunkKey(long blockPos) {
return blockPos & GLOBAL_TO_CHUNK_MASK;
return set;
}

private static int toLocalKey(long pos) {
int x = BlockPos.unpackLongX(pos) & 0xF;
int y = BlockPos.unpackLongY(pos) & 0xF;
int z = BlockPos.unpackLongZ(pos) & 0xF;
// Finds the bit-flag index of a local position within a chunk section
private static int getLocalIndex(long blockPos) {
int x = BlockPos.unpackLongX(blockPos) & 15;
int y = BlockPos.unpackLongY(blockPos) & 15;
int z = BlockPos.unpackLongZ(blockPos) & 15;

return x << 8 | y << 4 | z;
return (x << 8) | (y << 4) | z;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private byte redirectRemovePendingUpdate(Long2ByteMap map, long key) {
private byte redirectAddPendingUpdate(Long2ByteMap map, long key, byte value) {
byte ret = map.put(key, value);

if (ret != map.defaultReturnValue()) {
if (ret == map.defaultReturnValue()) {
this.onPendingUpdateAdded(key);
}

Expand Down

0 comments on commit e9ac5e1

Please sign in to comment.