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){
//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;
}

View File

@@ -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<PalettedBlockArray> $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<PalettedBlockArray>
* @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){

View File

@@ -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);

View File

@@ -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());

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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{

View File

@@ -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);
}
}

View File

@@ -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);