From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Wed, 28 Oct 2020 16:51:55 -0700 Subject: [PATCH] Rewrite the light engine The standard vanilla light engine is plagued by awful performance. Paper's changes to the light engine help a bit, however they appear to cause some lighting errors - most easily noticed in coral generation. The vanilla light engine's is too abstract to be modified - so an entirely new implementation is required to fix the performance and lighting errors. The new implementation is designed primarily to optimise light level propagations (increase and decrease). Unlike the vanilla light engine, this implementation tracks more information per queued value when performing a breadth first search. Vanilla just tracks coordinate, which means every time they handle a queued value, they must also determine the coordinate's target light level from its neighbours - very wasteful, especially considering these checks read neighbour block data. The new light engine tracks both position and target level, as well as whether the target block needs to be read at all (for checking sided propagation). So, the work done per coordinate is significantly reduced because no work is done for calculating the target level. In my testing, the block get calls were reduced by approximately an order of magnitude. However, the light read checks were only reduced by approximately 2x - but this is fine, light read checks are extremely cheap compared to block gets. Generation testing showed that the new light engine improved total generation (not lighting itself, but the whole generation process) by 2x. According to cpu time, the light engine itself spent 10x less time lighting chunks for generation. diff --git a/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..9efbdba758aebcad3454a9a52c8a7eae4b7fc7eb --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java @@ -0,0 +1,283 @@ +package ca.spottedleaf.starlight.light; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.*; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public final class BlockStarLightEngine extends StarLightEngine { + + public BlockStarLightEngine(final Level world) { + super(false, world); + } + + @Override + protected boolean[] getEmptinessMap(final ChunkAccess chunk) { + return chunk.getBlockEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { + chunk.setBlockEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { + return chunk.getBlockNibbles(); + } + + @Override + protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { + chunk.setBlockNibbles(to); + } + + @Override + protected boolean canUseChunk(final ChunkAccess chunk) { + return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); + } + + @Override + protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically + // because a block was removed - which can decrease light. with sky data, block breaking can only result + // in increases, and thus the existing sky block check will actually correctly propagate light through + // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove + // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running + // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence + // of vanilla data management we "hide" them. + nibble.setHidden(); + } + } + + @Override + protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); + } + } else { + nibble.setNonNull(); + } + } + + @Override + protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change emitted light + // blocks can change direction of propagation + + final int encodeOffset = this.coordinateOffset; + final int emittedMask = this.emittedLightMask; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); + final int emittedLevel = blockState.getLightEmission() & emittedMask; + + this.setLightLevel(worldX, worldY, worldZ, emittedLevel); + // this accounts for change in emitted light that would cause an increase + if (emittedLevel != 0) { + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + } + // this also accounts for a change in emitted light that would cause a decrease + // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa) + // as it checks all neighbours (even if current level is 0) + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + // always keep sided transparent false here, new block might be conditionally transparent which would + // prevent us from decreasing sources in the directions where the new block is opaque + // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always + // catch that and fix it. + ); + // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block + } + + protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); + int level = centerState.getLightEmission() & 0xF; + + if (level >= (15 - 1) || level > expect) { + return level; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final BlockState conditionallyOpaqueState; + int opacity = centerState.getOpacityIfCached(); + + if (opacity == -1) { + this.recalcCenterPos.set(worldX, worldY, worldZ); + opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos); + if (centerState.isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + } else if (opacity >= 15) { + return level; + } else { + conditionallyOpaqueState = null; + } + opacity = Math.max(1, opacity); + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final BlockState neighbourState = this.getBlockState(offX, offY, offZ); + if (neighbourState.isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + this.recalcNeighbourPos.set(offX, offY, offZ); + final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + // passed transparency, + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { + for (final BlockPos pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected Iterator getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { + if (chunk instanceof ImposterProtoChunk || chunk instanceof LevelChunk) { + // implementation on Chunk is pretty awful, so write our own here. The big optimisation is + // skipping empty sections, and the far more optimised reading of types. + List sources = new ArrayList<>(); + + int offX = chunk.getPos().x << 4; + int offZ = chunk.getPos().z << 4; + + final LevelChunkSection[] sections = chunk.getSections(); + for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { + final LevelChunkSection section = sections[sectionY - this.minSection]; + if (section == null || section.isEmpty()) { + // no sources in empty sections + continue; + } + final PalettedContainer states = section.states; + final int offY = sectionY << 4; + + for (int index = 0; index < (16 * 16 * 16); ++index) { + final BlockState state = states.get(index); + if (state.getLightEmission() <= 0) { + continue; + } + + // index = x | (z << 4) | (y << 8) + sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); + } + } + + return sources.iterator(); + } else { + // world gen and lighting run in parallel, and if lighting keeps up it can be lighting chunks that are + // being generated. In the nether, lava will add a lot of sources. This resulted in quite a few CME crashes. + // So all we do spinloop until we can collect a list of sources, and even if it is out of date we will pick up + // the missing sources from checkBlock. + for (;;) { + try { + return chunk.getLights().collect(Collectors.toList()).iterator(); + } catch (final Exception cme) { + continue; + } + } + } + } + + @Override + public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + // setup sources + final int emittedMask = this.emittedLightMask; + for (final Iterator positions = this.getSources(lightAccess, chunk); positions.hasNext();) { + final BlockPos pos = positions.next(); + final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); + final int emittedLight = blockState.getLightEmission() & emittedMask; + + if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { + // some other source is brighter + continue; + } + + this.appendToIncreaseQueue( + ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLight & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + + + // propagation wont set this for us + this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); + } + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + // verify neighbour edges + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + } else { + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); + + this.performLightIncrease(lightAccess); + } + } +} diff --git a/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java new file mode 100644 index 0000000000000000000000000000000000000000..174dc7ffa66258da0b867fba5c54880e81daa6ce --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java @@ -0,0 +1,439 @@ +package ca.spottedleaf.starlight.light; + +import net.minecraft.world.level.chunk.DataLayer; + +import java.util.ArrayDeque; +import java.util.Arrays; + +// SWMR -> Single Writer Multi Reader Nibble Array +public final class SWMRNibbleArray { + + /* + * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null + * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised + * nibbles can be written to. + * + * Uninitialised nibble - They are all 0, but the backing array isn't initialised. + * + * Initialised nibble - Has light data. + */ + + protected static final int INIT_STATE_NULL = 0; // null + protected static final int INIT_STATE_UNINIT = 1; // uninitialised + protected static final int INIT_STATE_INIT = 2; // initialised + protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL + + public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block + // this allows us to maintain only 1 byte array when we're not updating + static final ThreadLocal> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); + + private static byte[] allocateBytes() { + final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); + if (inPool != null) { + return inPool; + } + + return new byte[ARRAY_SIZE]; + } + + private static void freeBytes(final byte[] bytes) { + WORKING_BYTES_POOL.get().addFirst(bytes); + } + + public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { + if (nibble == null) { + return new SWMRNibbleArray(null, true); + } else if (nibble.isEmpty()) { + return new SWMRNibbleArray(); + } else { + return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later + } + } + + protected int stateUpdating; + protected volatile int stateVisible; + + protected byte[] storageUpdating; + protected boolean updatingDirty; // only returns whether storageUpdating is dirty + protected byte[] storageVisible; + + public SWMRNibbleArray() { + this(null, false); // lazy init + } + + public SWMRNibbleArray(final byte[] bytes) { + this(bytes, false); + } + + public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException("Data of wrong length: " + bytes.length); + } + this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; + this.storageUpdating = this.storageVisible = bytes; + } + + public SWMRNibbleArray(final byte[] bytes, final int state) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException("Data of wrong length: " + bytes.length); + } + if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { + throw new IllegalArgumentException("Data cannot be null and have state be initialised"); + } + this.stateUpdating = this.stateVisible = state; + this.storageUpdating = this.storageVisible = bytes; + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("State: "); + switch (this.stateVisible) { + case INIT_STATE_NULL: + stringBuilder.append("null"); + break; + case INIT_STATE_UNINIT: + stringBuilder.append("uninitialised"); + break; + case INIT_STATE_INIT: + stringBuilder.append("initialised"); + break; + case INIT_STATE_HIDDEN: + stringBuilder.append("hidden"); + break; + default: + stringBuilder.append("unknown"); + break; + } + stringBuilder.append("\nData:\n"); + + final byte[] data = this.storageVisible; + if (data != null) { + for (int i = 0; i < 4096; ++i) { + // Copied from NibbleArray#toString + final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); + + stringBuilder.append(Integer.toHexString(level)); + if ((i & 15) == 15) { + stringBuilder.append("\n"); + } + + if ((i & 255) == 255) { + stringBuilder.append("\n"); + } + } + } else { + stringBuilder.append("null"); + } + + return stringBuilder.toString(); + } + + public SaveState getSaveState() { + synchronized (this) { + final int state = this.stateVisible; + final byte[] data = this.storageVisible; + if (state == INIT_STATE_NULL) { + return null; + } + if (state == INIT_STATE_UNINIT) { + return new SaveState(null, state); + } + final boolean zero = isAllZero(data); + if (zero) { + return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; + } else { + return new SaveState(data.clone(), state); + } + } + } + + protected static boolean isAllZero(final byte[] data) { + for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { + byte whole = data[i << 4]; + + for (int k = 1; k < (1 << 4); ++k) { + whole |= data[(i << 4) | k]; + } + + if (whole != 0) { + return false; + } + } + + return true; + } + + // operation type: updating on src, updating on other + public void extrudeLower(final SWMRNibbleArray other) { + if (other.stateUpdating == INIT_STATE_NULL) { + throw new IllegalArgumentException(); + } + + if (other.storageUpdating == null) { + this.setUninitialised(); + return; + } + + final byte[] src = other.storageUpdating; + final byte[] into; + + if (this.storageUpdating != null) { + into = this.storageUpdating; + } else { + this.storageUpdating = into = allocateBytes(); + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + + final int start = 0; + final int end = (15 | (15 << 4)) >>> 1; + + /* x | (z << 4) | (y << 8) */ + for (int y = 0; y <= 15; ++y) { + System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); + } + } + + // operation type: updating + public void setFull() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); + this.updatingDirty = true; + } + + // operation type: updating + public void setZero() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); + this.updatingDirty = true; + } + + // operation type: updating + public void setNonNull() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + return; + } + if (this.stateUpdating != INIT_STATE_NULL) { + return; + } + this.stateUpdating = INIT_STATE_UNINIT; + } + + // operation type: updating + public void setNull() { + this.stateUpdating = INIT_STATE_NULL; + if (this.updatingDirty && this.storageUpdating != null) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setUninitialised() { + this.stateUpdating = INIT_STATE_UNINIT; + if (this.storageUpdating != null && this.updatingDirty) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setHidden() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + return; + } + if (this.stateUpdating != INIT_STATE_INIT) { + this.setNull(); + } else { + this.stateUpdating = INIT_STATE_HIDDEN; + } + } + + // operation type: updating + public boolean isDirty() { + return this.stateUpdating != this.stateVisible || this.updatingDirty; + } + + // operation type: updating + public boolean isNullNibbleUpdating() { + return this.stateUpdating == INIT_STATE_NULL; + } + + // operation type: visible + public boolean isNullNibbleVisible() { + return this.stateVisible == INIT_STATE_NULL; + } + + // opeartion type: updating + public boolean isUninitialisedUpdating() { + return this.stateUpdating == INIT_STATE_UNINIT; + } + + // operation type: visible + public boolean isUninitialisedVisible() { + return this.stateVisible == INIT_STATE_UNINIT; + } + + // operation type: updating + public boolean isInitialisedUpdating() { + return this.stateUpdating == INIT_STATE_INIT; + } + + // operation type: visible + public boolean isInitialisedVisible() { + return this.stateVisible == INIT_STATE_INIT; + } + + // operation type: updating + public boolean isHiddenUpdating() { + return this.stateUpdating == INIT_STATE_HIDDEN; + } + + // operation type: updating + public boolean isHiddenVisible() { + return this.stateVisible == INIT_STATE_HIDDEN; + } + + // operation type: updating + protected void swapUpdatingAndMarkDirty() { + if (this.updatingDirty) { + return; + } + + if (this.storageUpdating == null) { + this.storageUpdating = allocateBytes(); + Arrays.fill(this.storageUpdating, (byte)0); + } else { + System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); + } + + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + } + + // operation type: updating + public boolean updateVisible() { + if (!this.isDirty()) { + return false; + } + + synchronized (this) { + if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { + this.storageVisible = null; + } else { + if (this.storageVisible == null) { + this.storageVisible = this.storageUpdating.clone(); + } else { + if (this.storageUpdating != this.storageVisible) { + System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); + } + } + + if (this.storageUpdating != this.storageVisible) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = this.storageVisible; + } + this.updatingDirty = false; + this.stateVisible = this.stateUpdating; + } + + return true; + } + + // operation type: visible + public DataLayer toVanillaNibble() { + synchronized (this) { + switch (this.stateVisible) { + case INIT_STATE_HIDDEN: + case INIT_STATE_NULL: + return null; + case INIT_STATE_UNINIT: + return new DataLayer(); + case INIT_STATE_INIT: + return new DataLayer(this.storageVisible.clone()); + default: + throw new IllegalStateException(); + } + } + } + + /* x | (z << 4) | (y << 8) */ + + // operation type: updating + public int getUpdating(final int x, final int y, final int z) { + return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: updating + public int getUpdating(final int index) { + // indices range from 0 -> 4096 + final byte[] bytes = this.storageUpdating; + if (bytes == null) { + return 0; + } + final byte value = bytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + + // operation type: visible + public int getVisible(final int x, final int y, final int z) { + return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: visible + public int getVisible(final int index) { + synchronized (this) { + // indices range from 0 -> 4096 + final byte[] visibleBytes = this.storageVisible; + if (visibleBytes == null) { + return 0; + } + final byte value = visibleBytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + } + + // operation type: updating + public void set(final int x, final int y, final int z, final int value) { + this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); + } + + // operation type: updating + public void set(final int index, final int value) { + if (!this.updatingDirty) { + this.swapUpdatingAndMarkDirty(); + } + final int shift = (index & 1) << 2; + final int i = index >>> 1; + + this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); + } + + public static final class SaveState { + + public final byte[] data; + public final int state; + + public SaveState(final byte[] data, final int state) { + this.data = data; + this.state = state; + } + } +} diff --git a/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..e843ceb27bce134f7785e8c45fac25d5ec747233 --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java @@ -0,0 +1,715 @@ +package ca.spottedleaf.starlight.light; + +import io.papermc.paper.util.WorldUtil; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.Arrays; +import java.util.Set; + +public final class SkyStarLightEngine extends StarLightEngine { + + /* + Specification for managing the initialisation and de-initialisation of skylight nibble arrays: + + Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null. + + This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks. + However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees + that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise + our own) - we need a radius of 2 to de-initialise neighbour nibbles. + How do we solve this? + + Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections. + If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the + chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last + known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data + to see if any of its nibbles need to be de-initialised. + + The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data, + and if it doesn't have data then we know it will correctly de-initialise once it fills up. + + Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking + around those. + */ + + protected final int[] heightMapBlockChange = new int[16 * 16]; + { + Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap + } + + protected final boolean[] nullPropagationCheckCache; + + public SkyStarLightEngine(final Level world) { + super(true, world); + this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)]; + } + + @Override + protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true)); + } + } + this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude); + } + + @Override + protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + nibble.setNull(); + } + } + + protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) { + if (!currNibble.isNullNibbleUpdating()) { + // already initialised + return; + } + + final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ); + + // are we above this chunk's lowest empty section? + int lowestY = this.minLightSection - 1; + for (int currY = this.maxSection; currY >= this.minSection; --currY) { + if (emptinessMap == null) { + // cannot delay nibble init for lit chunks, as we need to init to propagate into them. + final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ); + if (current == null || current == EMPTY_CHUNK_SECTION) { + continue; + } + } else { + if (emptinessMap[currY - this.minSection]) { + continue; + } + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (chunkY > lowestY) { + // we need to set this one to full + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + nibble.setNonNull(); + nibble.setFull(); + return; + } + + if (extrude) { + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + currNibble.setNonNull(); + currNibble.extrudeLower(nibble); + break; + } + } + } else { + currNibble.setNonNull(); + } + } + + protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (nibble != null && nibble.isNullNibbleUpdating()) { + // stop propagation in these areas + this.nibbleCache[index] = null; + nibble.updateVisible(); + } + } + } + + // rets whether neighbours were init'd + + protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ, + final boolean extrudeInitialised) { + // null chunk sections may have nibble neighbours in the horizontal 1 radius that are + // non-null. Propagation to these neighbours is necessary. + // What makes this easy is we know none of these neighbours are non-empty (otherwise + // this nibble would be initialised). So, we don't have to initialise + // the neighbours in the full 1 radius, because there's no worry that any "paths" + // to the neighbours on this horizontal plane are blocked. + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) { + return false; + } + this.nullPropagationCheckCache[chunkY - this.minLightSection] = true; + + // check horizontal neighbours + boolean needInitNeighbours = false; + neighbour_search: + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + needInitNeighbours = true; + break neighbour_search; + } + } + } + + if (needInitNeighbours) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true); + } + } + } + + return needInitNeighbours; + } + + protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) { + final int chunkX = worldX >> 4; + int chunkY = worldY >> 4; + final int chunkZ = worldZ >> 4; + + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + return nibble.getUpdating(worldX, worldY, worldZ); + } + + for (;;) { + if (++chunkY > this.maxLightSection) { + return 15; + } + + nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + + if (nibble != null) { + return nibble.getUpdating(worldX, 0, worldZ); + } + } + } + + @Override + protected boolean[] getEmptinessMap(final ChunkAccess chunk) { + return chunk.getSkyEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { + chunk.setSkyEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { + return chunk.getSkyNibbles(); + } + + @Override + protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { + chunk.setSkyNibbles(to); + } + + @Override + protected boolean canUseChunk(final ChunkAccess chunk) { + // can only use chunks for sky stuff if their sections have been init'd + return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); + } + + @Override + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, + final int toSection) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (int y = toSection; y >= fromSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, fromSection, toSection); + } + + @Override + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + final int y = (int)iterator.nextShort(); + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, sections); + } + + @Override + protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change direction of propagation + + // same logic applies from BlockStarLightEngine#checkBlock + + final int encodeOffset = this.coordinateOffset; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + + if (currentLevel == 15) { + // must re-propagate clobbered source + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent + ); + } else { + this.setLightLevel(worldX, worldY, worldZ, 0); + } + + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + ); + } + + protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); + + @Override + protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + if (expect == 15) { + return expect; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); + int opacity = centerState.getOpacityIfCached(); + + BlockState conditionallyOpaqueState; + if (opacity < 0) { + this.recalcCenterPos.set(worldX, worldY, worldZ); + opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos)); + if (centerState.isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + } else { + conditionallyOpaqueState = null; + opacity = Math.max(1, opacity); + } + + int level = 0; + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final BlockState neighbourState = this.getBlockState(offX, offY, offZ); + + if (neighbourState.isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + this.recalcNeighbourPos.set(offX, offY, offZ); + final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms); + if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { + this.rewriteNibbleCacheForSkylight(atChunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final BlockGetter world = lightAccess.getLevel(); + final int chunkX = atChunk.getPos().x; + final int chunkZ = atChunk.getPos().z; + final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16)); + + // setup heightmap for changes + for (final BlockPos pos : positions) { + final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset; + final int curr = this.heightMapBlockChange[index]; + if (pos.getY() > curr) { + this.heightMapBlockChange[index] = pos.getY(); + } + } + + // note: light sets are delayed while processing skylight source changes due to how + // nibbles are initialised, as we want to avoid clobbering nibble values so what when + // below nibbles are initialised they aren't reading from partially modified nibbles + + // now we can recalculate the sources for the changed columns + for (int index = 0; index < (16 * 16); ++index) { + final int maxY = this.heightMapBlockChange[index]; + if (maxY == Integer.MIN_VALUE) { + // not changed + continue; + } + this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller + + final int columnX = (index & 15) | (chunkX << 4); + final int columnZ = (index >>> 4) | (chunkZ << 4); + + // try and propagate from the above y + // delay light set until after processing all sources to setup + final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true); + + // maxPropagationY is now the highest block that could not be propagated to + + // remove all sources below that are 15 + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; + final int encodeOffset = this.coordinateOffset; + + if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true); + + for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) { + if ((currY & 15) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true); + } + + // ensure section below is always checked + final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4); + if (nibble == null) { + // advance currY to the the top of the section below + currY = (currY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + continue; + } + + if (nibble.getUpdating(columnX, currY, columnZ) != 15) { + break; + } + + // delay light set until after processing all sources to setup + this.appendToDecreaseQueue( + ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + // do not set transparent blocks for the same reason we don't in the checkBlock method + ); + } + } + } + + // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads + // immediate light value + this.processDelayedIncreases(); + this.processDelayedDecreases(); + + for (final BlockPos pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected final int[] heightMapGen = new int[32 * 32]; + + @Override + protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { + this.rewriteNibbleCacheForSkylight(chunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final BlockGetter world = lightAccess.getLevel(); + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + final LevelChunkSection[] sections = chunk.getSections(); + + int highestNonEmptySection = this.maxSection; + while (highestNonEmptySection == (this.minSection - 1) || + sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].isEmpty()) { + this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false); + // try propagate FULL to neighbours + + // check neighbours to see if we need to propagate into them + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourX = chunkX + direction.x; + final int neighbourZ = chunkZ + direction.z; + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ); + if (neighbourNibble == null) { + // unloaded neighbour + // most of the time we fall here + continue; + } + + // it looks like we need to propagate into the neighbour + + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (direction.x != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (direction.z < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction + + for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY) + ); + } + } + } + + if (highestNonEmptySection-- == (this.minSection - 1)) { + break; + } + } + + if (highestNonEmptySection >= this.minSection) { + // fill out our other sources + final int minX = chunkPos.x << 4; + final int maxX = chunkPos.x << 4 | 15; + final int minZ = chunkPos.z << 4; + final int maxZ = chunkPos.z << 4 | 15; + final int startY = highestNonEmptySection << 4 | 15; + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false); + } + } + } // else: apparently the chunk is empty + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + // no need to rewrite the nibble cache again + super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + } else { + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + + this.performLightIncrease(lightAccess); + } + } + + protected final void processDelayedIncreases() { + // copied from performLightIncrease + final long[] queue = this.increaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + + this.setLightLevel(posX, posY, posZ, propagatedLightLevel); + } + } + + protected final void processDelayedDecreases() { + // copied from performLightDecrease + final long[] queue = this.decreaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + + this.setLightLevel(posX, posY, posZ, 0); + } + } + + // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays + // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so + // clobbering the light values will result in broken propagation) + protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ, + final boolean extrudeInitialised, final boolean delayLightSet) { + final BlockPos.MutableBlockPos mutablePos = this.mutablePos3; + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. + + if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) { + return startY; + } + + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + + BlockState above = this.getBlockState(worldX, startY + 1, worldZ); + if (above == null) { + above = AIR_BLOCK_STATE; + } + + for (;startY >= (this.minLightSection << 4); --startY) { + if ((startY & 15) == 15) { + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + } + BlockState current = this.getBlockState(worldX, startY, worldZ); + if (current == null) { + current = AIR_BLOCK_STATE; + } + + final VoxelShape fromShape; + if (above.isConditionallyFullOpaque()) { + this.mutablePos2.set(worldX, startY + 1, worldZ); + fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); + if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + // above wont let us propagate + break; + } + } else { + fromShape = Shapes.empty(); + } + + final int opacityIfCached = current.getOpacityIfCached(); + // does light propagate from the top down? + if (opacityIfCached != -1) { + if (opacityIfCached != 0) { + // we cannot propagate 15 through this + break; + } + // most of the time it falls here. + // add to propagate + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + ); + } else { + mutablePos.set(worldX, startY, worldZ); + long flags = 0L; + if (current.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + // can't propagate here, we're done on this column. + break; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = current.getLightBlock(world, mutablePos); + if (opacity > 0) { + // let the queued value (if any) handle it from here. + break; + } + + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + | flags + ); + } + + above = current; + + if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { + // we skip empty sections here, as this is just an easy way of making sure the above block + // can propagate through air. + + // nothing can propagate in null sections, remove the queue entry for it + --this.increaseQueueInitialLength; + + // advance currY to the the top of the section below + startY = (startY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + + // make sure this is marked as AIR + above = AIR_BLOCK_STATE; + } else if (!delayLightSet) { + this.setLightLevel(worldX, startY, worldZ, 15); + } + } + + return startY; + } +} diff --git a/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..319e5c674f027e2e06322bb75b38acd46b51cc7a --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java @@ -0,0 +1,1577 @@ +package ca.spottedleaf.starlight.light; + +import io.papermc.paper.util.CoordinateUtils; +import io.papermc.paper.util.IntegerUtil; +import io.papermc.paper.util.WorldUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.*; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public abstract class StarLightEngine { + + protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); + + protected static final LevelChunkSection EMPTY_CHUNK_SECTION = new LevelChunkSection(0); + + protected static final AxisDirection[] DIRECTIONS = AxisDirection.values(); + protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS; + protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] { + AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X, + AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z + }; + + protected static enum AxisDirection { + + // Declaration order is important and relied upon. Do not change without modifying propagation code. + POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0), + POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1), + POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0); + + static { + POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; + POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z; + POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y; + } + + protected AxisDirection opposite; + + public final int x; + public final int y; + public final int z; + public final Direction nms; + public final long everythingButThisDirection; + public final long everythingButTheOppositeDirection; + + AxisDirection(final int x, final int y, final int z) { + this.x = x; + this.y = y; + this.z = z; + this.nms = Direction.fromNormal(x, y, z); + this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); + // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. + this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); + } + + public AxisDirection getOpposite() { + return this.opposite; + } + } + + // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1 + // for explaining how light propagates via breadth-first search + + // While the above is a good start to understanding the general idea of what the general principles are, it's not + // exactly how the vanilla light engine should behave for minecraft. + + // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2] + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + // null index indicates the chunk section doesn't exist (empty or out of bounds) + protected final LevelChunkSection[] sectionCache; + + // the exact same as above, except for storing fast access to SWMRNibbleArray + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final SWMRNibbleArray[] nibbleCache; + + // the exact same as above, except for storing fast access to nibbles to call change callbacks for + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final boolean[] notifyUpdateCache; + + // always initialsed during start of lighting. + // index = x + (z * 5) + protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5]; + + // index = x + (z * 5) + protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; + + protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos(); + protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos(); + + protected int encodeOffsetX; + protected int encodeOffsetY; + protected int encodeOffsetZ; + + protected int coordinateOffset; + + protected int chunkOffsetX; + protected int chunkOffsetY; + protected int chunkOffsetZ; + + protected int chunkIndexOffset; + protected int chunkSectionIndexOffset; + + protected final boolean skylightPropagator; + protected final int emittedLightMask; + protected final boolean isClientSide; + + protected final Level world; + protected final int minLightSection; + protected final int maxLightSection; + protected final int minSection; + protected final int maxSection; + + protected StarLightEngine(final boolean skylightPropagator, final Level world) { + this.skylightPropagator = skylightPropagator; + this.emittedLightMask = skylightPropagator ? 0 : 0xF; + this.isClientSide = world.isClientSide; + this.world = world; + this.minLightSection = WorldUtil.getMinLightSection(world); + this.maxLightSection = WorldUtil.getMaxLightSection(world); + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + + this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + } + + protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) { + // 31 = center + encodeOffset + this.encodeOffsetX = 31 - centerX; + this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value + this.encodeOffsetZ = 31 - centerZ; + + // coordinateIndex = x | (z << 6) | (y << 12) + this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12); + + // 2 = (centerX >> 4) + chunkOffset + this.chunkOffsetX = 2 - (centerX >> 4); + this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0 + this.chunkOffsetZ = 2 - (centerZ >> 4); + + // chunk index = x + (5 * z) + this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ); + + // chunk section index = x + (5 * z) + ((5*5) * y) + this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY); + } + + protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ, + final boolean relaxed, final boolean tryToLoadChunksFor2Radius) { + final int centerChunkX = centerX >> 4; + final int centerChunkY = centerY >> 4; + final int centerChunkZ = centerZ >> 4; + + this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7); + + final int radius = tryToLoadChunksFor2Radius ? 2 : 1; + + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + final int cx = centerChunkX + dx; + final int cz = centerChunkZ + dz; + final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2; + final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz); + + if (chunk == null) { + if (relaxed | isTwoRadius) { + continue; + } + throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready"); + } + + if (!this.canUseChunk(chunk)) { + continue; + } + + this.setChunkInCache(cx, cz, chunk); + this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk)); + if (!isTwoRadius) { + this.setBlocksForChunkInCache(cx, cz, chunk.getSections()); + this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk)); + } + } + } + } + + protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) { + return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) { + this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; + } + + protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) { + return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) { + this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; + } + + protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setChunkSectionInCache(chunkX, cy, chunkZ, + sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? (sections[cy - this.minSection] == null || sections[cy - this.minSection].isEmpty() ? EMPTY_CHUNK_SECTION : sections[cy - this.minSection]) : EMPTY_CHUNK_SECTION)); + } + } + + protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) { + return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1]; + + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset]; + } + + return ret; + } + + protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) { + this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble; + } + + protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]); + } + } + + protected final void updateVisible(final LightChunkGetter lightAccess) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) { + continue; + } + + final int chunkX = (index % 5) - this.chunkOffsetX; + final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ; + final int chunkY = ((index / (5*5)) % (16 + 2 + 2)) - this.chunkOffsetY; + if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) { + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ)); + } + } + } + + protected final void destroyCaches() { + Arrays.fill(this.sectionCache, null); + Arrays.fill(this.nibbleCache, null); + Arrays.fill(this.chunkCache, null); + Arrays.fill(this.emptinessMapCache, null); + if (this.isClientSide) { + Arrays.fill(this.notifyUpdateCache, false); + } + } + + protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) { + final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + if (section != null) { + return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15); + } + + return null; + } + + protected final BlockState getBlockState(final int sectionIndex, final int localIndex) { + final LevelChunkSection section = this.sectionCache[sectionIndex]; + + if (section != null) { + return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.states.get(localIndex); + } + + return null; + } + + protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) { + final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8)); + } + + protected final int getLightLevel(final int sectionIndex, final int localIndex) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + return nibble == null ? 0 : nibble.getUpdating(localIndex); + } + + protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) { + final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset; + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) { + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + + protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set(localIndex, level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) { + return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) { + this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap; + } + + protected final long getKnownTransparency(final int worldX, final int worldY, final int worldZ) { + throw new UnsupportedOperationException(); // :( + } + + // warn: localIndex = y | (x << 4) | (z << 8) + protected final long getKnownTransparency(final int sectionIndex, final int localIndex) { + throw new UnsupportedOperationException(); // :( + } + + public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) { + return getFilledEmptyLight(WorldUtil.getTotalLightSections(world)); + } + + private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections]; + + for (int i = 0, len = ret.length; i < len; ++i) { + ret[i] = new SWMRNibbleArray(null, true); + } + + return ret; + } + + protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk); + + protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to); + + protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk); + + protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to); + + protected abstract boolean canUseChunk(final ChunkAccess chunk); + + public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, + final Set positions, final Boolean[] changedSections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + if (changedSections != null) { + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + } + if (!positions.isEmpty()) { + this.propagateBlockChanges(lightAccess, chunk, positions); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions); + + protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ); + + // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual) + // if ret == expect, then expect is the correct light value for pos + // if ret < expect, then ret is the real light value + protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect); + + protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; + protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; + + protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk, + final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (currNibble == null) { + return; + } + + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + chunkY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null) { + continue; + } + + if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) { + // both are zero, nothing to check. + continue; + } + + // this chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + int centerDelayedChecks = 0; + int neighbourDelayedChecks = 0; + for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int neighbourX = currX + neighbourOffX; + final int neighbourZ = currZ + neighbourOffZ; + + final int currentIndex = (currX & 15) | + ((currZ & 15)) << 4 | + ((currY & 15) << 8); + final int currentLevel = currNibble.getUpdating(currentIndex); + + final int neighbourIndex = + (neighbourX & 15) | + ((neighbourZ & 15)) << 4 | + ((currY & 15) << 8); + final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex); + + // the checks are delayed because the checkBlock method clobbers light values - which then + // affect later calculate light value operations. While they don't affect it in a behaviourly significant + // way, they do have a negative performance impact due to simply queueing more values + + if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) { + this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; + } + + if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) { + this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex; + } + } + } + + final int currentChunkOffX = chunkX << 4; + final int currentChunkOffZ = chunkZ << 4; + final int neighbourChunkOffX = (chunkX + direction.x) << 4; + final int neighbourChunkOffZ = (chunkZ + direction.z) << 4; + final int chunkOffY = chunkY << 4; + for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) { + // try to queue neighbouring data together + // index = x | (z << 4) | (y << 8) + if (i < centerDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesCenter[i]; + this.checkBlock(lightAccess, currentChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + currentChunkOffZ | ((value >>> 4) & 0xF)); + } + if (i < neighbourDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesNeighbour[i]; + this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + neighbourChunkOffZ | ((value >>> 4) & 0xF)); + } + } + } + } + + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // verifies that light levels on this chunks edges are consistent with this chunk's neighbours + // edges. if they are not, they are decreased (effectively performing the logic in checkBlock). + // This does not resolve skylight source problems. + protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // pulls light from neighbours, and adds them into the increase queue. does not actually propagate. + protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkPos chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ); + if (currNibble == null) { + continue; + } + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + currSectionY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) { + // can't pull from 0 + continue; + } + + // neighbour chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = (chunkX << 4) - 1; + } else { + startX = (chunkX << 4) + 16; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = (chunkZ << 4) - 1; + } else { + startZ = (chunkZ << 4) + 16; + } + startX = chunkX << 4; + } + + final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk + final int encodeOffset = this.coordinateOffset; + + for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int level = neighbourNibble.getUpdating( + (currX & 15) + | ((currZ & 15) << 4) + | ((currY & 15) << 8) + ); + + if (level <= 1) { + // nothing to propagate + continue; + } + + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((level & 0xFL) << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check. + ); + } + } + } + } + } + + public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) { + final LevelChunkSection[] sections = chunk.getSections(); + final Boolean[] ret = new Boolean[sections.length]; + + for (int i = 0; i < sections.length; ++i) { + if (sections[i] == null || sections[i].isEmpty()) { + ret[i] = Boolean.TRUE; + } else { + ret[i] = Boolean.FALSE; + } + } + + return ret; + } + + public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk)); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, + final Boolean[] emptinessChanges) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles); + + protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ); + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // subclasses are guaranteed that this is always called before a changed block set + // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks + // rets non-null when the emptiness map changed and needs to be updated + protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, + final Boolean[] emptinessChanges, final boolean unlit) { + final Level world = (Level)lightAccess.getLevel(); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + + boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ); + boolean[] ret = null; + final boolean needsInit = unlit || chunkEmptinessMap == null; + if (needsInit) { + this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]); + } + + // update emptiness map + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + final Boolean valueBoxed = emptinessChanges[sectionIndex]; + if (valueBoxed == null) { + if (needsInit) { + throw new IllegalStateException("Current chunk has not initialised emptiness map yet supplied emptiness map isn't filled?"); + } + continue; + } + chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue(); + } + + // now init neighbour nibbles + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + final Boolean valueBoxed = emptinessChanges[sectionIndex]; + final int sectionY = sectionIndex + this.minSection; + if (valueBoxed == null) { + continue; + } + + final boolean empty = valueBoxed.booleanValue(); + + if (empty) { + continue; + } + + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // if we're not empty, we also need to initialise nibbles + // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up + final boolean extrude = (dx | dz) != 0 || !unlit; + for (int dy = 1; dy >= -1; --dy) { + this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + // check for de-init and lazy-init + // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running + // init checks. + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // does this neighbour have 1 radius loaded? + boolean neighboursLoaded = true; + neighbour_loaded_search: + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) { + neighboursLoaded = false; + break neighbour_loaded_search; + } + } + } + + for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) { + // check neighbours to see if we need to de-init this one + boolean allEmpty = true; + neighbour_search: + for (int dy2 = -1; dy2 <= 1; ++dy2) { + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int y = sectionY + dy2; + if (y < this.minSection || y > this.maxSection) { + // empty + continue; + } + final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ); + if (emptinessMap != null) { + if (!emptinessMap[y - this.minSection]) { + allEmpty = false; + break neighbour_search; + } + } else { + final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); + if (section != null && section != EMPTY_CHUNK_SECTION) { + allEmpty = false; + break neighbour_search; + } + } + } + } + } + + if (allEmpty & neighboursLoaded) { + // can only de-init when neighbours are loaded + // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting + // to be correct + + // all were empty, so de-init + this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ); + } else if (!allEmpty) { + // must init + final boolean extrude = (dx | dz) != 0 || !unlit; + this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + return ret; + } + + public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, sections); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current + // chunks light values with respect to neighbours + // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function + // does not need to detect empty chunks itself (and it should do no handling for them either!) + protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks); + + public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + + try { + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1); + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.lightChunk(lightAccess, chunk, true); + this.setNibbles(chunk, nibbles); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void relightChunks(final LightChunkGetter lightAccess, final Set chunks, + final Consumer chunkLightCallback, final IntConsumer onComplete) { + // it's recommended for maximum performance that the set is ordered according to a BFS from the center of + // the region of chunks to relight + // it's required that tickets are added for each chunk to keep them loaded + final Long2ObjectOpenHashMap nibblesByChunk = new Long2ObjectOpenHashMap<>(); + final Long2ObjectOpenHashMap emptinessMapByChunk = new Long2ObjectOpenHashMap<>(); + + final int[] neighbourLightOrder = new int[] { + // d = 0 + 0, 0, + // d = 1 + -1, 0, + 0, -1, + 1, 0, + 0, 1, + // d = 2 + -1, 1, + 1, 1, + -1, -1, + 1, -1, + }; + + int lightCalls = 0; + + for (final ChunkPos chunkPos : chunks) { + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ); + if (chunk == null || !this.canUseChunk(chunk)) { + throw new IllegalStateException(); + } + + for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) { + final int dx = neighbourLightOrder[i]; + final int dz = neighbourLightOrder[i + 1]; + final int neighbourX = dx + chunkX; + final int neighbourZ = dz + chunkZ; + + final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ); + if (neighbour == null || !this.canUseChunk(neighbour)) { + continue; + } + + if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) { + // lit already called for neighbour, no need to light it now + continue; + } + + // light neighbour chunk + this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7); + try { + // insert all neighbouring chunks for this neighbour that we have data for + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int neighbourX2 = neighbourX + dx2; + final int neighbourZ2 = neighbourZ + dz2; + final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2); + final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2); + if (neighbour2 == null || !this.canUseChunk(neighbour2)) { + continue; + } + + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key); + if (nibbles == null) { + // we haven't lit this chunk + continue; + } + + this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2); + this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections()); + this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles); + this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key)); + } + } + + final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + // now insert the neighbour chunk and light it + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world); + nibblesByChunk.put(key, nibbles); + + this.setChunkInCache(neighbourX, neighbourZ, neighbour); + this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections()); + this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles); + + final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true); + emptinessMapByChunk.put(key, neighbourEmptiness); + if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) { + this.setEmptinessMap(neighbour, neighbourEmptiness); + } + + this.lightChunk(lightAccess, neighbour, false); + } finally { + this.destroyCaches(); + } + } + + // done lighting all neighbours, so the chunk is now fully lit + + // make sure nibbles are fully updated before calling back + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + for (final SWMRNibbleArray nibble : nibbles) { + nibble.updateVisible(); + } + + this.setNibbles(chunk, nibbles); + + for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { + lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX)); + } + + // now do callback + if (chunkLightCallback != null) { + chunkLightCallback.accept(chunkPos); + } + ++lightCalls; + } + + if (onComplete != null) { + onComplete.accept(lightCalls); + } + } + + // contains: + // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6)))) + // next 4 bits: propagated light level (0, 15] + // next 6 bits: propagation direction bitset + // next 24 bits: unused + // last 3 bits: state flags + // state flags: + // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light + // updates for block sources + protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2; + // whether the propagation needs to check if its current level is equal to the expected level + // used only in increase propagation + protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1; + // whether the propagation needs to consider if its block is conditionally transparent + protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE; + + protected long[] increaseQueue = new long[16 * 16 * 16]; + protected int increaseQueueInitialLength; + protected long[] decreaseQueue = new long[16 * 16 * 16]; + protected int decreaseQueueInitialLength; + + protected final long[] resizeIncreaseQueue() { + return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); + } + + protected final long[] resizeDecreaseQueue() { + return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); + } + + protected final void appendToIncreaseQueue(final long value) { + final int idx = this.increaseQueueInitialLength++; + long[] queue = this.increaseQueue; + if (idx >= queue.length) { + queue = this.resizeIncreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected final void appendToDecreaseQueue(final long value) { + final int idx = this.decreaseQueueInitialLength++; + long[] queue = this.decreaseQueue; + if (idx >= queue.length) { + queue = this.resizeDecreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][]; + protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1; + static { + for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) { + final List directions = new ArrayList<>(); + for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) { + directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]); + } + OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]); + } + } + + protected final void performLightIncrease(final LightChunkGetter lightAccess) { + final BlockGetter world = lightAccess.getLevel(); + long[] queue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.increaseQueueInitialLength; + this.increaseQueueInitialLength = 0; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)]; + + if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { + if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) { + // not at the level we expect, so something changed. + continue; + } + } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) { + // these are used to restore block sources after a propagation decrease + this.setLightLevel(posX, posY, posZ, propagatedLightLevel); + } + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want or unloaded + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); + if (targetLevel > currentLevel) { + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + continue; + } + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); + this.mutablePos2.set(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; + } + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); + if (targetLevel > currentLevel) { + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + continue; + } + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } + } + } + + protected final void performLightDecrease(final LightChunkGetter lightAccess) { + final BlockGetter world = lightAccess.getLevel(); + long[] queue = this.decreaseQueue; + long[] increaseQueue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.decreaseQueueInitialLength; + this.decreaseQueueInitialLength = 0; + int increaseQueueLength = this.increaseQueueInitialLength; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + final int emittedMask = this.emittedLightMask; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)]; + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_RECHECK_LEVEL; + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + continue; + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (flags | FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } else { + // we actually need to worry about our state here + final BlockState fromBlock = this.getBlockState(posX, posY, posZ); + this.mutablePos2.set(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty(); + + if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) { + continue; + } + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final BlockState blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_RECHECK_LEVEL; + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + continue; + } + continue; + } else { + this.mutablePos1.set(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms); + + if (Shapes.faceShapeOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getLightBlock(world, this.mutablePos1); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = blockState.getLightEmission() & emittedMask; + if (emittedLight != 0) { + // re-propagate source + // note: do not set recheck level, or else the propagation will fail + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (flags | FLAG_WRITE_LEVEL); + } + + currentNibble.set(localIndex, 0); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } + } + + // propagate sources we clobbered + this.increaseQueueInitialLength = increaseQueueLength; + this.performLightIncrease(lightAccess); + } +} diff --git a/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..9b8412f50d161471166cdf5c9effc2d58915faa1 --- /dev/null +++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java @@ -0,0 +1,635 @@ +package ca.spottedleaf.starlight.light; + +import io.papermc.paper.util.CoordinateUtils; +import io.papermc.paper.util.WorldUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.lighting.LayerLightEventListener; +import net.minecraft.world.level.lighting.LevelLightEngine; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public final class StarLightInterface { + + public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong())); + + /** + * Can be {@code null}, indicating the light is all empty. + */ + protected final Level world; + protected final LightChunkGetter lightAccess; + + protected final ArrayDeque cachedSkyPropagators; + protected final ArrayDeque cachedBlockPropagators; + + protected final LightQueue lightQueue = new LightQueue(this); + + protected final LayerLightEventListener skyReader; + protected final LayerLightEventListener blockReader; + protected final boolean isClientSide; + + protected final int minSection; + protected final int maxSection; + protected final int minLightSection; + protected final int maxLightSection; + + public final LevelLightEngine lightEngine; + + public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { + this.lightAccess = lightAccess; + this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); + this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; + this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; + this.isClientSide = !(this.world instanceof ServerLevel); + if (this.world == null) { + this.minSection = 0; + this.maxSection = 15; + this.minLightSection = -1; + this.maxLightSection = 16; + } else { + this.minSection = WorldUtil.getMinSection(this.world); + this.maxSection = WorldUtil.getMaxSection(this.world); + this.minLightSection = WorldUtil.getMinLightSection(this.world); + this.maxLightSection = WorldUtil.getMaxLightSection(this.world); + } + this.lightEngine = lightEngine; + this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { + @Override + public void checkBlock(final BlockPos blockPos) { + StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); + } + + @Override + public void onBlockEmissionIncrease(final BlockPos blockPos, final int i) { + // skylight doesn't care + } + + @Override + public boolean hasLightWork() { + // not really correct... + return StarLightInterface.this.hasUpdates(); + } + + @Override + public int runUpdates(final int i, final boolean bl, final boolean bl2) { + throw new UnsupportedOperationException(); + } + + @Override + public void enableLightSources(final ChunkPos chunkPos, final boolean bl) { + throw new UnsupportedOperationException(); + } + + @Override + public DataLayer getDataLayerData(final SectionPos pos) { + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + return null; + } + + final int sectionY = pos.getY(); + + if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { + return null; + } + + if (chunk.getSkyEmptinessMap() == null) { + return null; + } + + return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int getLightValue(final BlockPos blockPos) { + final int x = blockPos.getX(); + int y = blockPos.getY(); + final int z = blockPos.getZ(); + + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(x >> 4, z >> 4); + if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + return 15; + } + + int sectionY = y >> 4; + + if (sectionY > StarLightInterface.this.maxLightSection) { + return 15; + } + + if (sectionY < StarLightInterface.this.minLightSection) { + sectionY = StarLightInterface.this.minLightSection; + y = sectionY << 4; + } + + final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles(); + final SWMRNibbleArray immediate = nibbles[sectionY - StarLightInterface.this.minLightSection]; + + if (StarLightInterface.this.isClientSide) { + if (!immediate.isNullNibbleUpdating()) { + return immediate.getUpdating(x, y, z); + } + } else { + if (!immediate.isNullNibbleVisible()) { + return immediate.getVisible(x, y, z); + } + } + + final boolean[] emptinessMap = chunk.getSkyEmptinessMap(); + + if (emptinessMap == null) { + return 15; + } + + // are we above this chunk's lowest empty section? + int lowestY = StarLightInterface.this.minLightSection - 1; + for (int currY = StarLightInterface.this.maxSection; currY >= StarLightInterface.this.minSection; --currY) { + if (emptinessMap[currY - StarLightInterface.this.minSection]) { + continue; + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (sectionY > lowestY) { + return 15; + } + + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = sectionY + 1; currY <= StarLightInterface.this.maxLightSection; ++currY) { + final SWMRNibbleArray nibble = nibbles[currY - StarLightInterface.this.minLightSection]; + if (StarLightInterface.this.isClientSide) { + if (!nibble.isNullNibbleUpdating()) { + return nibble.getUpdating(x, 0, z); + } + } else { + if (!nibble.isNullNibbleVisible()) { + return nibble.getVisible(x, 0, z); + } + } + } + + // should never reach here + return 15; + } + + @Override + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { + @Override + public void checkBlock(final BlockPos blockPos) { + StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); + } + + @Override + public void onBlockEmissionIncrease(final BlockPos blockPos, final int i) { + this.checkBlock(blockPos); + } + + @Override + public boolean hasLightWork() { + // not really correct... + return StarLightInterface.this.hasUpdates(); + } + + @Override + public int runUpdates(final int i, final boolean bl, final boolean bl2) { + throw new UnsupportedOperationException(); + } + + @Override + public void enableLightSources(final ChunkPos chunkPos, final boolean bl) { + throw new UnsupportedOperationException(); + } + + @Override + public DataLayer getDataLayerData(final SectionPos pos) { + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + + if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { + return null; + } + + return chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int getLightValue(final BlockPos blockPos) { + final int cx = blockPos.getX() >> 4; + final int cy = blockPos.getY() >> 4; + final int cz = blockPos.getZ() >> 4; + + if (cy < StarLightInterface.this.minLightSection || cy > StarLightInterface.this.maxLightSection) { + return 0; + } + + final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(cx, cz); + + if (chunk == null) { + return 0; + } + + final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - StarLightInterface.this.minLightSection]; + if (StarLightInterface.this.isClientSide) { + return nibble.getUpdating(blockPos.getX(), blockPos.getY(), blockPos.getZ()); + } else { + return nibble.getVisible(blockPos.getX(), blockPos.getY(), blockPos.getZ()); + } + } + + @Override + public void updateSectionStatus(final SectionPos pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + } + + public LayerLightEventListener getSkyReader() { + return this.skyReader; + } + + public LayerLightEventListener getBlockReader() { + return this.blockReader; + } + + public boolean isClientSide() { + return this.isClientSide; + } + + public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { + if (this.world == null) { + // empty world + return null; + } + return ((ServerLevel)this.world).getChunkSource().getChunkAtImmediately(chunkX, chunkZ); + } + + public boolean hasUpdates() { + return !this.lightQueue.isEmpty(); + } + + public Level getWorld() { + return this.world; + } + + public LightChunkGetter getLightAccess() { + return this.lightAccess; + } + + protected final SkyStarLightEngine getSkyLightEngine() { + if (this.cachedSkyPropagators == null) { + return null; + } + final SkyStarLightEngine ret; + synchronized (this.cachedSkyPropagators) { + ret = this.cachedSkyPropagators.pollFirst(); + } + + if (ret == null) { + return new SkyStarLightEngine(this.world); + } + return ret; + } + + protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { + if (this.cachedSkyPropagators == null) { + return; + } + synchronized (this.cachedSkyPropagators) { + this.cachedSkyPropagators.addFirst(engine); + } + } + + protected final BlockStarLightEngine getBlockLightEngine() { + if (this.cachedBlockPropagators == null) { + return null; + } + final BlockStarLightEngine ret; + synchronized (this.cachedBlockPropagators) { + ret = this.cachedBlockPropagators.pollFirst(); + } + + if (ret == null) { + return new BlockStarLightEngine(this.world); + } + return ret; + } + + protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { + if (this.cachedBlockPropagators == null) { + return; + } + synchronized (this.cachedBlockPropagators) { + this.cachedBlockPropagators.addFirst(engine); + } + } + + public CompletableFuture blockChange(final BlockPos pos) { + if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world + return null; + } + + return this.lightQueue.queueBlockChange(pos); + } + + public CompletableFuture sectionChange(final SectionPos pos, final boolean newEmptyValue) { + if (this.world == null) { // empty world + return null; + } + + return this.lightQueue.queueSectionChange(pos, newEmptyValue); + } + + public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + if (blockEngine != null) { + blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.light(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.light(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void relightChunks(final Set chunks, final Consumer chunkLightCallback, + final IntConsumer onComplete) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null, + blockEngine == null ? onComplete : null); + } + if (blockEngine != null) { + blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkChunkEdges(final int chunkX, final int chunkZ) { + this.checkSkyEdges(chunkX, chunkZ); + this.checkBlockEdges(chunkX, chunkZ); + } + + public void checkSkyEdges(final int chunkX, final int chunkZ) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { + this.lightQueue.queueChunkLighting(pos, run); + } + + public void removeChunkTasks(final ChunkPos pos) { + this.lightQueue.removeChunk(pos); + } + + public void propagateChanges() { + if (this.lightQueue.isEmpty()) { + return; + } + + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + LightQueue.ChunkTasks task; + while ((task = this.lightQueue.removeFirstTask()) != null) { + if (task.lightTasks != null) { + for (final Runnable run : task.lightTasks) { + run.run(); + } + } + + final long coordinate = task.chunkCoordinate; + final int chunkX = CoordinateUtils.getChunkX(coordinate); + final int chunkZ = CoordinateUtils.getChunkZ(coordinate); + + final Set positions = task.changedPositions; + final Boolean[] sectionChanges = task.changedSectionSet; + + if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); + } + if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { + blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); + } + + if (skyEngine != null && task.queuedEdgeChecksSky != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); + } + if (blockEngine != null && task.queuedEdgeChecksBlock != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); + } + + task.onComplete.complete(null); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + protected static final class LightQueue { + + protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); + protected final StarLightInterface manager; + + public LightQueue(final StarLightInterface manager) { + this.manager = manager; + } + + public synchronized boolean isEmpty() { + return this.chunkTasks.isEmpty(); + } + + public synchronized CompletableFuture queueBlockChange(final BlockPos pos) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + tasks.changedPositions.add(pos.immutable()); + return tasks.onComplete; + } + + public synchronized CompletableFuture queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + if (tasks.changedSectionSet == null) { + tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; + } + tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); + + return tasks.onComplete; + } + + public synchronized CompletableFuture queueChunkLighting(final ChunkPos pos, final Runnable lightTask) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + if (tasks.lightTasks == null) { + tasks.lightTasks = new ArrayList<>(); + } + tasks.lightTasks.add(lightTask); + + return tasks.onComplete; + } + + public synchronized CompletableFuture queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; + if (queuedEdges == null) { + queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); + } + queuedEdges.addAll(sections); + + return tasks.onComplete; + } + + public synchronized CompletableFuture queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { + final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); + + ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; + if (queuedEdges == null) { + queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); + } + queuedEdges.addAll(sections); + + return tasks.onComplete; + } + + public void removeChunk(final ChunkPos pos) { + final ChunkTasks tasks; + synchronized (this) { + tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); + } + if (tasks != null) { + tasks.onComplete.complete(null); + } + } + + public synchronized ChunkTasks removeFirstTask() { + if (this.chunkTasks.isEmpty()) { + return null; + } + return this.chunkTasks.removeFirst(); + } + + protected static final class ChunkTasks { + + public final Set changedPositions = new HashSet<>(); + public Boolean[] changedSectionSet; + public ShortOpenHashSet queuedEdgeChecksSky; + public ShortOpenHashSet queuedEdgeChecksBlock; + public List lightTasks; + + public final CompletableFuture onComplete = new CompletableFuture<>(); + + public final long chunkCoordinate; + + public ChunkTasks(final long chunkCoordinate) { + this.chunkCoordinate = chunkCoordinate; + } + } + } +} diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java index f436ab35798c9b6e6cb2eb60d2c02cbf9b742e69..807bbe54f6516f794bdcb735bb7b8d6812e3ef01 100644 --- a/src/main/java/com/destroystokyo/paper/PaperCommand.java +++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java @@ -501,6 +501,46 @@ public class PaperCommand extends Command { } } + // Paper start - rewrite light engine + private void starlightFixLight(ServerPlayer sender, ServerLevel world, ThreadedLevelLightEngine lightengine, int radius) { + long start = System.nanoTime(); + java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos + + int[] pending = new int[1]; + for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { + final ChunkPos chunkPos = iterator.next(); + + final net.minecraft.world.level.chunk.ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z); + if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + continue; + } + + ++pending[0]; + } + + int[] relitChunks = new int[1]; + lightengine.relight(chunks, + (ChunkPos chunkPos) -> { + ++relitChunks[0]; + sender.getBukkitEntity().sendMessage( + ChatColor.BLUE + "Relit chunk " + ChatColor.DARK_AQUA + chunkPos + ChatColor.BLUE + + ", progress: " + ChatColor.DARK_AQUA + (int)(Math.round(100.0 * (double)(relitChunks[0])/(double)pending[0])) + "%" + ); + }, + (int totalRelit) -> { + final long end = System.nanoTime(); + final long diff = Math.round(1.0e-6*(end - start)); + sender.getBukkitEntity().sendMessage( + ChatColor.BLUE + "Relit " + ChatColor.DARK_AQUA + totalRelit + ChatColor.BLUE + " chunks. Took " + + ChatColor.DARK_AQUA + diff + "ms" + ); + }); + sender.getBukkitEntity().sendMessage(ChatColor.BLUE + "Relighting " + ChatColor.DARK_AQUA + pending[0] + ChatColor.BLUE + " chunks"); + } + // Paper end - rewrite light engine + private void doFixLight(CommandSender sender, String[] args) { if (!(sender instanceof Player)) { sender.sendMessage("Only players can use this command"); @@ -509,7 +549,7 @@ public class PaperCommand extends Command { int radius = 2; if (args.length > 1) { try { - radius = Math.min(5, Integer.parseInt(args[1])); + radius = Math.min(32, Integer.parseInt(args[1])); // Paper - MOOOOOORE } catch (Exception e) { sender.sendMessage("Not a number"); return; @@ -522,6 +562,13 @@ public class PaperCommand extends Command { ServerLevel world = (ServerLevel) handle.level; ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); + // Paper start - rewrite light engine + if (true) { + this.starlightFixLight(handle, world, lightengine, radius); + return; + } + // Paper end - rewrite light engine + net.minecraft.core.BlockPos center = MCUtil.toBlockPosition(player.getLocation()); Deque queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius)); updateLight(sender, world, lightengine, queue); diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java index d8be2ad889f46491e50404916fb4ae0de5f42098..29ba097a7b230ef67d4d1b5f4ebe20cd5228d214 100644 --- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java +++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java @@ -32,25 +32,17 @@ public class ClientboundLightUpdatePacket implements Packet { - if (remainingSends.get() == 0) { - cleaner1.run(); - cleaner2.run(); - } - }, "Light Packet Release"); - } + // Paper - rewrite light engine } @Override public boolean hasFinishListener() { - return true; + return false; // Paper - rewrite light engine } // Paper end @@ -63,8 +55,8 @@ public class ClientboundLightUpdatePacket implements Packet> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage private volatile CompletableFuture> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage private volatile CompletableFuture> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage - private CompletableFuture chunkToSave; + public CompletableFuture chunkToSave; // Paper - public @Nullable private final DebugBuffer chunkToSaveHistory; public int oldTicketLevel; diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 410d3d840e4858dd2a4f0ac9c61ff9bb98d57c1c..6cf01d98d5d7a41f743527381d496c0d01421928 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -121,7 +121,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final LongSet entitiesInLevel; public final ServerLevel level; private final ThreadedLevelLightEngine lightEngine; - private final BlockableEventLoop mainThreadExecutor; + public final BlockableEventLoop mainThreadExecutor; // Paper - public final java.util.concurrent.Executor mainInvokingExecutor; // Paper public final ChunkGenerator generator; public final Supplier overworldDataStorage; diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java index 833b6b2193cf08e123aabb344f2283730aed1bcd..136ca55ac38b709e086f8e2e68dc1d0db2670ae4 100644 --- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java +++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java @@ -25,6 +25,17 @@ import net.minecraft.world.level.lighting.LevelLightEngine; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +// Paper start +import ca.spottedleaf.starlight.light.StarLightEngine; +import io.papermc.paper.util.CoordinateUtils; +import java.util.function.Supplier; +import net.minecraft.world.level.lighting.LayerLightEventListener; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongIterator; +import net.minecraft.world.level.chunk.ChunkStatus; +// Paper end + public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable { private static final Logger LOGGER = LogManager.getLogger(); private final ProcessorMailbox taskMailbox; @@ -159,13 +170,166 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl private volatile int taskPerBatch = 5; private final AtomicBoolean scheduled = new AtomicBoolean(); + // Paper start - replace light engine impl + protected final ca.spottedleaf.starlight.light.StarLightInterface theLightEngine; + public final boolean hasBlockLight; + public final boolean hasSkyLight; + // Paper end - replace light engine impl + public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox processor, ProcessorHandle> executor) { - super(chunkProvider, true, hasBlockLight); + super(chunkProvider, false, false); // Paper - destroy vanilla light engine state this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper this.sorterMailbox = executor; this.taskMailbox = processor; + // Paper start - replace light engine impl + this.hasBlockLight = true; + this.hasSkyLight = hasBlockLight; // Nice variable name. + this.theLightEngine = new ca.spottedleaf.starlight.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this); + // Paper end - replace light engine impl + } + + // Paper start - replace light engine impl + protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) { + return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ); + } + + protected long relightCounter; + + public int relight(java.util.Set chunks_param, + java.util.function.Consumer chunkLightCallback, + java.util.function.IntConsumer onComplete) { + if (!org.bukkit.Bukkit.isPrimaryThread()) { + throw new IllegalStateException("Must only be called on the main thread"); + } + + java.util.Set chunks = new java.util.LinkedHashSet<>(chunks_param); + // add tickets + java.util.Map ticketIds = new java.util.HashMap<>(); + int totalChunks = 0; + for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { + final ChunkPos chunkPos = iterator.next(); + + final ChunkAccess chunk = ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z); + if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + continue; + } + + final Long id = Long.valueOf(this.relightCounter++); + + ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id); + ticketIds.put(chunkPos, id); + + ++totalChunks; + } + + this.taskMailbox.tell(() -> { + this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> { + chunkLightCallback.accept(chunkPos); + ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> { + ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false); + ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); + }); + }, onComplete); + }); + this.tryScheduleUpdate(); + + return totalChunks; + } + + private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); + + private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier> runnable) { + final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld(); + + final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ); + if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) { + // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing + // chunk scheduling, we could be lighting and generating a chunk at the same time + return; + } + + if (center.getStatus() != ChunkStatus.FULL) { + // do not keep chunk loaded, we are probably in a gen thread + // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) + runnable.get(); + return; + } + + if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { + // ticket logic is not safe to run off-main, re-schedule + world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { + this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); + }); + return; + } + + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + + final CompletableFuture updateFuture = runnable.get(); + + if (updateFuture == null) { + // not scheduled + return; + } + + final int references = this.chunksBeingWorkedOn.addTo(key, 1); + if (references == 0) { + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); + } + + // append future to this chunk and 1 radius neighbours chunk save futures + // this prevents us from saving the world without first waiting for the light engine + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + ChunkHolder neighbour = world.getChunkSource().chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ)); + if (neighbour != null) { + neighbour.chunkToSave = neighbour.chunkToSave.thenCombine(updateFuture, (final ChunkAccess curr, final Void ignore) -> { + return curr; + }); + } + } + } + + updateFuture.thenAcceptAsync((final Void ignore) -> { + final int newReferences = this.chunksBeingWorkedOn.get(key); + if (newReferences == 1) { + this.chunksBeingWorkedOn.remove(key); + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); + } else { + this.chunksBeingWorkedOn.put(key, newReferences - 1); + } + }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { + if (thr != null) { + LOGGER.fatal("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); + } + }); + } + + @Override + public boolean hasLightWork() { + // route to new light engine + return this.theLightEngine.hasUpdates() || !this.queue.isEmpty(); } + @Override + public LayerLightEventListener getLayerListener(final LightLayer lightType) { + return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader(); + } + + @Override + public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { + // need to use new light hooks for this + final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness; + final int block = this.theLightEngine.getBlockReader().getLightValue(pos); + return Math.max(sky, block); + } + // Paper end - replace light engine impl + @Override public void close() { } @@ -182,15 +346,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl @Override public void checkBlock(BlockPos pos) { - BlockPos blockPos = pos.immutable(); - this.addTask(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ThreadedLevelLightEngine.TaskType.POST_UPDATE, Util.name(() -> { - super.checkBlock(blockPos); - }, () -> { - return "checkBlock " + blockPos; - })); + // Paper start - replace light engine impl + final BlockPos posCopy = pos.immutable(); + this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { + return this.theLightEngine.blockChange(posCopy); + }); + // Paper end - replace light engine impl } protected void updateChunkStatus(ChunkPos pos) { + if (true) return; // Paper - replace light engine impl this.addTask(pos.x, pos.z, () -> { return 0; }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { @@ -213,17 +378,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl @Override public void updateSectionStatus(SectionPos pos, boolean notReady) { - this.addTask(pos.x(), pos.z(), () -> { - return 0; - }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { - super.updateSectionStatus(pos, notReady); - }, () -> { - return "updateSectionStatus " + pos + " " + notReady; - })); + // Paper start - replace light engine impl + this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { + return this.theLightEngine.sectionChange(pos, notReady); + }); + // Paper end - replace light engine impl } @Override public void enableLightSources(ChunkPos chunkPos, boolean bl) { + if (true) return; // Paper - replace light engine impl this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { super.enableLightSources(chunkPos, bl); }, () -> { @@ -233,6 +397,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl @Override public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles, boolean bl) { + if (true) return; // Paper - replace light engine impl this.addTask(pos.x(), pos.z(), () -> { return 0; }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { @@ -254,6 +419,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl @Override public void retainData(ChunkPos pos, boolean retainData) { + if (true) return; // Paper - replace light engine impl this.addTask(pos.x, pos.z, () -> { return 0; }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { @@ -264,6 +430,37 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl } public CompletableFuture lightChunk(ChunkAccess chunk, boolean excludeBlocks) { + // Paper start - replace light engine impl + if (true) { + boolean lit = excludeBlocks; + final ChunkPos chunkPos = chunk.getPos(); + + return CompletableFuture.supplyAsync(() -> { + final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); + if (!lit) { + chunk.setLightCorrect(false); + this.theLightEngine.lightChunk(chunk, emptySections); + chunk.setLightCorrect(true); + } else { + this.theLightEngine.forceLoadInChunk(chunk, emptySections); + // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have + // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should + // catch what we miss here. + this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z); + } + + this.chunkMap.releaseLightTicket(chunkPos); + return chunk; + }, (runnable) -> { + this.theLightEngine.scheduleChunkLight(chunkPos, runnable); + this.tryScheduleUpdate(); + }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { + if (throwable != null) { + LOGGER.fatal("Failed to light chunk " + chunkPos, throwable); + } + }); + } + // Paper end - replace light engine impl ChunkPos chunkPos = chunk.getPos(); // Paper start //ichunkaccess.b(false); // Don't need to disable this @@ -306,7 +503,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl } public void tryScheduleUpdate() { - if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper + if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine this.taskMailbox.tell(() -> { this.runUpdate(); this.scheduled.set(false); @@ -323,12 +520,12 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl if (queue.poll(pre, post)) { pre.forEach(Runnable::run); pre.clear(); - super.runUpdates(Integer.MAX_VALUE, true, true); + this.theLightEngine.propagateChanges(); // Paper - rewrite light engine post.forEach(Runnable::run); post.clear(); } else { // might have level updates to go still - super.runUpdates(Integer.MAX_VALUE, true, true); + this.theLightEngine.propagateChanges(); // Paper - rewrite light engine } // Paper end } diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java index 41ddcf6775f99c56cf4b13b284420061e5dd6bdc..ae46429264e6a7e5c88b6b6a41a6df4db7b3e70d 100644 --- a/src/main/java/net/minecraft/server/level/TicketType.java +++ b/src/main/java/net/minecraft/server/level/TicketType.java @@ -32,6 +32,7 @@ public class TicketType { public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit public static final TicketType DELAY_UNLOAD = create("delay_unload", Long::compareTo, 300); // Paper public static final TicketType REQUIRED_LOAD = create("required_load", Long::compareTo); // Paper - make sure getChunkAt does not fail + public static final TicketType CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting public static TicketType create(String name, Comparator argumentComparator) { return new TicketType<>(name, argumentComparator, 0L); diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java index b534fd9e5d2a17926282cf40c9d66a2143a37bfe..4a7fdea6a5f966db444dc41f7215faa99e3820b3 100644 --- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java +++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java @@ -643,6 +643,7 @@ public abstract class BlockBehaviour { this.isViewBlocking = blockbase_info.isViewBlocking; this.hasPostProcess = blockbase_info.hasPostProcess; this.emissiveRendering = blockbase_info.emissiveRendering; + this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Paper } // Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; @@ -663,6 +664,18 @@ public abstract class BlockBehaviour { protected boolean isTicking; protected FluidState fluid; // Paper end + // Paper start + protected int opacityIfCached = -1; + // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15] + public final int getOpacityIfCached() { + return this.opacityIfCached; + } + + protected final boolean conditionallyFullOpaque; + public final boolean isConditionallyFullOpaque() { + return this.conditionallyFullOpaque; + } + // Paper end public void initCache() { this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid() @@ -671,6 +684,7 @@ public abstract class BlockBehaviour { this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState()); } this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here + this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - cache opacity for light } diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java index 8393950a0b38ec7897d7643803d5accdb1f983f3..ae2050da03ea2ed82b5b0dadbe4e9d37162e8fdb 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java @@ -41,6 +41,36 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess { net.minecraft.world.level.Level getLevel(); // Paper end + // Paper start + default ca.spottedleaf.starlight.light.SWMRNibbleArray[] getBlockNibbles() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + default void setBlockNibbles(ca.spottedleaf.starlight.light.SWMRNibbleArray[] nibbles) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + + default ca.spottedleaf.starlight.light.SWMRNibbleArray[] getSkyNibbles() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + default void setSkyNibbles(ca.spottedleaf.starlight.light.SWMRNibbleArray[] nibbles) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + public default boolean[] getSkyEmptinessMap() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + public default void setSkyEmptinessMap(final boolean[] emptinessMap) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + + public default boolean[] getBlockEmptinessMap() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + + public default void setBlockEmptinessMap(final boolean[] emptinessMap) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + // Paper end + BlockState getType(final int x, final int y, final int z); // Paper @Nullable BlockState setBlockState(BlockPos pos, BlockState state, boolean moved); diff --git a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java index c561d69b4b903cd3625468b239cb1ace3e317700..08c1be9c9735132f383b9be9fda6f8af4a502107 100644 --- a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java +++ b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java @@ -12,7 +12,7 @@ public class DataLayer { public static final int SIZE = 2048; private static final int NIBBLE_SIZE = 4; @Nullable - protected byte[] data; + protected byte[] data; public final byte[] getDataRaw() { return this.data; } // Paper - provide accessor // Paper start public static byte[] EMPTY_NIBBLE = new byte[2048]; private static final int nibbleBucketSizeMultiplier = Integer.getInteger("Paper.nibbleBucketSize", 3072); @@ -55,6 +55,7 @@ public class DataLayer { boolean poolSafe = false; public java.lang.Runnable cleaner; private void registerCleaner() { + if (true) return; // Paper - purge cleaner usage if (!poolSafe) { cleaner = net.minecraft.server.MCUtil.registerCleaner(this, this.data, DataLayer::releaseBytes); } else { @@ -69,7 +70,7 @@ public class DataLayer { } public DataLayer(byte[] bytes, boolean isSafe) { this.data = bytes; - if (!isSafe) this.data = getCloneIfSet(); // Paper - clone for safety + // Paper - purge cleaner usage registerCleaner(); // Paper end if (bytes.length != 2048) { @@ -155,7 +156,7 @@ public class DataLayer { } // Paper end public DataLayer copy() { - return this.data == null ? new DataLayer() : new DataLayer(this.data); // Paper - clone in ctor + return this.data == null ? new DataLayer() : new DataLayer(this.data.clone()); // Paper - clone in ctor // Paper - no longer clone in constructor } public String toString() { diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java index 8245c5834ec69beb8e3b95fb3900601009a9273f..5f6b611dc99cc04cd553b9e01dba19ec6d8b250e 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java @@ -1,5 +1,6 @@ package net.minecraft.world.level.chunk; +import ca.spottedleaf.starlight.light.SWMRNibbleArray; import it.unimi.dsi.fastutil.longs.LongSet; import java.util.BitSet; import java.util.Map; @@ -29,6 +30,48 @@ public class ImposterProtoChunk extends ProtoChunk { this.wrapped = wrapped; } + // Paper start - rewrite light engine + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return this.getWrapped().getBlockNibbles(); + } + + @Override + public void setBlockNibbles(SWMRNibbleArray[] nibbles) { + this.getWrapped().setBlockNibbles(nibbles); + } + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return this.getWrapped().getSkyNibbles(); + } + + @Override + public void setSkyNibbles(SWMRNibbleArray[] nibbles) { + this.getWrapped().setSkyNibbles(nibbles); + } + + @Override + public boolean[] getSkyEmptinessMap() { + return this.getWrapped().getSkyEmptinessMap(); + } + + @Override + public void setSkyEmptinessMap(boolean[] emptinessMap) { + this.getWrapped().setSkyEmptinessMap(emptinessMap); + } + + @Override + public boolean[] getBlockEmptinessMap() { + return this.getWrapped().getBlockEmptinessMap(); + } + + @Override + public void setBlockEmptinessMap(boolean[] emptinessMap) { + this.getWrapped().setBlockEmptinessMap(emptinessMap); + } + // Paper end - rewrite light engine + @Nullable @Override public BlockEntity getBlockEntity(BlockPos pos) { diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java index d580f0375cce5e995c024f1b0cd4843b5718121c..f0c43f9f636a5dd1f0dfbae604dfa1f4ff9ebd4e 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -1,5 +1,7 @@ package net.minecraft.world.level.chunk; +import ca.spottedleaf.starlight.light.SWMRNibbleArray; +import ca.spottedleaf.starlight.light.StarLightEngine; import com.google.common.collect.ImmutableList; import com.destroystokyo.paper.exception.ServerInternalException; import com.google.common.collect.Maps; @@ -17,7 +19,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; @@ -28,7 +29,6 @@ import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; import net.minecraft.core.BlockPos; -import net.minecraft.core.DefaultedRegistry; import net.minecraft.core.Registry; import net.minecraft.core.SectionPos; import net.minecraft.nbt.CompoundTag; @@ -125,11 +125,62 @@ public class LevelChunk implements ChunkAccess { private volatile boolean isLightCorrect; private final Int2ObjectMap gameEventDispatcherSections; + // Paper start - rewrite light engine + protected volatile SWMRNibbleArray[] blockNibbles; + protected volatile SWMRNibbleArray[] skyNibbles; + protected volatile boolean[] skyEmptinessMap; + protected volatile boolean[] blockEmptinessMap; + + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return this.blockNibbles; + } + + @Override + public void setBlockNibbles(SWMRNibbleArray[] nibbles) { + this.blockNibbles = nibbles; + } + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return this.skyNibbles; + } + + @Override + public void setSkyNibbles(SWMRNibbleArray[] nibbles) { + this.skyNibbles = nibbles; + } + + @Override + public boolean[] getSkyEmptinessMap() { + return this.skyEmptinessMap; + } + + @Override + public void setSkyEmptinessMap(boolean[] emptinessMap) { + this.skyEmptinessMap = emptinessMap; + } + + @Override + public boolean[] getBlockEmptinessMap() { + return this.blockEmptinessMap; + } + + @Override + public void setBlockEmptinessMap(boolean[] emptinessMap) { + this.blockEmptinessMap = emptinessMap; + } + // Paper end - rewrite light engine + public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes) { this(world, pos, biomes, UpgradeData.EMPTY, EmptyTickList.empty(), EmptyTickList.empty(), 0L, (LevelChunkSection[]) null, (Consumer) null); } public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes, UpgradeData upgradeData, TickList blockTickScheduler, TickList fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sections, @Nullable Consumer loadToWorldConsumer) { + // Paper start + this.blockNibbles = StarLightEngine.getFilledEmptyLight(world); + this.skyNibbles = StarLightEngine.getFilledEmptyLight(world); + // Paper end this.pendingBlockEntities = Maps.newHashMap(); this.tickersInLevel = Maps.newHashMap(); this.heightmaps = Maps.newEnumMap(Heightmap.Types.class); @@ -333,6 +384,12 @@ public class LevelChunk implements ChunkAccess { public LevelChunk(ServerLevel worldserver, ProtoChunk protoChunk, @Nullable Consumer consumer) { this(worldserver, protoChunk.getPos(), protoChunk.getBiomes(), protoChunk.getUpgradeData(), protoChunk.getBlockTicks(), protoChunk.getLiquidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), consumer); + // Paper start - copy over protochunk light + this.setBlockNibbles(protoChunk.getBlockNibbles()); + this.setSkyNibbles(protoChunk.getSkyNibbles()); + this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap()); + this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap()); + // Paper end - copy over protochunk light Iterator iterator = protoChunk.getBlockEntities().values().iterator(); while (iterator.hasNext()) { diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java index cdac1f7b30e4c043dcb12ac9e29af926df8170bd..c1c95ac9deb134a0cf5c7763090ac5f3cddf24cc 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java @@ -18,7 +18,7 @@ public class LevelChunkSection { short nonEmptyBlockCount; // Paper - package-private private short tickingBlockCount; private short tickingFluidCount; - final PalettedContainer states; // Paper - package-private + public final PalettedContainer states; // Paper - package-private // Paper - public // Paper start - Anti-Xray - Add parameters @Deprecated public LevelChunkSection(int yOffset) { this(yOffset, null, null, true); } // Notice for updates: Please make sure this constructor isn't used anywhere diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java index 554474d4b2e57d8a005b3c3b9b23f32a62243058..79fd7a6e8a6eb1f699d03801910d97066677311c 100644 --- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java @@ -174,7 +174,7 @@ public class PalettedContainer implements PaletteResize { return this.get(y << 8 | z << 4 | x); // Paper - inline } - protected T get(int index) { + public T get(int index) { // Paper - public T object = this.palette.valueFor(this.storage.get(index)); return (T)(object == null ? this.defaultValue : object); } diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java index 7dc3d806a680150c6a2fffa1436fd63bbdc31eb3..e1b32b644bc976ff66258ed706f4d1e8de99420d 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java @@ -1,5 +1,7 @@ package net.minecraft.world.level.chunk; +import ca.spottedleaf.starlight.light.SWMRNibbleArray; +import ca.spottedleaf.starlight.light.StarLightEngine; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -72,6 +74,53 @@ public class ProtoChunk implements ChunkAccess { // Paper end private static boolean PRINTED_OUTDATED_CTOR_MSG = false; // Paper - Add level + // Paper start - rewrite light engine + protected volatile SWMRNibbleArray[] blockNibbles; + protected volatile SWMRNibbleArray[] skyNibbles; + protected volatile boolean[] skyEmptinessMap; + protected volatile boolean[] blockEmptinessMap; + + @Override + public SWMRNibbleArray[] getBlockNibbles() { + return this.blockNibbles; + } + + @Override + public void setBlockNibbles(SWMRNibbleArray[] nibbles) { + this.blockNibbles = nibbles; + } + + @Override + public SWMRNibbleArray[] getSkyNibbles() { + return this.skyNibbles; + } + + @Override + public void setSkyNibbles(SWMRNibbleArray[] nibbles) { + this.skyNibbles = nibbles; + } + + @Override + public boolean[] getSkyEmptinessMap() { + return this.skyEmptinessMap; + } + + @Override + public void setSkyEmptinessMap(boolean[] emptinessMap) { + this.skyEmptinessMap = emptinessMap; + } + + @Override + public boolean[] getBlockEmptinessMap() { + return this.blockEmptinessMap; + } + + @Override + public void setBlockEmptinessMap(boolean[] emptinessMap) { + this.blockEmptinessMap = emptinessMap; + } + // Paper end - rewrite light engine + @Deprecated // Paper start - add level public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world) { // Paper start @@ -100,6 +149,10 @@ public class ProtoChunk implements ChunkAccess { } } public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList blockTickScheduler, ProtoTickList fluidTickScheduler, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) { + // Paper start + this.blockNibbles = StarLightEngine.getFilledEmptyLight(world); + this.skyNibbles = StarLightEngine.getFilledEmptyLight(world); + // Paper end this.level = level; // Paper end this.chunkPos = pos; @@ -197,7 +250,7 @@ public class ProtoChunk implements ChunkAccess { LevelChunkSection levelChunkSection = this.getOrCreateSection(l); BlockState blockState = levelChunkSection.setBlockState(i & 15, j & 15, k & 15, state); - if (this.status.isOrAfter(ChunkStatus.FEATURES) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { + if (this.status.isOrAfter(ChunkStatus.LIGHT) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { // Paper - move block updates to only happen after lighting occurs (or during, thanks chunk system) this.lightEngine.checkBlock(pos); } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java index 80b9f3547bc30cb470d272132e96fcce188efd91..c81392f5b4a6dcef9c1864c1b2c268914b904561 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java @@ -63,6 +63,14 @@ import org.apache.logging.log4j.Logger; public class ChunkSerializer { + // Paper start - replace light engine impl + private static final int STARLIGHT_LIGHT_VERSION = 5; + + private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; + private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; + private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; + // Paper end - replace light engine impl + private static final Logger LOGGER = LogManager.getLogger(); public static final String TAG_UPGRADE_DATA = "UpgradeData"; @@ -131,13 +139,20 @@ public class ChunkSerializer { ProtoTickList protochunkticklist1 = new ProtoTickList<>((fluidtype) -> { return fluidtype == null || fluidtype == Fluids.EMPTY; }, pos, nbttagcompound1.getList("LiquidsToBeTicked", 9), world); - boolean flag = nbttagcompound1.getBoolean("isLightOn"); + boolean flag = getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbttagcompound1.get("isLightOn") != null && nbttagcompound1.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper ListTag nbttaglist = nbttagcompound1.getList("Sections", 10); int i = world.getSectionsCount(); LevelChunkSection[] achunksection = new LevelChunkSection[i]; boolean flag1 = world.dimensionType().hasSkyLight(); ServerChunkCache chunkproviderserver = world.getChunkSource(); LevelLightEngine lightengine = chunkproviderserver.getLightEngine(); + // Paper start + ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Paper - replace light impl + ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Paper - replace light impl + final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); + final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); + boolean canReadSky = world.dimensionType().hasSkyLight(); + // Paper end if (flag) { tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main @@ -146,7 +161,7 @@ public class ChunkSerializer { } for (int j = 0; j < nbttaglist.size(); ++j) { - CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); + CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound2; // Paper byte b0 = nbttagcompound2.getByte("Y"); if (nbttagcompound2.contains("Palette", 9) && nbttagcompound2.contains("BlockStates", 12)) { @@ -164,23 +179,29 @@ public class ChunkSerializer { } if (flag) { - if (nbttagcompound2.contains("BlockLight", 7)) { - // Paper start - delay this task since we're executing off-main - DataLayer blockLight = new DataLayer(nbttagcompound2.getByteArray("BlockLight")); - tasksToExecuteOnMain.add(() -> { - lightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkcoordintpair1, b0), blockLight, true); - }); - // Paper end - delay this task since we're executing off-main + // Paper start - rewrite light engine + int y = sectionData.getByte("Y"); + + if (sectionData.contains("BlockLight", 7)) { + // this is where our diff is + blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety + } else { + blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); } - if (flag1 && nbttagcompound2.contains("SkyLight", 7)) { - // Paper start - delay this task since we're executing off-main - DataLayer skyLight = new DataLayer(nbttagcompound2.getByteArray("SkyLight")); - tasksToExecuteOnMain.add(() -> { - lightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkcoordintpair1, b0), skyLight, true); - }); - // Paper end - delay this task since we're executing off-main + if (canReadSky) { + if (sectionData.contains("SkyLight", 7)) { + // we store under the same key so mod programs editing nbt + // can still read the data, hopefully. + // however, for compatibility we store chunks as unlit so vanilla + // is forced to re-light them if it encounters our data. It's too much of a burden + // to try and maintain compatibility with a broken and inferior skylight management system. + skyNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety + } else { + skyNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); + } } + // Paper end - rewrite light engine } } @@ -224,8 +245,12 @@ public class ChunkSerializer { object = new LevelChunk(world.getLevel(), pos, biomestorage, chunkconverter, (TickList) object1, (TickList) object2, k, achunksection, // Paper start - fix massive nbt memory leak due to lambda. move lambda into a container method to not leak scope. Only clone needed NBT keys. createLoadEntitiesConsumer(new SafeNBTCopy(nbttagcompound1, "TileEntities", "Entities", "ChunkBukkitValues")) // Paper - move CB Chunk PDC into here );// Paper end + ((LevelChunk)object).setBlockNibbles(blockNibbles); // Paper - replace light impl + ((LevelChunk)object).setSkyNibbles(skyNibbles); // Paper - replace light impl } else { ProtoChunk protochunk = new ProtoChunk(pos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, world); // Paper - add level + protochunk.setBlockNibbles(blockNibbles); // Paper - replace light impl + protochunk.setSkyNibbles(skyNibbles); // Paper - replace light impl protochunk.setBiomes(biomestorage); object = protochunk; @@ -406,7 +431,7 @@ public class ChunkSerializer { DataLayer[] blockLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; DataLayer[] skyLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()]; - for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) { + for (int i = lightenginethreaded.getMinLightSection(); false && i < lightenginethreaded.getMaxLightSection(); ++i) { // Paper - don't run loop, we don't need to - light data is per chunk now DataLayer blockArray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkPos, i)); DataLayer skyArray = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkPos, i)); @@ -455,6 +480,12 @@ public class ChunkSerializer { return saveChunk(world, chunk, null); } public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, AsyncSaveData asyncsavedata) { + // Paper start - rewrite light impl + final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world); + final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world); + ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles(); + ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles(); + // Paper end - rewrite light impl // Paper end ChunkPos chunkcoordintpair = chunk.getPos(); CompoundTag nbttagcompound = new CompoundTag(); @@ -483,32 +514,33 @@ public class ChunkSerializer { LevelChunkSection chunksection = (LevelChunkSection) Arrays.stream(achunksection).filter((chunksection1) -> { return chunksection1 != null && SectionPos.blockToSectionCoord(chunksection1.bottomBlockY()) == finalI; // CraftBukkit - decompile errors }).findFirst().orElse(LevelChunk.EMPTY_SECTION); - // Paper start - async chunk save for unload - DataLayer nibblearray; // block light - DataLayer nibblearray1; // sky light - if (asyncsavedata == null) { - nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData) - nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData) - } else { - nibblearray = asyncsavedata.blockLight[i - lightenginethreaded.getMinLightSection()]; - nibblearray1 = asyncsavedata.skyLight[i - lightenginethreaded.getMinLightSection()]; - } - // Paper end - if (chunksection != LevelChunk.EMPTY_SECTION || nibblearray != null || nibblearray1 != null) { - CompoundTag nbttagcompound2 = new CompoundTag(); + // Paper start - replace light engine + ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState(); + ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState(); + if (chunksection != LevelChunk.EMPTY_SECTION || blockNibble != null || skyNibble != null) { + // Paper end - replace light engine + CompoundTag nbttagcompound2 = new CompoundTag(); CompoundTag section = nbttagcompound2; // Paper nbttagcompound2.putByte("Y", (byte) (i & 255)); if (chunksection != LevelChunk.EMPTY_SECTION) { chunksection.getStates().write(nbttagcompound2, "Palette", "BlockStates"); } - if (nibblearray != null && !nibblearray.isEmpty()) { - nbttagcompound2.putByteArray("BlockLight", nibblearray.asBytesPoolSafe().clone()); // Paper + // Paper start - replace light engine + if (blockNibble != null) { + if (blockNibble.data != null) { + section.putByteArray("BlockLight", blockNibble.data); + } + section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); } - if (nibblearray1 != null && !nibblearray1.isEmpty()) { - nbttagcompound2.putByteArray("SkyLight", nibblearray1.asBytesPoolSafe().clone()); // Paper + if (skyNibble != null) { + if (skyNibble.data != null) { + section.putByteArray("SkyLight", skyNibble.data); + } + section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); } + // Paper end - replace light engine nbttaglist.add(nbttagcompound2); } @@ -516,7 +548,8 @@ public class ChunkSerializer { nbttagcompound1.put("Sections", nbttaglist); if (flag) { - nbttagcompound1.putBoolean("isLightOn", true); + nbttagcompound1.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Paper + nbttagcompound1.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_) } ChunkBiomeContainer biomestorage = chunk.getBiomes();