The skylight issues still exist in 1.14 pre-4.
An easy way to reproduce them is as follows:
Create a new redstone ready world
Restart the world after it is generated
Run
/setblock 0 96 0 stone
You will obtain the following
[media]While the client light is correctly at 15, note that the server light is at 0! Reloading the world does not fix the issue.
I have done some code digging and found the root cause of this. The problem lies within the way new lightmaps for subchunks are created, initialized and added to the world.
New light maps are created inside
nwt.minecraft.world.chunk.light.LightStorage.java
protected void setLevel(long long_1, int int_1) {
int int_2 = this.getLevel(long_1);
if (int_2 != 0 && int_1 == 0) {
this.field_15808.add(long_1);
this.field_15804.remove(long_1);
}
if (int_2 == 0 && int_1 != 0) {
this.field_15808.remove(long_1);
this.field_15797.remove(long_1);
}
if (int_2 >= 2 && int_1 != 2) {
if (this.toRemove.contains(long_1)) {
this.toRemove.remove(long_1);
} else {
this.dataStorage.addForChunk(long_1, this.getDataForChunk(long_1));
this.field_15802.add(long_1);
this.method_15523(long_1);
for(int int_3 = -1; int_3 <= 1; ++int_3) {
for(int int_4 = -1; int_4 <= 1; ++int_4) {
for(int int_5 = -1; int_5 <= 1; ++int_5) {
this.toNotify.add(ChunkSectionPos.toChunkLong(BlockPos.add(long_1, int_4, int_5, int_3)));
}
}
}
}
}
if (int_2 != 2 && int_1 >= 2) {
this.toRemove.add(long_1);
}
this.hasLightUpdates = !this.toRemove.isEmpty();
}
The relevant part here is
this.dataStorage.addForChunk(long_1, this.getDataForChunk(long_1));
this.field_15802.add(long_1);
this.method_15523(long_1);
this.getDataForChunk(long_1)
creates an uninitialized lightmap that is directly added to the world. Some sort of initialization is done via this.method_15523(long_1);
, so let's look at that:
net.minecraft.world.chunk.light.SkyLightStorage.java
protected void method_15523(long long_1) {
int int_1 = ChunkSectionPos.unpackLongY(long_1);
if (((SkyLightStorage.Data)this.dataStorage).defaultHeight > int_1) {
((SkyLightStorage.Data)this.dataStorage).defaultHeight = int_1;
((SkyLightStorage.Data)this.dataStorage).heightMap.defaultReturnValue(((SkyLightStorage.Data)this.dataStorage).defaultHeight);
}
long long_2 = ChunkSectionPos.toLightStorageIndex(long_1);
int int_2 = ((SkyLightStorage.Data)this.dataStorage).heightMap.get(long_2);
if (int_2 < int_1 + 1) {
((SkyLightStorage.Data)this.dataStorage).heightMap.put(long_2, int_1 + 1);
if (this.field_15817.contains(long_2)) {
this.field_15815.add(long_1);
this.field_15816.remove(long_1);
if (int_2 > ((SkyLightStorage.Data)this.dataStorage).defaultHeight) {
long long_3 = ChunkSectionPos.asLong(ChunkSectionPos.unpackLongX(long_1), int_2 - 1, ChunkSectionPos.unpackLongZ(long_1));
this.field_15815.remove(long_3);
this.field_15816.add(long_3);
}
this.checkForUpdates();
}
}
}
This method schedules the lightmaps to be initialized somewhere else. Without going into detail, the initialization will then set the topmost subchunk to a skylight value of 15 and propagate that to the neighbors. Lower subchunks are not initialized! (more on that later).
The problematic part here is the if (this.field_15817.contains(long_2))
check. As far as i understand it, this is meant as some way to disable skylight initialization for some chunks, eg. during worldgen.
But as it turns out, this disables basically all serverside skylight initialization.
Skipping some parts of the call tree, the only serverside place where skylight initialization is enabled for a chunk (ie. it is added to field_15817
) is inside
net.minecraft.server.world.ServerLightingProvider.java
public CompletableFuture<Chunk> light(final Chunk class_2791_1, final boolean isLightOn)
{
final ChunkPos class_1923_1 = class_2791_1.getPos();
this.enqueue(class_1923_1.x, class_1923_1.z, ServerLightingProvider.class_3901.field_17261, SystemUtil.debugRunnable(() -> {
if (!isLightOn)
{
super.enableLight(class_1923_1, true);
}
final ChunkSection[] class_2826s_1 = class_2791_1.getSectionArray();
for (int int_1 = 0; int_1 < 16; ++int_1)
{
final ChunkSection class_2826_1 = class_2826s_1[int_1];
if (!ChunkSection.isEmpty(class_2826_1))
{
super.updateSectionStatus(ChunkSectionPos.from(class_1923_1, int_1), false);
}
}
if (!isLightOn)
{
class_2791_1.getLightSourcesStream().forEach((class_2338_1) -> {
super.method_15560(class_2338_1, class_2791_1.getLuminance(class_2338_1));
});
}
class_2791_1.setLightOn(true);
this.chunkStorage.method_20441(class_1923_1);
}, () -> {
return "lightChunk " + class_1923_1 + " " + isLightOn;
}));
return CompletableFuture.supplyAsync(() -> {
return class_2791_1;
}, (runnable_1) -> {
this.enqueue(class_1923_1.x, class_1923_1.z, ServerLightingProvider.class_3901.field_17262, runnable_1);
});
}
This means that only chunks that had their initial lighting done recently are added to the set. This is why in the reproduction steps above you need to restart the world. That way, the chunks won't be inside this set anymore, and hence the skylight initialization is disabled. Because of that, new lightmaps stay at their initial light level of 0, which is exactly what was observed above.
The solution to this is rather easy: Just add the chunks that are already lighted to that set as well.
Now, with that out of the way, may I ask why you implemented the initialization the way you did? In my opinion, this is rather complicated and can be simplified quite a bit.
For newLight, I just initialize new lightmaps as follows which works perfectly fine:
Before adding a new lightmap to the world, initialize it to whatever light value would have been returned before adding it. This means, for the topmost section, initialize it to 15. For lower sections, apply the usual skylight lookup, ie. inherit the next skylight value above the position.
This makes sure that adding the lightmap does not change any light value. Currently, adding a new lightmap changes some skylight values to 0, so you have to do some work to compensate for that.
You don't need to schedule any light checks for the subchunk or any neighbors, because adding the initialized lightmap didn't change any light values.
In my opinion this approach is much simpler.
As it turns out, the current Vanilla initialization code has some other bug. As can be seen in method_15523
posted above, initialization only happens when adding a new topmost subchunk. This new topmost subchunk gets then initialized (modulo the fact that it currently doesn't work on the server). Subchunks that were previously topmost get some special handling, namely lighting for their top face get reevalutaed (this is not necessary for my simple initialization approach). However, adding a new subchunk below the current topmost subchunk does not trigegr any initialization. This leads to an error as can be reproduced as follows:
Create a new redstone ready world
Restart the world after it is generated
Run the following commands one after another
/setblock 0 95 32 stone /fill 0 112 16 15 112 31 stone /setblock 0 112 -16 stone /fill 0 95 0 15 95 31 stone
and obtain the following picture
[media]Note that clientside lighting is not affected by the above issue, so fixing it will not fix this new issue.
So, what is going on? Let's look at it step by step; I refer to the two subchunks seen in the picture as left and right subchunk.
/setblock 0 95 32 stone
makes sure that a lightmap for the right subchunk is created and added to the world. On the client, this lightmap is properly initialized./fill 0 112 16 15 112 31 stone
blocks direct skylight access for the right subchunk, so the right subchunk only gets its light values from the sides.Together with
/setblock 0 112 -16 stone
, this also adds lightmaps above the left subchunk and its neighbors (which are still properly initialized on the client).Now comes the interesting part:
/fill 0 95 0 15 95 31 stone
adds a lightmap for the left subchunk and its neighbors. In the previous step, we made sure that there are already lightmaps above it. By what I have said earlier, those new lightmaps don't trigger any initialization, because they are not topmost. Hence they stay at their initial light level of 0. This is why the left subchunk is completely dark.What's going on in the right subchunk? In step 2 we block the direct skylight access, so it only gets its light values from the sides. The fourth step adds another platform above it. But because the right subchunk already gets its light from the sides but not from above, this does not trigger the light values to decrease, because only the top face is blocked, which didn't contribute to the lighting anyway.
The important point is now that the left subchunk didn't cause any initialization. No light checks are scheduled for itself or its neighbors. That means that the addition of a lightmap for the left subchunk suddenly switches light values from 15 to 0 without the lighting engine noting it. While spawning the stone floor causes light checks, they don't have any effect because the lighting is already 0. Hence the lighting engine never detects that the right subchunk shouldn't receive any light from the left subchunk anymore.
What does this rather long explanation tell us? The main problem is basically that adding a lightmap causes the light values to change without the lighting engine noting it. While one could solve this by scheduling light checks for this subchunk and its neighbor, I find my simple initialization approach described above much more intuitive and easier to implement. Because the simple initialization approach does not change any light values, there are no checks that need to be performed.
So, are there any problems with my initialization approach?
I think the only problem is how you handle not yet lighted chunks.
There are several possibilities:
Just use the simple initialization algorithm all the time. This is not really desired during worldgen. That is because spreading light is easier than removing light. You don't want things to be lit while you are changing large amounts of blocks.
I assume that's the reasoning behind the Vanilla way of disabling skylight initialization before a chunk is initially lighted (ie. the mechanism from the first paragraph)Keep everything dark before the chunk is initially lighted, even those subchunks that are above all blocks. Turn on the skylight during initial lighting.
This approach has the problem that neighbor chunks now can spread light into subchunks that are above all blocks and would otherwise be at ligth level 15. This creates more unnecessary light updates, and furthermore causes more lightmaps to be created, which is not really desireable.
Hence I propose the following solution (which I basically implemented in newLight):
Keep the whole chunk dark before it is initially lighted, as in the second approach. But don't spread any light updates into it.
Whenever there is a light update that wants to propagate into the not yet lighted chunk, remeber it and carry it out during initial lighting. Also, don't create any lightmaps for this chunk yet, create them during initial lighting.
This eliminates the issue of the second approach I described above.You don't need to remeber individual light updates. You can just remember that a face of a subchunk got hit by some light update and then reevaluate the whole face. Usually, when a face gets hit, it actually gets hit by many updates, so checking the whole face isn't any more expensive than remembering individual updates.
Initial skylight then looks as follows:
Create lightmaps with the usual algorithm (ie. at chunk distance <= 1 from any block), but don't initialize them yet (the lighting is still everyhwere 0).
Set the skylight above the topmost lightmap to 15, ie. switch to the usual skylight calculation for empty subchunks.
Spread skylight to the top face of the topmost lightmap.
Above the topmost lightmap, spread skylight to the neighbor subchunks. If they are not yet lighted, don't do the spreading directly but remember those light updates, as with any other light updates.
Carry out the remembered light updates for this chunk. Skylight updates that point into a section above the topmost lightmap can be skipped, because we are already at ligth level 15. This eliminates unnecessary updates scheduled in the previous step.
Note: in the second last step, ie. spreading light to neighbors, we cannot omit checks for subchunks above the topmost subchunk if those neighbors are not yet lighted. That is because we don't know yet which subchunks will be above the topmost lightmap at the point of initial lighting. For already lighted neighbors, however, we can omit them.
As explained in the last step, those unnecessary checks for not yet lighted neighbors can be dropped again when doing the initial lighting for the neighbor.
In my opinion, this approach is cleaner and easier to implement than the current Vanilla implementation and avoids some bugs of the current implementation.
If you want to discuss, you can write me on Discord (or here ofc) 🙂
Hope this (rather long, sry for that 😛) analysis is helpful 🙂
PhiPro
Related issues
relates to
Attachments
Comments


Beautifully well explained. Seems like a pretty nasty bug indeed.

Only the first part of the issue was fixed in pre-5.
The second part still exists.
I have put them inside a single bug report, because they are thematically related, both having to do with initialization of lightmaps.
However, my reproduction steps above for the second part turned out to be wrong. My comment that "clientside lighting is not affected by the above issue, so fixing it will not fix this new issue" was actually wrong. Because the steps cause large numbers of block updates, light packets get sent to the client, so the client does get affected by the server light. This hided the fact that my reproduction steps were wrong.
The problem with the reproduction steps was that I assumed that a block update only schedules light checks for the block itself. However, it turns out that also light checks are scheduled for the 6 neighbors, which creates some problems.
Fortunately, I could design another list of repdroduction steps (the idea is basically the same as for the old steps, so I won't explain any further. Also, there are some additional steps because I have to circumvent MC-148723):
Create a new redstone ready world
Run the following commands one after another
/fill 0 95 32 15 95 32 stone /fill 0 95 0 15 95 0 stone /fill 0 128 16 15 128 31 stone /fill 0 95 0 15 95 0 air /fill 0 128 -16 15 128 -15 stone /fill 0 95 0 15 95 31 stone
While the image looks a bit different, it still shows the same basic features: the left subchunk is too dark and the right subchunk too bright.
Admittedly, those reproduction steps are rather artifical. I have designed them to show the case where "darkness is not spread to the neighbors", ie. the lighting engine does not recognize that the subchunk turns dark due to the missing initialization and hence does not remove light propagation to the neighbors. This is only meant for showing that there actually is a problem with the current initialization code, which can be fixed for example by the initialization approach i suggested.
On the other hand, the fact that the left subchunk is too dark can be reproduced more easily as follows
Create a new redstone ready world
Run the following commands one after another
/setblock 7 119 7 stone /setblock 7 87 7 stone
This situation occurs naturally when building under an overhang (although that overhang must be at a considerable height above ground).
Can confirm with the reproduction steps above. Reopening.
Thanks for the analysis, it's very much appreciated.
First part of the issue was indeed a simple omission - already lit chunks were not marked as such after reloading.
Initializing the light using the implicit light level was the initial approach I tool, but it was not compatible with the worldgen/recomputation of data, since it is only correct if the chunk section is only filled with air/non light blocking blocks, so initially it resulted in full chunks filled with skylight; it should be done if the light for the chunk is in a computed state already, and when the new section needs to be stored for that chunk; I tried that approach, but it caused large dark spots of light to appear during worldgen, which I think is caused either by not quite right order of operations in worldgen lighting, or by MC-148723, or by some other issue; I'll re-enable it after fixing that, and we'll see what happens 🙂

I'm not sure if this is related, but on my server there are chunks that keeps bugging still (see attached screenshot). I've upgraded the chunks to 1.14 pre-release 5 with --forceUpgrade --eraseCache:
[media]Haven't found a way to reproduce it other than going far enough to unload the chunk and randomly come back with the lights not working.

Hi Fry, thanks for your answer 🙂
If I understand you correctly, I think your first approach was also the first approach I mentioned for handling not yet lighted chunks, ie. "Just use the simple initialization algorithm all the time". That one I didn't like because you have to remove all of the light after worldgen again, which is exactly the problem you had with it.
Your second approach also sounds like my second approach, ie. "Keep everything dark before the chunk is initially lighted, ..." 😛. How did you handle light updates into unlit chunks?
Just carry them out directly (ie. my second approach)
Queue them like I did in my final approach
Just discard them
In case you just discard them, this might be the cause for the black spots you were observing.
You can write me on Discord, if you want to discuss in greater detail (or ofc continue discussion here). But take your time experimenting with it after fixing the other bug. 🙂
@Michel That issue doesn't really look related. It is not about lightmaps not being initialized when they are created, but it seems like lightmaps actually get lost during saving. I have a few ideas what could be the cause of it and will look into it during the weekend (hopefully) 🙂

This is still not completely fixed in 1.14. I'm posting this here since the new issue is again caused by lightmaps not being properly initialized, so it has the same cause as the previous issues.
You can reproduce the new issue by the following steps:
Create a new redstone ready world
/fill 0 109 0 31 109 15 stone
Observe that the ground of the right chunk is completely bright.
[media]
Actually, already the subchunk below the platform is bright
[media]
Looking at the code, light maps for already lighted chunks are now (most of the time) properly initialized inside
net.minecraft.world.chunk.light.SkyLightStorage.java
@Override
protected ChunkNibbleArray getDataForChunk(long long2) {
ChunkNibbleArray chunkNibbleArray4 = (ChunkNibbleArray)this.toUpdate.get(long2);
if (chunkNibbleArray4 != null) {
return chunkNibbleArray4;
}
if (!this.n(long2)) {
return new ChunkNibbleArray();
}
long long5;
int integer7;
for (long5 = ChunkSectionPos.offsetPacked(long2, Direction.UP), integer7 = ((SkyLightStorage.Data)this.dataStorage).heightMap.get(ChunkSectionPos.toLightStorageIndex(long2)); integer7 != ((SkyLightStorage.Data)this.dataStorage).defaultHeight && ChunkSectionPos.unpackLongY(long5) < integer7 && !this.hasChunk(long5); long5 = ChunkSectionPos.offsetPacked(long5, Direction.UP)) {}
ChunkNibbleArray chunkNibbleArray8 = ((SkyLightStorage.Data)this.dataStorage).getDataForChunk(long5);
if (chunkNibbleArray8 != null) {
return new ChunkNibbleArray(new ckm(chunkNibbleArray8, 0).asByteArray());
}
if (this.m.contains(ChunkSectionPos.asLong(ChunkSectionPos.unpackLongX(long2), integer7 - 1, ChunkSectionPos.unpackLongZ(long2)))) {
return new ChunkNibbleArray();
}
ChunkNibbleArray chunkNibbleArray9 = new ChunkNibbleArray();
Arrays.fill(chunkNibbleArray9.asByteArray(), (byte)(-1));
return chunkNibbleArray9;
}
protected boolean n(long long2) {
long long4 = ChunkSectionPos.toLightStorageIndex(long2);
return this.o.contains(long4);
}
However, the check if (this.m.contains(ChunkSectionPos.asLong(ChunkSectionPos.unpackLongX(long2), integer7 - 1, ChunkSectionPos.unpackLongZ(long2))))
prevents the lightmap to be initialized if it is the new topmost lightmap for this chunk and there was another lightmap added above the old topmost one in the current update run.
So, what is happening in the example above?
First, the lightmap for the left subchunk (in the picture) containing the stone platform is created. This also creates lightmaps for the 26 neighbor subchunks (diagonal neighbors included). Apart from the first chunk, the lightmaps are created and initialized bottom to top. This means, the lowest lightmap is created first and initialized correctly to a skylight value of 15. Then it is passed to the old initialization logic, which adds the subchunk coordinates to the set m
. Later, the lightmap above it (corresponding to the height where the stone platform was added) gets created and initialized. This time, though, it gets initialized to a skylight value of 0, because of the additional check. Then, by the old initialization logic, it gets added to the set m
and the previous subchunk gets removed and added to the set n
instead. Finally, the topmost lightmap will be created and initialized at some later point. And then, analogously, the lightmap for the subchunk containing the stone platform will get removed from m
and added to n
.
In the end, apart from the first column that was processed, the situation looks as follows:
The lowest subchunk is initialized to a skylight value of 15, the other two subchunks above it have a skylight value of 0.
The topmost subchunk is contained in the set
m
, the other two in the setn
What does the old initialization logic do then?
The topmost lightmap gets initialized to a skylight value of 15 and this gets spread to the neighbors (which would have been unnecessary, would it have been properly initialized in the first place)
The two lower lightmaps are handled inside this part of the code
if (!this.n.isEmpty()) { for (final long var5 : this.n) { if (this.l.remove(var5)) { if (!this.hasChunk(var5)) { continue; } for (int var6 = 0; integer8 < 16; ++integer8) { for (int var7 = 0; integer9 < 16; ++integer9) { long long10 = BlockPos.asLong(ChunkSectionPos.fromChunkCoord(ChunkSectionPos.unpackLongX(var5)) + integer8, ChunkSectionPos.fromChunkCoord(ChunkSectionPos.unpackLongY(var5)) + 16 - 1, ChunkSectionPos.fromChunkCoord(ChunkSectionPos.unpackLongZ(var5)) + integer9); lightProvider.update(Long.MAX_VALUE, long10, 15, false); } } } } }
As far as I can tell, this.l
is a set consisting of the previous topmost lightmaps, ie. the topmost lightmaps before the current update run. In particular, it does not contain the two lower lightmaps which were only temporarily at the top, but it only contains the lightmap slightly above the ground, which was the previous topmost lightmap. Hence, this initialization logic will just be skipped for those two lightmaps.
In summary, this means for the right chunk in the picture
The subchunk containing the stone platform has a skylight value of 0. No checks are scheduled due to initialization; checks are only scheduled for the placed blocks.
The subchunk below it is initialized to a skylight value of 15. No checks are scheduled for it.
Because the subchunk containing the platform is already dark after the lightmaps are added to the world, the lighting engine does not propagate darkness caused by the added stone platform. And hence it will not notice that the subchunk below is too bright.
So, the problem is exactly the same as in the previous cases: The subchunk containing the stone platform is not initialized properly, and hence the lighting engine doesn't notice that it should propagate updates to the (correctly initialized) subchunk below it.
What is the idea behind this additional check if (this.m.contains(ChunkSectionPos.asLong(ChunkSectionPos.unpackLongX(long2), integer7 - 1, ChunkSectionPos.unpackLongZ(long2))))
?
Apart from that check, the new initialization logic looks correct to me. If everything works correctly, the old initialization logic should be obsolete and could be removed.

More problem in my server for this bug !
Please, reopen this Issue
1.14.1 Pre-Re 2
@unknown, that's a different issue, see MC-142134.

Ok, thx 😛

In case it helps, we have a Realm where the problem remains. https://www.youtube.com/watch?v=2jNm5P-MMKU
@unknown, linking to a 15min video is not helpful at all. Your issue is probably MC-142134.