PocketMine-MP/src/world/light/LightUpdate.php
Dylan K. Taylor ee26d6d570
LightUpdate: avoid trying to propagate light into nodes with higher light levels
Track which direction the current node's light came from, and don't
check it again when we check the current node's adjacent blocks.

e.g. if this node was the eastern neighbour of a light source, we don't
need to check this node's western neighbour, as we already know it has
a higher light level than our own.

This improves performance of basic light spread in a void by about 6%,
which isn't a huge amount, but it's something.

I've yet to explore whether light removal could also benefit from this
change.
2023-10-09 17:06:02 +01:00

228 lines
7.7 KiB
PHP

<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\world\light;
use pocketmine\math\Facing;
use pocketmine\world\format\LightArray;
use pocketmine\world\format\SubChunk;
use pocketmine\world\utils\SubChunkExplorer;
use pocketmine\world\utils\SubChunkExplorerStatus;
use pocketmine\world\World;
use function assert;
use function max;
//TODO: make light updates asynchronous
abstract class LightUpdate{
public const BASE_LIGHT_FILTER = 1;
/**
* @var int[][] blockhash => [x, y, z, new light level]
* @phpstan-var array<int, array{int, int, int, int}>
*/
protected array $updateNodes = [];
/**
* @param int[] $lightFilters
* @phpstan-param array<int, int> $lightFilters
*/
public function __construct(
protected SubChunkExplorer $subChunkExplorer,
protected array $lightFilters
){}
abstract protected function getCurrentLightArray() : LightArray;
abstract public function recalculateNode(int $x, int $y, int $z) : void;
/**
* Scans for all light sources in the target chunk and adds them to the propagation queue.
* This erases preexisting light in the chunk.
*/
abstract public function recalculateChunk(int $chunkX, int $chunkZ) : int;
protected function getEffectiveLight(int $x, int $y, int $z) : int{
if($this->subChunkExplorer->moveTo($x, $y, $z) !== SubChunkExplorerStatus::INVALID){
return $this->getCurrentLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
}
return 0;
}
protected function getHighestAdjacentLight(int $x, int $y, int $z) : int{
$adjacent = 0;
foreach(Facing::OFFSET as [$ox, $oy, $oz]){
if(($adjacent = max($adjacent, $this->getEffectiveLight($x + $ox, $y + $oy, $z + $oz))) === 15){
break;
}
}
return $adjacent;
}
public function setAndUpdateLight(int $x, int $y, int $z, int $newLevel) : void{
$this->updateNodes[World::blockHash($x, $y, $z)] = [$x, $y, $z, $newLevel];
}
private function prepareNodes() : LightPropagationContext{
$context = new LightPropagationContext();
foreach($this->updateNodes as $blockHash => [$x, $y, $z, $newLevel]){
if($this->subChunkExplorer->moveTo($x, $y, $z) !== SubChunkExplorerStatus::INVALID){
$lightArray = $this->getCurrentLightArray();
$oldLevel = $lightArray->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
if($oldLevel !== $newLevel){
$lightArray->set($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK, $newLevel);
if($oldLevel < $newLevel){ //light increased
$context->spreadVisited[$blockHash] = true;
$context->spreadQueue->enqueue([$x, $y, $z]);
}else{ //light removed
$context->removalVisited[$blockHash] = true;
$context->removalQueue->enqueue([$x, $y, $z, $oldLevel]);
}
}
}
}
return $context;
}
public function execute() : int{
$context = $this->prepareNodes();
$touched = 0;
$lightArray = null;
$subChunkExplorer = $this->subChunkExplorer;
$subChunkExplorer->invalidate();
while(!$context->removalQueue->isEmpty()){
$touched++;
[$x, $y, $z, $oldAdjacentLight] = $context->removalQueue->dequeue();
foreach(Facing::OFFSET as [$ox, $oy, $oz]){
$cx = $x + $ox;
$cy = $y + $oy;
$cz = $z + $oz;
$moveStatus = $subChunkExplorer->moveTo($cx, $cy, $cz);
if($moveStatus === SubChunkExplorerStatus::INVALID){
continue;
}
if($moveStatus === SubChunkExplorerStatus::MOVED){
$lightArray = $this->getCurrentLightArray();
}
assert($lightArray !== null);
$this->computeRemoveLight($cx, $cy, $cz, $oldAdjacentLight, $context, $lightArray);
}
}
$subChunk = null;
$subChunkExplorer->invalidate();
while(!$context->spreadQueue->isEmpty()){
$touched++;
[$x, $y, $z] = $context->spreadQueue->dequeue();
$from = $context->spreadVisited[World::blockHash($x, $y, $z)];
unset($context->spreadVisited[World::blockHash($x, $y, $z)]);
$moveStatus = $subChunkExplorer->moveTo($x, $y, $z);
if($moveStatus === SubChunkExplorerStatus::INVALID){
continue;
}
if($moveStatus === SubChunkExplorerStatus::MOVED){
$subChunk = $subChunkExplorer->currentSubChunk;
$lightArray = $this->getCurrentLightArray();
}
assert($lightArray !== null);
$newAdjacentLight = $lightArray->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
if($newAdjacentLight <= 0){
continue;
}
foreach(Facing::OFFSET as $side => [$ox, $oy, $oz]){
if($from === $side){
//don't check the side that this node received its initial light from
continue;
}
$cx = $x + $ox;
$cy = $y + $oy;
$cz = $z + $oz;
$moveStatus = $subChunkExplorer->moveTo($cx, $cy, $cz);
if($moveStatus === SubChunkExplorerStatus::INVALID){
continue;
}
if($moveStatus === SubChunkExplorerStatus::MOVED){
$subChunk = $subChunkExplorer->currentSubChunk;
$lightArray = $this->getCurrentLightArray();
}
assert($subChunk !== null);
$this->computeSpreadLight($cx, $cy, $cz, $newAdjacentLight, $context, $lightArray, $subChunk, $side);
}
}
return $touched;
}
protected function computeRemoveLight(int $x, int $y, int $z, int $oldAdjacentLevel, LightPropagationContext $context, LightArray $lightArray) : void{
$lx = $x & SubChunk::COORD_MASK;
$ly = $y & SubChunk::COORD_MASK;
$lz = $z & SubChunk::COORD_MASK;
$current = $lightArray->get($lx, $ly, $lz);
if($current !== 0 && $current < $oldAdjacentLevel){
$lightArray->set($lx, $ly, $lz, 0);
if(!isset($context->removalVisited[$index = World::blockHash($x, $y, $z)])){
$context->removalVisited[$index] = true;
if($current > 1){
$context->removalQueue->enqueue([$x, $y, $z, $current]);
}
}
}elseif($current >= $oldAdjacentLevel){
if(!isset($context->spreadVisited[$index = World::blockHash($x, $y, $z)])){
$context->spreadVisited[$index] = true;
$context->spreadQueue->enqueue([$x, $y, $z]);
}
}
}
protected function computeSpreadLight(int $x, int $y, int $z, int $newAdjacentLevel, LightPropagationContext $context, LightArray $lightArray, SubChunk $subChunk, int $side) : void{
$lx = $x & SubChunk::COORD_MASK;
$ly = $y & SubChunk::COORD_MASK;
$lz = $z & SubChunk::COORD_MASK;
$current = $lightArray->get($lx, $ly, $lz);
$potentialLight = $newAdjacentLevel - ($this->lightFilters[$subChunk->getBlockStateId($lx, $ly, $lz)] ?? self::BASE_LIGHT_FILTER);
if($current < $potentialLight){
$lightArray->set($lx, $ly, $lz, $potentialLight);
if(!isset($context->spreadVisited[$index = World::blockHash($x, $y, $z)]) && $potentialLight > 1){
//Track where this node was lit from, to avoid checking the source again when we propagate from here
//TODO: In the future it might be worth tracking more than one adjacent source face in case multiple
//nodes try to light the same node. However, this is a rare case since the vast majority of calls are
//basic propagation with only one source anyway.
$context->spreadVisited[$index] = Facing::opposite($side);
$context->spreadQueue->enqueue([$x, $y, $z]);
}
}
}
}