mojira.dev
MC-148580

Server lighting still broken in 1.14 pre-4

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:

  1. 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)

  2. 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

Linked issues

Attachments

Comments 12

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
[media]

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
[media]

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.

2 more comments

More problem in my server for this bug !
Please, reopen this Issue

[media]

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.

PhiPro

(Unassigned)

Confirmed

Lighting

Minecraft 1.14 Pre-Release 4, Minecraft 1.14 Pre-Release 5, Minecraft 1.14

Minecraft 1.14.1 Pre-Release 1

Retrieved