Extract GeneratorExecutor system from World, v2 (#6682)

- `AsyncGeneratorExecutor` class added that encapsulates the logic of generating chunks using async tasks as previously
- `GeneratorExecutor` interface added that can be implemented to provide chunks in other ways
- `SyncGeneratorExecutor` which invokes the generator directly on the main thread, useful for simple generators like `Flat` where async tasks are not needed
- Some redundant APIs were removed from `World` (these will probably come back as deprecated stubs for the remainder of 5.x, but I was having too much fun deleting code)
- Removed internal `World->registerGeneratorToWorker()` (no longer useful)
- `World` now invokes generator executor instead of posting AsyncTasks directly
- Some internal classes moved to `pocketmine\world\generator\executor` (PopulationTask excluded because plugins use it in lieu of being able to regenerate chunks
- Generators can opt into main-thread execution by setting the `$fast` parameter to `true` in `GeneratorManager::register()`
This commit is contained in:
Dylan T. 2025-05-27 21:51:10 +01:00 committed by GitHub
parent dd17adeaaf
commit 059f4ee7bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 318 additions and 94 deletions

View File

@ -93,9 +93,11 @@ use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\WritableWorldProvider;
use pocketmine\world\format\LightArray;
use pocketmine\world\format\SubChunk;
use pocketmine\world\generator\executor\AsyncGeneratorExecutor;
use pocketmine\world\generator\executor\GeneratorExecutor;
use pocketmine\world\generator\executor\GeneratorExecutorSetupParameters;
use pocketmine\world\generator\executor\SyncGeneratorExecutor;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\generator\GeneratorRegisterTask;
use pocketmine\world\generator\GeneratorUnregisterTask;
use pocketmine\world\generator\PopulationTask;
use pocketmine\world\light\BlockLightUpdate;
use pocketmine\world\light\LightPopulationTask;
@ -336,11 +338,7 @@ class World implements ChunkManager{
*/
private array $chunkPopulationRequestQueueIndex = [];
/**
* @var true[]
* @phpstan-var array<int, true>
*/
private array $generatorRegisteredWorkers = [];
private readonly GeneratorExecutor $generatorExecutor;
private bool $autoSave = true;
@ -360,9 +358,6 @@ class World implements ChunkManager{
private bool $doingTick = false;
/** @phpstan-var class-string<generator\Generator> */
private string $generator;
private bool $unloaded = false;
/**
* @var \Closure[]
@ -498,7 +493,23 @@ class World implements ChunkManager{
$generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
$generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
$this->generator = $generator->getGeneratorClass();
$executorSetupParameters = new GeneratorExecutorSetupParameters(
worldMinY: $this->minY,
worldMaxY: $this->maxY,
generatorSeed: $this->getSeed(),
generatorClass: $generator->getGeneratorClass(),
generatorSettings: $this->provider->getWorldData()->getGeneratorOptions()
);
$this->generatorExecutor = $generator->isFast() ?
new SyncGeneratorExecutor($executorSetupParameters) :
new AsyncGeneratorExecutor(
$this->logger,
$this->workerPool,
$executorSetupParameters,
$this->worldId
);
$this->chunkPopulationRequestQueue = new \SplQueue();
$this->addOnUnloadCallback(function() : void{
$this->logger->debug("Cancelling unfulfilled generation requests");
@ -534,17 +545,6 @@ class World implements ChunkManager{
$this->initRandomTickBlocksFromConfig($cfg);
$this->timings = new WorldTimings($this);
$this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{
if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
$this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
unset($this->generatorRegisteredWorkers[$workerId]);
}
});
$workerPool = $this->workerPool;
$this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{
$workerPool->removeWorkerStartHook($workerStartHook);
});
}
private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
@ -585,21 +585,6 @@ class World implements ChunkManager{
return $this->tickRateTime;
}
public function registerGeneratorToWorker(int $worker) : void{
$this->logger->debug("Registering generator on worker $worker");
$this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker);
$this->generatorRegisteredWorkers[$worker] = true;
}
public function unregisterGenerator() : void{
foreach($this->workerPool->getRunningWorkers() as $i){
if(isset($this->generatorRegisteredWorkers[$i])){
$this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i);
}
}
$this->generatorRegisteredWorkers = [];
}
public function getServer() : Server{
return $this->server;
}
@ -657,7 +642,7 @@ class World implements ChunkManager{
$this->save();
$this->unregisterGenerator();
$this->generatorExecutor->shutdown();
$this->provider->close();
$this->blockCache = [];
@ -3486,8 +3471,8 @@ class World implements ChunkManager{
$centerChunk = $this->loadChunk($chunkX, $chunkZ);
$adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
$task = new PopulationTask(
$this->worldId,
$this->generatorExecutor->populate(
$chunkX,
$chunkZ,
$centerChunk,
@ -3500,15 +3485,6 @@ class World implements ChunkManager{
$this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
}
);
$workerId = $this->workerPool->selectWorker();
if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
$this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
unset($this->generatorRegisteredWorkers[$workerId]);
}
if(!isset($this->generatorRegisteredWorkers[$workerId])){
$this->registerGeneratorToWorker($workerId);
}
$this->workerPool->submitTaskToWorker($task, $workerId);
return $resolver->getPromise();
}finally{

View File

@ -50,7 +50,7 @@ final class GeneratorManager{
}catch(InvalidGeneratorOptionsException $e){
return $e;
}
});
}, fast: true);
$this->addGenerator(Normal::class, "normal", fn() => null);
$this->addAlias("normal", "default");
$this->addGenerator(Nether::class, "nether", fn() => null);
@ -62,6 +62,7 @@ final class GeneratorManager{
* @param string $name Alias for this generator type that can be written in configs
* @param \Closure $presetValidator Callback to validate generator options for new worlds
* @param bool $overwrite Whether to force overwriting any existing registered generator with the same name
* @param bool $fast Whether this generator is fast enough to run without async tasks
*
* @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator
*
@ -69,7 +70,7 @@ final class GeneratorManager{
*
* @throws \InvalidArgumentException
*/
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false) : void{
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false, bool $fast = false) : void{
Utils::testValidInstance($class, Generator::class);
$name = strtolower($name);
@ -77,7 +78,7 @@ final class GeneratorManager{
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");
}
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator);
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator, $fast);
}
/**

View File

@ -31,12 +31,15 @@ final class GeneratorManagerEntry{
*/
public function __construct(
private string $generatorClass,
private \Closure $presetValidator
private \Closure $presetValidator,
private readonly bool $fast
){}
/** @phpstan-return class-string<Generator> */
public function getGeneratorClass() : string{ return $this->generatorClass; }
public function isFast() : bool{ return $this->fast; }
/**
* @throws InvalidGeneratorOptionsException
*/

View File

@ -27,11 +27,18 @@ use pocketmine\scheduler\AsyncTask;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\FastChunkSerializer;
use pocketmine\world\generator\executor\ThreadLocalGeneratorContext;
use function array_map;
use function igbinary_serialize;
use function igbinary_unserialize;
/**
* @internal
*
* TODO: this should be moved to the executor namespace, but plugins have unfortunately used it directly due to the
* difficulty of regenerating chunks. This should be addressed in the future.
* For the remainder of PM5, we can't relocate this class.
*
* @phpstan-type OnCompletion \Closure(Chunk $centerChunk, array<int, Chunk> $adjacentChunks) : void
*/
class PopulationTask extends AsyncTask{

View File

@ -0,0 +1,106 @@
<?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\generator\executor;
use pocketmine\scheduler\AsyncPool;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\PopulationTask;
use function array_key_exists;
final class AsyncGeneratorExecutor implements GeneratorExecutor{
private static int $nextAsyncContextId = 1;
private readonly \Logger $logger;
/** @phpstan-var \Closure(int) : void */
private readonly \Closure $workerStartHook;
private readonly int $asyncContextId;
/**
* @var true[]
* @phpstan-var array<int, true>
*/
private array $generatorRegisteredWorkers = [];
public function __construct(
\Logger $logger,
private readonly AsyncPool $workerPool,
private readonly GeneratorExecutorSetupParameters $setupParameters,
int $asyncContextId = null
){
$this->logger = new \PrefixedLogger($logger, "AsyncGeneratorExecutor");
//TODO: we only allow setting this for PM5 because of PopulationTask uses in plugins
$this->asyncContextId = $asyncContextId ?? self::$nextAsyncContextId++;
$this->workerStartHook = function(int $workerId) : void{
if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
$this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
unset($this->generatorRegisteredWorkers[$workerId]);
}
};
$this->workerPool->addWorkerStartHook($this->workerStartHook);
}
private function registerGeneratorToWorker(int $worker) : void{
$this->logger->debug("Registering generator on worker $worker");
$this->workerPool->submitTaskToWorker(new AsyncGeneratorRegisterTask($this->setupParameters, $this->asyncContextId), $worker);
$this->generatorRegisteredWorkers[$worker] = true;
}
private function unregisterGenerator() : void{
foreach($this->workerPool->getRunningWorkers() as $i){
if(isset($this->generatorRegisteredWorkers[$i])){
$this->workerPool->submitTaskToWorker(new AsyncGeneratorUnregisterTask($this->asyncContextId), $i);
}
}
$this->generatorRegisteredWorkers = [];
}
public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{
$task = new PopulationTask(
$this->asyncContextId,
$chunkX,
$chunkZ,
$centerChunk,
$adjacentChunks,
$onCompletion
);
$workerId = $this->workerPool->selectWorker();
if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
$this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
unset($this->generatorRegisteredWorkers[$workerId]);
}
if(!isset($this->generatorRegisteredWorkers[$workerId])){
$this->registerGeneratorToWorker($workerId);
}
$this->workerPool->submitTaskToWorker($task, $workerId);
}
public function shutdown() : void{
$this->unregisterGenerator();
$this->workerPool->removeWorkerStartHook($this->workerStartHook);
}
}

View File

@ -21,37 +21,20 @@
declare(strict_types=1);
namespace pocketmine\world\generator;
namespace pocketmine\world\generator\executor;
use pocketmine\scheduler\AsyncTask;
use pocketmine\world\World;
class GeneratorRegisterTask extends AsyncTask{
public int $seed;
public int $worldId;
public int $worldMinY;
public int $worldMaxY;
class AsyncGeneratorRegisterTask extends AsyncTask{
/**
* @phpstan-param class-string<Generator> $generatorClass
*/
public function __construct(
World $world,
public string $generatorClass,
public string $generatorSettings
){
$this->seed = $world->getSeed();
$this->worldId = $world->getId();
$this->worldMinY = $world->getMinY();
$this->worldMaxY = $world->getMaxY();
}
private readonly GeneratorExecutorSetupParameters $setupParameters,
private readonly int $contextId
){}
public function onRun() : void{
/**
* @var Generator $generator
* @see Generator::__construct()
*/
$generator = new $this->generatorClass($this->seed, $this->generatorSettings);
ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $this->worldMinY, $this->worldMaxY), $this->worldId);
$setupParameters = $this->setupParameters;
$generator = $setupParameters->createGenerator();
ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $setupParameters->worldMinY, $setupParameters->worldMaxY), $this->contextId);
}
}

View File

@ -21,19 +21,16 @@
declare(strict_types=1);
namespace pocketmine\world\generator;
namespace pocketmine\world\generator\executor;
use pocketmine\scheduler\AsyncTask;
use pocketmine\world\World;
class GeneratorUnregisterTask extends AsyncTask{
public int $worldId;
public function __construct(World $world){
$this->worldId = $world->getId();
}
class AsyncGeneratorUnregisterTask extends AsyncTask{
public function __construct(
private readonly int $contextId
){}
public function onRun() : void{
ThreadLocalGeneratorContext::unregister($this->worldId);
ThreadLocalGeneratorContext::unregister($this->contextId);
}
}

View File

@ -0,0 +1,38 @@
<?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\generator\executor;
use pocketmine\world\format\Chunk;
interface GeneratorExecutor{
/**
* @param Chunk[]|null[] $adjacentChunks
* @phpstan-param array<int, Chunk|null> $adjacentChunks
* @phpstan-param \Closure(Chunk $centerChunk, array<int, Chunk> $adjacentChunks) : void $onCompletion
*/
public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void;
public function shutdown() : void;
}

View File

@ -0,0 +1,50 @@
<?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\generator\executor;
use pmmp\thread\ThreadSafe;
use pocketmine\world\generator\Generator;
final class GeneratorExecutorSetupParameters extends ThreadSafe{
/**
* @phpstan-param class-string<covariant \pocketmine\world\generator\Generator> $generatorClass
*/
public function __construct(
public readonly int $worldMinY,
public readonly int $worldMaxY,
public readonly int $generatorSeed,
public readonly string $generatorClass,
public readonly string $generatorSettings,
){}
public function createGenerator() : Generator{
/**
* @var Generator $generator
* @see Generator::__construct()
*/
$generator = new $this->generatorClass($this->generatorSeed, $this->generatorSettings);
return $generator;
}
}

View File

@ -0,0 +1,61 @@
<?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\generator\executor;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\Generator;
use pocketmine\world\generator\PopulationUtils;
final class SyncGeneratorExecutor implements GeneratorExecutor{
private readonly Generator $generator;
private readonly int $worldMinY;
private readonly int $worldMaxY;
public function __construct(
GeneratorExecutorSetupParameters $setupParameters
){
$this->generator = $setupParameters->createGenerator();
$this->worldMinY = $setupParameters->worldMinY;
$this->worldMaxY = $setupParameters->worldMaxY;
}
public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{
[$centerChunk, $adjacentChunks] = PopulationUtils::populateChunkWithAdjacents(
$this->worldMinY,
$this->worldMaxY,
$this->generator,
$chunkX,
$chunkZ,
$centerChunk,
$adjacentChunks
);
$onCompletion($centerChunk, $adjacentChunks);
}
public function shutdown() : void{
//NOOP
}
}

View File

@ -21,7 +21,9 @@
declare(strict_types=1);
namespace pocketmine\world\generator;
namespace pocketmine\world\generator\executor;
use pocketmine\world\generator\Generator;
/**
* Manages thread-local caches for generators and the things needed to support them

View File

@ -1272,18 +1272,18 @@ parameters:
count: 1
path: ../../../src/world/format/io/region/RegionLoader.php
-
message: '#^Dynamic new is not allowed\.$#'
identifier: pocketmine.new.dynamic
count: 1
path: ../../../src/world/generator/GeneratorRegisterTask.php
-
message: '#^Method pocketmine\\world\\generator\\biome\\BiomeSelector\:\:pickBiome\(\) should return pocketmine\\world\\biome\\Biome but returns pocketmine\\world\\biome\\Biome\|null\.$#'
identifier: return.type
count: 1
path: ../../../src/world/generator/biome/BiomeSelector.php
-
message: '#^Dynamic new is not allowed\.$#'
identifier: pocketmine.new.dynamic
count: 1
path: ../../../src/world/generator/executor/GeneratorExecutorSetupParameters.php
-
message: '#^Cannot call method getBiomeId\(\) on pocketmine\\world\\format\\Chunk\|null\.$#'
identifier: method.nonObject