From 7ec548774c093ddbaf20abb470c4a085cae5f320 Mon Sep 17 00:00:00 2001 From: "Dylan T." Date: Sat, 27 Sep 2025 18:51:15 +0100 Subject: [PATCH] Limit SubChunk to 2 layers, avoid arrays (#6747) Initially proposed in #6575 This shows about a 10% performance improvement to both SubChunk->getBlockStateId() and SubChunk->setBlockStateId(), so definitely worth doing. It does result in increased complexity, but for a double digits performance gain, I think it's worth it. Closes #6575 --- src/world/format/Chunk.php | 4 +- src/world/format/SubChunk.php | 73 +++++++++++-------- src/world/format/io/FastChunkSerializer.php | 12 +-- src/world/format/io/leveldb/LevelDB.php | 45 ++++++------ src/world/format/io/region/Anvil.php | 4 +- .../io/region/LegacyAnvilChunkTrait.php | 2 +- src/world/format/io/region/McRegion.php | 7 +- src/world/format/io/region/PMAnvil.php | 4 +- src/world/light/BlockLightUpdate.php | 27 +++++-- tests/phpunit/world/format/SubChunkTest.php | 2 +- 10 files changed, 104 insertions(+), 76 deletions(-) diff --git a/src/world/format/Chunk.php b/src/world/format/Chunk.php index 9ea5d3f8e..b72d89492 100644 --- a/src/world/format/Chunk.php +++ b/src/world/format/Chunk.php @@ -73,7 +73,7 @@ class Chunk{ foreach($this->subChunks as $y => $null){ //TODO: we should probably require all subchunks to be provided here - $this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(Block::EMPTY_STATE_ID, [], new PalettedBlockArray(BiomeIds::OCEAN)); + $this->subChunks[$y] = $subChunks[$y + self::MIN_SUBCHUNK_INDEX] ?? new SubChunk(Block::EMPTY_STATE_ID, null, null, new PalettedBlockArray(BiomeIds::OCEAN)); } $val = (self::MAX_SUBCHUNK_INDEX + 1) * SubChunk::EDGE_LENGTH; @@ -298,7 +298,7 @@ class Chunk{ throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y"); } - $this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(Block::EMPTY_STATE_ID, [], new PalettedBlockArray(BiomeIds::OCEAN)); + $this->subChunks[$y - self::MIN_SUBCHUNK_INDEX] = $subChunk ?? new SubChunk(Block::EMPTY_STATE_ID, null, null, new PalettedBlockArray(BiomeIds::OCEAN)); $this->terrainDirtyFlags |= self::DIRTY_FLAG_BLOCKS; } diff --git a/src/world/format/SubChunk.php b/src/world/format/SubChunk.php index cc6673430..58ada37c8 100644 --- a/src/world/format/SubChunk.php +++ b/src/world/format/SubChunk.php @@ -23,9 +23,6 @@ declare(strict_types=1); namespace pocketmine\world\format; -use function array_map; -use function count; - class SubChunk{ public const COORD_BIT_SIZE = 4; public const COORD_MASK = ~(~0 << self::COORD_BIT_SIZE); @@ -33,13 +30,11 @@ class SubChunk{ /** * SubChunk constructor. - * - * @param PalettedBlockArray[] $blockLayers - * @phpstan-param list $blockLayers */ public function __construct( private int $emptyBlockId, - private array $blockLayers, + private ?PalettedBlockArray $blockLayer0, + private ?PalettedBlockArray $blockLayer1, private PalettedBlockArray $biomes, private ?LightArray $skyLight = null, private ?LightArray $blockLight = null @@ -60,7 +55,7 @@ class SubChunk{ * This may report non-empty erroneously if the chunk has been modified and not garbage-collected. */ public function isEmptyFast() : bool{ - return count($this->blockLayers) === 0; + return $this->blockLayer0 === null && $this->blockLayer1 === null; } /** @@ -70,33 +65,45 @@ class SubChunk{ public function getEmptyBlockId() : int{ return $this->emptyBlockId; } public function getBlockStateId(int $x, int $y, int $z) : int{ - if(count($this->blockLayers) === 0){ - return $this->emptyBlockId; - } - return $this->blockLayers[0]->get($x, $y, $z); + return $this->blockLayer0?->get($x, $y, $z) ?? $this->emptyBlockId; } public function setBlockStateId(int $x, int $y, int $z, int $block) : void{ - if(count($this->blockLayers) === 0){ - $this->blockLayers[] = new PalettedBlockArray($this->emptyBlockId); + if($this->blockLayer0 === null){ + $this->blockLayer0 = new PalettedBlockArray($this->emptyBlockId); } - $this->blockLayers[0]->set($x, $y, $z, $block); + $this->blockLayer0->set($x, $y, $z, $block); + } + + public function getBlockLayer0() : ?PalettedBlockArray{ + return $this->blockLayer0; + } + + public function getBlockLayer1() : ?PalettedBlockArray{ + return $this->blockLayer1; } /** * @return PalettedBlockArray[] - * @phpstan-return list + * @phpstan-return array{}|array{PalettedBlockArray}|array{PalettedBlockArray, PalettedBlockArray} */ public function getBlockLayers() : array{ - return $this->blockLayers; + $layers = []; + if($this->blockLayer0 !== null){ + $layers[] = $this->blockLayer0; + } + if($this->blockLayer1 !== null){ + $layers[] = $this->blockLayer1; + } + return $layers; } public function getHighestBlockAt(int $x, int $z) : ?int{ - if(count($this->blockLayers) === 0){ + if($this->blockLayer0 === null){ return null; } for($y = self::EDGE_LENGTH - 1; $y >= 0; --$y){ - if($this->blockLayers[0]->get($x, $y, $z) !== $this->emptyBlockId){ + if($this->blockLayer0->get($x, $y, $z) !== $this->emptyBlockId){ return $y; } } @@ -129,16 +136,21 @@ class SubChunk{ return []; } - public function collectGarbage() : void{ - $cleanedLayers = []; - foreach($this->blockLayers as $layer){ - $layer->collectGarbage(); - - if($layer->getBitsPerBlock() !== 0 || $layer->get(0, 0, 0) !== $this->emptyBlockId){ - $cleanedLayers[] = $layer; - } + private static function gcBlockPalette(?PalettedBlockArray $layer, int $emptyBlockId) : ?PalettedBlockArray{ + if($layer === null){ + return null; + } + $layer->collectGarbage(); + return $layer->getBitsPerBlock() === 0 && $layer->get(0, 0, 0) === $emptyBlockId ? null : $layer; + } + + public function collectGarbage() : void{ + $this->blockLayer0 = self::gcBlockPalette($this->blockLayer0, $this->emptyBlockId); + $this->blockLayer1 = self::gcBlockPalette($this->blockLayer1, $this->emptyBlockId); + if($this->blockLayer0 === null && $this->blockLayer1 !== null){ + $this->blockLayer0 = $this->blockLayer1; + $this->blockLayer1 = null; } - $this->blockLayers = $cleanedLayers; $this->biomes->collectGarbage(); if($this->skyLight !== null && $this->skyLight->isUniform(0)){ @@ -150,9 +162,8 @@ class SubChunk{ } public function __clone(){ - $this->blockLayers = array_map(function(PalettedBlockArray $array) : PalettedBlockArray{ - return clone $array; - }, $this->blockLayers); + $this->blockLayer0 = $this->blockLayer0 !== null ? clone $this->blockLayer0 : null; + $this->blockLayer1 = $this->blockLayer1 !== null ? clone $this->blockLayer1 : null; $this->biomes = clone $this->biomes; if($this->skyLight !== null){ diff --git a/src/world/format/io/FastChunkSerializer.php b/src/world/format/io/FastChunkSerializer.php index a186ec07f..0f026f557 100644 --- a/src/world/format/io/FastChunkSerializer.php +++ b/src/world/format/io/FastChunkSerializer.php @@ -81,7 +81,6 @@ final class FastChunkSerializer{ self::serializePalettedArray($stream, $blocks); } self::serializePalettedArray($stream, $subChunk->getBiomeArray()); - } return $stream->getData(); @@ -115,12 +114,15 @@ final class FastChunkSerializer{ //TODO: why the heck are we using big-endian here? $airBlockId = BE::readUnsignedInt($stream); - $layers = []; - for($i = 0, $layerCount = Byte::readUnsigned($stream); $i < $layerCount; ++$i){ - $layers[] = self::deserializePalettedArray($stream); + $layerCount = Byte::readUnsigned($stream); + if($layerCount > 2){ + throw new \UnexpectedValueException("Expected at most 2 layers, but got $layerCount"); } + $layer0 = $layerCount >= 1 ? self::deserializePalettedArray($stream) : null; + $layer1 = $layerCount === 2 ? self::deserializePalettedArray($stream) : null; + $biomeArray = self::deserializePalettedArray($stream); - $subChunks[$y] = new SubChunk($airBlockId, $layers, $biomeArray); + $subChunks[$y] = new SubChunk($airBlockId, $layer0, $layer1, $biomeArray); } return new Chunk($subChunks, $terrainPopulated); diff --git a/src/world/format/io/leveldb/LevelDB.php b/src/world/format/io/leveldb/LevelDB.php index 3a64f93f6..93862afd2 100644 --- a/src/world/format/io/leveldb/LevelDB.php +++ b/src/world/format/io/leveldb/LevelDB.php @@ -462,17 +462,15 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $subChunks = []; for($yy = 0; $yy < 8; ++$yy){ - $storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, new \PrefixedLogger($logger, "Subchunk y=$yy"))]; - if(isset($convertedLegacyExtraData[$yy])){ - $storages[] = $convertedLegacyExtraData[$yy]; - } - $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $storages, clone $biomes3d); + $layer0 = $this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, new \PrefixedLogger($logger, "Subchunk y=$yy")); + $layer1 = $convertedLegacyExtraData[$yy] ?? null; + $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $layer0, $layer1, clone $biomes3d); } //make sure extrapolated biomes get filled in correctly for($yy = Chunk::MIN_SUBCHUNK_INDEX; $yy <= Chunk::MAX_SUBCHUNK_INDEX; ++$yy){ if(!isset($subChunks[$yy])){ - $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d); + $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, null, null, clone $biomes3d); } } @@ -501,12 +499,10 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ } } - $storages = [$this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger)]; - if($convertedLegacyExtraData !== null){ - $storages[] = $convertedLegacyExtraData; - } + $layer0 = $this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger); + $layer1 = $convertedLegacyExtraData; - return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette); + return new SubChunk(Block::EMPTY_STATE_ID, $layer0, $layer1, $biomePalette); } /** @@ -526,11 +522,9 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ case SubChunkVersion::CLASSIC_BUG_7: return $this->deserializeNonPalettedSubChunkData($binaryStream, $chunkVersion, $convertedLegacyExtraData, $biomePalette, $logger); case SubChunkVersion::PALETTED_SINGLE: - $storages = [$this->deserializeBlockPalette($binaryStream, $logger)]; - if($convertedLegacyExtraData !== null){ - $storages[] = $convertedLegacyExtraData; - } - return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette); + $layer0 = $this->deserializeBlockPalette($binaryStream, $logger); + $layer1 = $convertedLegacyExtraData; + return new SubChunk(Block::EMPTY_STATE_ID, $layer0, $layer1, $biomePalette); case SubChunkVersion::PALETTED_MULTI: case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET: //legacy extradata layers intentionally ignored because they aren't supposed to exist in v8 @@ -541,11 +535,18 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $binaryStream->getByte(); } - $storages = []; + $layer0 = null; + $layer1 = null; for($k = 0; $k < $storageCount; ++$k){ - $storages[] = $this->deserializeBlockPalette($binaryStream, $logger); + $layer = $this->deserializeBlockPalette($binaryStream, $logger); + if($k === 0){ + $layer0 = $layer; + }elseif($k === 1){ + $layer1 = $layer; + } + // Ignore additional layers beyond the first two } - return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette); + return new SubChunk(Block::EMPTY_STATE_ID, $layer0, $layer1, $biomePalette); default: //this should never happen - an unsupported chunk appearing in a supported world is a sign of corruption throw new CorruptedChunkException("don't know how to decode LevelDB subchunk format version $subChunkVersion"); @@ -575,7 +576,7 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0; for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ if(($data = $this->db->get($index . ChunkDataKey::SUBCHUNK . chr($y + $subChunkKeyOffset))) === false){ - $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [], $biomeArrays[$y]); + $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, null, null, $biomeArrays[$y]); continue; } @@ -776,8 +777,8 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{ $layers = $subChunk->getBlockLayers(); $subStream->putByte(count($layers)); - foreach($layers as $blocks){ - $this->serializeBlockPalette($subStream, $blocks); + foreach($layers as $layer){ + $this->serializeBlockPalette($subStream, $layer); } $write->put($key, $subStream->getBuffer()); diff --git a/src/world/format/io/region/Anvil.php b/src/world/format/io/region/Anvil.php index 2c14b54e8..8b0c1e4dd 100644 --- a/src/world/format/io/region/Anvil.php +++ b/src/world/format/io/region/Anvil.php @@ -32,11 +32,11 @@ class Anvil extends RegionWorldProvider{ use LegacyAnvilChunkTrait; protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d, \Logger $logger) : SubChunk{ - return new SubChunk(Block::EMPTY_STATE_ID, [$this->palettizeLegacySubChunkYZX( + return new SubChunk(Block::EMPTY_STATE_ID, $this->palettizeLegacySubChunkYZX( self::readFixedSizeByteArray($subChunk, "Blocks", 4096), self::readFixedSizeByteArray($subChunk, "Data", 2048), $logger - )], $biomes3d); + ), null, $biomes3d); //ignore legacy light information } diff --git a/src/world/format/io/region/LegacyAnvilChunkTrait.php b/src/world/format/io/region/LegacyAnvilChunkTrait.php index 05c79ea63..43618a22d 100644 --- a/src/world/format/io/region/LegacyAnvilChunkTrait.php +++ b/src/world/format/io/region/LegacyAnvilChunkTrait.php @@ -94,7 +94,7 @@ trait LegacyAnvilChunkTrait{ } for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ if(!isset($subChunks[$y])){ - $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d); + $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, null, null, clone $biomes3d); } } diff --git a/src/world/format/io/region/McRegion.php b/src/world/format/io/region/McRegion.php index ad6a2d7f2..55c64f1a8 100644 --- a/src/world/format/io/region/McRegion.php +++ b/src/world/format/io/region/McRegion.php @@ -90,16 +90,17 @@ class McRegion extends RegionWorldProvider{ $fullData = self::readFixedSizeByteArray($chunk, "Data", 16384); for($y = 0; $y < 8; ++$y){ - $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [$this->palettizeLegacySubChunkFromColumn( + $blockLayer = $this->palettizeLegacySubChunkFromColumn( $fullIds, $fullData, $y, new \PrefixedLogger($logger, "Subchunk y=$y"), - )], clone $biomes3d); + ); + $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, $blockLayer, null, clone $biomes3d); } for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ if(!isset($subChunks[$y])){ - $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [], clone $biomes3d); + $subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, null, null, clone $biomes3d); } } diff --git a/src/world/format/io/region/PMAnvil.php b/src/world/format/io/region/PMAnvil.php index 4fb463124..dfb8e6683 100644 --- a/src/world/format/io/region/PMAnvil.php +++ b/src/world/format/io/region/PMAnvil.php @@ -36,11 +36,11 @@ class PMAnvil extends RegionWorldProvider{ use LegacyAnvilChunkTrait; protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d, \Logger $logger) : SubChunk{ - return new SubChunk(Block::EMPTY_STATE_ID, [$this->palettizeLegacySubChunkXZY( + return new SubChunk(Block::EMPTY_STATE_ID, $this->palettizeLegacySubChunkXZY( self::readFixedSizeByteArray($subChunk, "Blocks", 4096), self::readFixedSizeByteArray($subChunk, "Data", 2048), $logger - )], $biomes3d); + ), null, $biomes3d); } protected static function getRegionFileExtension() : string{ diff --git a/src/world/light/BlockLightUpdate.php b/src/world/light/BlockLightUpdate.php index b7e0aa9e3..04abc9e2a 100644 --- a/src/world/light/BlockLightUpdate.php +++ b/src/world/light/BlockLightUpdate.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\world\light; use pocketmine\world\format\LightArray; +use pocketmine\world\format\PalettedBlockArray; use pocketmine\world\format\SubChunk; use pocketmine\world\utils\SubChunkExplorer; use pocketmine\world\utils\SubChunkExplorerStatus; @@ -55,6 +56,20 @@ class BlockLightUpdate extends LightUpdate{ } } + private function layerHasLightEmitter(?PalettedBlockArray $layer) : bool{ + if($layer === null){ + return false; + } + + foreach($layer->getPalette() as $state){ + if(($this->lightEmitters[$state] ?? 0) > 0){ + return true; + } + } + + return false; + } + public function recalculateChunk(int $chunkX, int $chunkZ) : int{ if($this->subChunkExplorer->moveToChunk($chunkX, 0, $chunkZ) === SubChunkExplorerStatus::INVALID){ throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not exist"); @@ -65,13 +80,11 @@ class BlockLightUpdate extends LightUpdate{ foreach($chunk->getSubChunks() as $subChunkY => $subChunk){ $subChunk->setBlockLightArray(LightArray::fill(0)); - foreach($subChunk->getBlockLayers() as $layer){ - foreach($layer->getPalette() as $state){ - if(($this->lightEmitters[$state] ?? 0) > 0){ - $lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << SubChunk::COORD_BIT_SIZE, $subChunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE); - break 2; - } - } + if( + $this->layerHasLightEmitter($subChunk->getBlockLayer0()) || + $this->layerHasLightEmitter($subChunk->getBlockLayer1()) + ){ + $lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << SubChunk::COORD_BIT_SIZE, $subChunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE); } } diff --git a/tests/phpunit/world/format/SubChunkTest.php b/tests/phpunit/world/format/SubChunkTest.php index cdb440147..9d8625ff7 100644 --- a/tests/phpunit/world/format/SubChunkTest.php +++ b/tests/phpunit/world/format/SubChunkTest.php @@ -32,7 +32,7 @@ class SubChunkTest extends TestCase{ * Test that a cloned SubChunk instance doesn't influence the original */ public function testClone() : void{ - $sub1 = new SubChunk(0, [], new PalettedBlockArray(BiomeIds::OCEAN)); + $sub1 = new SubChunk(0, null, null, new PalettedBlockArray(BiomeIds::OCEAN)); $sub1->setBlockStateId(0, 0, 0, 1); $sub1->getBlockLightArray()->set(0, 0, 0, 1);