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
This commit is contained in:
Dylan T.
2025-09-27 18:51:15 +01:00
committed by GitHub
parent 9eadf59bf5
commit 7ec548774c
10 changed files with 104 additions and 76 deletions

View File

@@ -73,7 +73,7 @@ class Chunk{
foreach($this->subChunks as $y => $null){ foreach($this->subChunks as $y => $null){
//TODO: we should probably require all subchunks to be provided here //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; $val = (self::MAX_SUBCHUNK_INDEX + 1) * SubChunk::EDGE_LENGTH;
@@ -298,7 +298,7 @@ class Chunk{
throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y"); 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; $this->terrainDirtyFlags |= self::DIRTY_FLAG_BLOCKS;
} }

View File

@@ -23,9 +23,6 @@ declare(strict_types=1);
namespace pocketmine\world\format; namespace pocketmine\world\format;
use function array_map;
use function count;
class SubChunk{ class SubChunk{
public const COORD_BIT_SIZE = 4; public const COORD_BIT_SIZE = 4;
public const COORD_MASK = ~(~0 << self::COORD_BIT_SIZE); public const COORD_MASK = ~(~0 << self::COORD_BIT_SIZE);
@@ -33,13 +30,11 @@ class SubChunk{
/** /**
* SubChunk constructor. * SubChunk constructor.
*
* @param PalettedBlockArray[] $blockLayers
* @phpstan-param list<PalettedBlockArray> $blockLayers
*/ */
public function __construct( public function __construct(
private int $emptyBlockId, private int $emptyBlockId,
private array $blockLayers, private ?PalettedBlockArray $blockLayer0,
private ?PalettedBlockArray $blockLayer1,
private PalettedBlockArray $biomes, private PalettedBlockArray $biomes,
private ?LightArray $skyLight = null, private ?LightArray $skyLight = null,
private ?LightArray $blockLight = 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. * This may report non-empty erroneously if the chunk has been modified and not garbage-collected.
*/ */
public function isEmptyFast() : bool{ 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 getEmptyBlockId() : int{ return $this->emptyBlockId; }
public function getBlockStateId(int $x, int $y, int $z) : int{ public function getBlockStateId(int $x, int $y, int $z) : int{
if(count($this->blockLayers) === 0){ return $this->blockLayer0?->get($x, $y, $z) ?? $this->emptyBlockId;
return $this->emptyBlockId;
}
return $this->blockLayers[0]->get($x, $y, $z);
} }
public function setBlockStateId(int $x, int $y, int $z, int $block) : void{ public function setBlockStateId(int $x, int $y, int $z, int $block) : void{
if(count($this->blockLayers) === 0){ if($this->blockLayer0 === null){
$this->blockLayers[] = new PalettedBlockArray($this->emptyBlockId); $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[] * @return PalettedBlockArray[]
* @phpstan-return list<PalettedBlockArray> * @phpstan-return array{}|array{PalettedBlockArray}|array{PalettedBlockArray, PalettedBlockArray}
*/ */
public function getBlockLayers() : array{ 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{ public function getHighestBlockAt(int $x, int $z) : ?int{
if(count($this->blockLayers) === 0){ if($this->blockLayer0 === null){
return null; return null;
} }
for($y = self::EDGE_LENGTH - 1; $y >= 0; --$y){ 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; return $y;
} }
} }
@@ -129,16 +136,21 @@ class SubChunk{
return []; return [];
} }
public function collectGarbage() : void{ private static function gcBlockPalette(?PalettedBlockArray $layer, int $emptyBlockId) : ?PalettedBlockArray{
$cleanedLayers = []; if($layer === null){
foreach($this->blockLayers as $layer){ return null;
}
$layer->collectGarbage(); $layer->collectGarbage();
return $layer->getBitsPerBlock() === 0 && $layer->get(0, 0, 0) === $emptyBlockId ? null : $layer;
}
if($layer->getBitsPerBlock() !== 0 || $layer->get(0, 0, 0) !== $this->emptyBlockId){ public function collectGarbage() : void{
$cleanedLayers[] = $layer; $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(); $this->biomes->collectGarbage();
if($this->skyLight !== null && $this->skyLight->isUniform(0)){ if($this->skyLight !== null && $this->skyLight->isUniform(0)){
@@ -150,9 +162,8 @@ class SubChunk{
} }
public function __clone(){ public function __clone(){
$this->blockLayers = array_map(function(PalettedBlockArray $array) : PalettedBlockArray{ $this->blockLayer0 = $this->blockLayer0 !== null ? clone $this->blockLayer0 : null;
return clone $array; $this->blockLayer1 = $this->blockLayer1 !== null ? clone $this->blockLayer1 : null;
}, $this->blockLayers);
$this->biomes = clone $this->biomes; $this->biomes = clone $this->biomes;
if($this->skyLight !== null){ if($this->skyLight !== null){

View File

@@ -81,7 +81,6 @@ final class FastChunkSerializer{
self::serializePalettedArray($stream, $blocks); self::serializePalettedArray($stream, $blocks);
} }
self::serializePalettedArray($stream, $subChunk->getBiomeArray()); self::serializePalettedArray($stream, $subChunk->getBiomeArray());
} }
return $stream->getData(); return $stream->getData();
@@ -115,12 +114,15 @@ final class FastChunkSerializer{
//TODO: why the heck are we using big-endian here? //TODO: why the heck are we using big-endian here?
$airBlockId = BE::readUnsignedInt($stream); $airBlockId = BE::readUnsignedInt($stream);
$layers = []; $layerCount = Byte::readUnsigned($stream);
for($i = 0, $layerCount = Byte::readUnsigned($stream); $i < $layerCount; ++$i){ if($layerCount > 2){
$layers[] = self::deserializePalettedArray($stream); 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); $biomeArray = self::deserializePalettedArray($stream);
$subChunks[$y] = new SubChunk($airBlockId, $layers, $biomeArray); $subChunks[$y] = new SubChunk($airBlockId, $layer0, $layer1, $biomeArray);
} }
return new Chunk($subChunks, $terrainPopulated); return new Chunk($subChunks, $terrainPopulated);

View File

@@ -462,17 +462,15 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$subChunks = []; $subChunks = [];
for($yy = 0; $yy < 8; ++$yy){ for($yy = 0; $yy < 8; ++$yy){
$storages = [$this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, new \PrefixedLogger($logger, "Subchunk y=$yy"))]; $layer0 = $this->palettizeLegacySubChunkFromColumn($fullIds, $fullData, $yy, new \PrefixedLogger($logger, "Subchunk y=$yy"));
if(isset($convertedLegacyExtraData[$yy])){ $layer1 = $convertedLegacyExtraData[$yy] ?? null;
$storages[] = $convertedLegacyExtraData[$yy]; $subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $layer0, $layer1, clone $biomes3d);
}
$subChunks[$yy] = new SubChunk(Block::EMPTY_STATE_ID, $storages, clone $biomes3d);
} }
//make sure extrapolated biomes get filled in correctly //make sure extrapolated biomes get filled in correctly
for($yy = Chunk::MIN_SUBCHUNK_INDEX; $yy <= Chunk::MAX_SUBCHUNK_INDEX; ++$yy){ for($yy = Chunk::MIN_SUBCHUNK_INDEX; $yy <= Chunk::MAX_SUBCHUNK_INDEX; ++$yy){
if(!isset($subChunks[$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)]; $layer0 = $this->palettizeLegacySubChunkXZY($blocks, $blockData, $logger);
if($convertedLegacyExtraData !== null){ $layer1 = $convertedLegacyExtraData;
$storages[] = $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: case SubChunkVersion::CLASSIC_BUG_7:
return $this->deserializeNonPalettedSubChunkData($binaryStream, $chunkVersion, $convertedLegacyExtraData, $biomePalette, $logger); return $this->deserializeNonPalettedSubChunkData($binaryStream, $chunkVersion, $convertedLegacyExtraData, $biomePalette, $logger);
case SubChunkVersion::PALETTED_SINGLE: case SubChunkVersion::PALETTED_SINGLE:
$storages = [$this->deserializeBlockPalette($binaryStream, $logger)]; $layer0 = $this->deserializeBlockPalette($binaryStream, $logger);
if($convertedLegacyExtraData !== null){ $layer1 = $convertedLegacyExtraData;
$storages[] = $convertedLegacyExtraData; return new SubChunk(Block::EMPTY_STATE_ID, $layer0, $layer1, $biomePalette);
}
return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette);
case SubChunkVersion::PALETTED_MULTI: case SubChunkVersion::PALETTED_MULTI:
case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET: case SubChunkVersion::PALETTED_MULTI_WITH_OFFSET:
//legacy extradata layers intentionally ignored because they aren't supposed to exist in v8 //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(); $binaryStream->getByte();
} }
$storages = []; $layer0 = null;
$layer1 = null;
for($k = 0; $k < $storageCount; ++$k){ 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;
} }
return new SubChunk(Block::EMPTY_STATE_ID, $storages, $biomePalette); // Ignore additional layers beyond the first two
}
return new SubChunk(Block::EMPTY_STATE_ID, $layer0, $layer1, $biomePalette);
default: default:
//this should never happen - an unsupported chunk appearing in a supported world is a sign of corruption //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"); 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; $subChunkKeyOffset = self::hasOffsetCavesAndCliffsSubChunks($chunkVersion) ? self::CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET : 0;
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(($data = $this->db->get($index . ChunkDataKey::SUBCHUNK . chr($y + $subChunkKeyOffset))) === false){ 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; continue;
} }
@@ -776,8 +777,8 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$layers = $subChunk->getBlockLayers(); $layers = $subChunk->getBlockLayers();
$subStream->putByte(count($layers)); $subStream->putByte(count($layers));
foreach($layers as $blocks){ foreach($layers as $layer){
$this->serializeBlockPalette($subStream, $blocks); $this->serializeBlockPalette($subStream, $layer);
} }
$write->put($key, $subStream->getBuffer()); $write->put($key, $subStream->getBuffer());

View File

@@ -32,11 +32,11 @@ class Anvil extends RegionWorldProvider{
use LegacyAnvilChunkTrait; use LegacyAnvilChunkTrait;
protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d, \Logger $logger) : SubChunk{ 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, "Blocks", 4096),
self::readFixedSizeByteArray($subChunk, "Data", 2048), self::readFixedSizeByteArray($subChunk, "Data", 2048),
$logger $logger
)], $biomes3d); ), null, $biomes3d);
//ignore legacy light information //ignore legacy light information
} }

View File

@@ -94,7 +94,7 @@ trait LegacyAnvilChunkTrait{
} }
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){ for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(!isset($subChunks[$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);
} }
} }

View File

@@ -90,16 +90,17 @@ class McRegion extends RegionWorldProvider{
$fullData = self::readFixedSizeByteArray($chunk, "Data", 16384); $fullData = self::readFixedSizeByteArray($chunk, "Data", 16384);
for($y = 0; $y < 8; ++$y){ for($y = 0; $y < 8; ++$y){
$subChunks[$y] = new SubChunk(Block::EMPTY_STATE_ID, [$this->palettizeLegacySubChunkFromColumn( $blockLayer = $this->palettizeLegacySubChunkFromColumn(
$fullIds, $fullIds,
$fullData, $fullData,
$y, $y,
new \PrefixedLogger($logger, "Subchunk y=$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){ for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
if(!isset($subChunks[$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);
} }
} }

View File

@@ -36,11 +36,11 @@ class PMAnvil extends RegionWorldProvider{
use LegacyAnvilChunkTrait; use LegacyAnvilChunkTrait;
protected function deserializeSubChunk(CompoundTag $subChunk, PalettedBlockArray $biomes3d, \Logger $logger) : SubChunk{ 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, "Blocks", 4096),
self::readFixedSizeByteArray($subChunk, "Data", 2048), self::readFixedSizeByteArray($subChunk, "Data", 2048),
$logger $logger
)], $biomes3d); ), null, $biomes3d);
} }
protected static function getRegionFileExtension() : string{ protected static function getRegionFileExtension() : string{

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\world\light; namespace pocketmine\world\light;
use pocketmine\world\format\LightArray; use pocketmine\world\format\LightArray;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk; use pocketmine\world\format\SubChunk;
use pocketmine\world\utils\SubChunkExplorer; use pocketmine\world\utils\SubChunkExplorer;
use pocketmine\world\utils\SubChunkExplorerStatus; 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{ public function recalculateChunk(int $chunkX, int $chunkZ) : int{
if($this->subChunkExplorer->moveToChunk($chunkX, 0, $chunkZ) === SubChunkExplorerStatus::INVALID){ if($this->subChunkExplorer->moveToChunk($chunkX, 0, $chunkZ) === SubChunkExplorerStatus::INVALID){
throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not exist"); throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not exist");
@@ -65,13 +80,11 @@ class BlockLightUpdate extends LightUpdate{
foreach($chunk->getSubChunks() as $subChunkY => $subChunk){ foreach($chunk->getSubChunks() as $subChunkY => $subChunk){
$subChunk->setBlockLightArray(LightArray::fill(0)); $subChunk->setBlockLightArray(LightArray::fill(0));
foreach($subChunk->getBlockLayers() as $layer){ if(
foreach($layer->getPalette() as $state){ $this->layerHasLightEmitter($subChunk->getBlockLayer0()) ||
if(($this->lightEmitters[$state] ?? 0) > 0){ $this->layerHasLightEmitter($subChunk->getBlockLayer1())
){
$lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << SubChunk::COORD_BIT_SIZE, $subChunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE); $lightSources += $this->scanForLightEmittingBlocks($subChunk, $chunkX << SubChunk::COORD_BIT_SIZE, $subChunkY << SubChunk::COORD_BIT_SIZE, $chunkZ << SubChunk::COORD_BIT_SIZE);
break 2;
}
}
} }
} }

View File

@@ -32,7 +32,7 @@ class SubChunkTest extends TestCase{
* Test that a cloned SubChunk instance doesn't influence the original * Test that a cloned SubChunk instance doesn't influence the original
*/ */
public function testClone() : void{ 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->setBlockStateId(0, 0, 0, 1);
$sub1->getBlockLightArray()->set(0, 0, 0, 1); $sub1->getBlockLightArray()->set(0, 0, 0, 1);