Merge pull request #680 from MihailRis/new-block-event

Fix major chunks loading performance issue & add new block events
This commit is contained in:
MihailRis 2025-11-18 22:50:16 +03:00 committed by GitHub
commit dda823ac24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 353 additions and 111 deletions

View File

@ -4,6 +4,10 @@
Callbacks specified in block script.
> [!WARNING]
> events such as on_block_tick, on_block_present, and on_block_removed
> can cause performance issues if used carelessly or excessively.
```lua
function on_placed(x, y, z, playerid)
```
@ -53,6 +57,21 @@ function on_block_tick(x, y, z, tps: number)
Called tps (20 / tick-interval) times per second for a block.
Use 1/tps instead of `time.delta()`.
```lua
function on_block_present(x, y, z)
```
Called for a specific block when it appears in the world (generated/loaded/placed).
The call occurs within a time period that may depend on the event queue load.
Under light load, it occurs during the first tick interval of the block.
on_block_tick is not called until the event is called.
```lua
function on_block_removed(x, y, z)
```
Called when chunk containing the block unloads.
```lua
function on_player_tick(playerid: int, tps: int)
```

View File

@ -4,6 +4,10 @@
Функции для обработки событий, прописываемые в скрипте блока.
> [!WARNING]
> Mass events such as on_block_tick, on_block_present, and on_block_removed,
> if used carelessly or excessively, can lead to performance issues.
```lua
function on_placed(x, y, z, playerid)
```
@ -53,6 +57,21 @@ function on_block_tick(x, y, z, tps: number)
Вызывается tps (20 / tick-interval) раз в секунду для конкретного блока.
Используйте 1/tps вместо `time.delta()`.
```lua
function on_block_present(x, y, z)
```
Вызывается для конкретного блока при появлении (генерации/загрузке/размещении).
Вызов происходит в течение времени, которое может зависеть от нагрузки очереди событий.
При малой нагрузке происходит в течение первого такта блока (tick-interval).
До вызова события on_block_tick не вызывается.
```lua
function on_block_removed(x, y, z)
```
Вызывается при выгрузке чанка, в котором находится блок.
```lua
function on_player_tick(playerid: int, tps: int)
```

View File

@ -171,73 +171,6 @@ local function clean(iterable, checkFun, ...)
end
end
local updating_blocks = {}
local TYPE_REGISTER = 0
local TYPE_UNREGISTER = 1
block.__perform_ticks = function(delta)
for id, entry in pairs(updating_blocks) do
entry.timer = entry.timer + delta
local steps = math.floor(entry.timer / entry.delta * #entry / 3)
if steps == 0 then
goto continue
end
entry.timer = 0.0
local event = entry.event
local tps = entry.tps
for i=1, steps do
local x = entry[entry.pointer + 1]
local y = entry[entry.pointer + 2]
local z = entry[entry.pointer + 3]
entry.pointer = (entry.pointer + 3) % #entry
events.emit(event, x, y, z, tps)
end
::continue::
end
end
block.__process_register_events = function()
local register_events = block.__pull_register_events()
if not register_events then
return
end
for i=1, #register_events, 4 do
local header = register_events[i]
local type = bit.band(header, 0xFFFF)
local id = bit.rshift(header, 16)
local x = register_events[i + 1]
local y = register_events[i + 2]
local z = register_events[i + 3]
local list = updating_blocks[id]
if type == TYPE_REGISTER then
if not list then
list = {}
list.event = block.name(id) .. ".blocktick"
list.tps = 20 / (block.properties[id]["tick-interval"] or 1)
list.delta = 1.0 / list.tps
list.timer = 0.0
list.pointer = 0
updating_blocks[id] = list
end
table.insert(list, x)
table.insert(list, y)
table.insert(list, z)
elseif type == TYPE_UNREGISTER then
if list then
for j=1, #list, 3 do
if list[j] == x and list[j + 1] == y and list[j + 2] == z then
for k=1,3 do
table.remove(list, j)
end
j = j - 3
end
end
end
end
end
end
network.__process_events = function()
local CLIENT_CONNECTED = 1
local CONNECTED_TO_SERVER = 2

View File

@ -0,0 +1,144 @@
local updating_blocks = {}
local present_queues = {}
local REGISTER_BIT = 0x1
local UPDATING_BIT = 0x2
local PRESENT_BIT = 0x4
local REMOVED_BIT = 0x8
block.__perform_ticks = function(delta)
for id, entry in pairs(updating_blocks) do
entry.timer = entry.timer + delta
local steps = math.floor(entry.timer / entry.delta * #entry / 3)
if steps == 0 then
goto continue
end
entry.timer = 0.0
local event = entry.event
local tps = entry.tps
for i=1, steps do
local x = entry[entry.pointer + 1]
local y = entry[entry.pointer + 2]
local z = entry[entry.pointer + 3]
entry.pointer = (entry.pointer + 3) % #entry
events.emit(event, x, y, z, tps)
end
::continue::
end
for id, queue in pairs(present_queues) do
queue.timer = queue.timer + delta
local steps = math.floor(queue.timer / queue.delta * #queue / 3)
if steps == 0 then
goto continue
end
queue.timer = 0.0
local event = queue.event
local update_list = updating_blocks[id]
for i=1, steps do
local index = #queue - 2
if index <= 0 then
break
end
local x = queue[index]
local y = queue[index + 1]
local z = queue[index + 2]
for j=1,3 do
table.remove(queue, index)
end
events.emit(event, x, y, z)
if queue.updating then
table.insert(update_list, x)
table.insert(update_list, y)
table.insert(update_list, z)
end
end
::continue::
end
end
local block_pull_register_events = block.__pull_register_events
block.__pull_register_events = nil
block.__process_register_events = function()
local register_events = block_pull_register_events()
if not register_events then
return
end
local emit_event = events.emit
local removed_events = {}
for i=1, #register_events, 4 do
local header = register_events[i]
local event_bits = bit.band(header, 0xFFFF)
local id = bit.rshift(header, 16)
local x = register_events[i + 1]
local y = register_events[i + 2]
local z = register_events[i + 3]
local is_register = bit.band(event_bits, REGISTER_BIT) ~= 0
local is_updating = bit.band(event_bits, UPDATING_BIT) ~= 0
local is_present = bit.band(event_bits, PRESENT_BIT) ~= 0
local is_removed = bit.band(event_bits, REMOVED_BIT) ~= 0
local list = updating_blocks[id]
if not is_register and is_removed then
local rm_event = removed_events[id]
if not rm_event then
rm_event = block.name(id) .. ".blockremoved"
removed_events[id] = rm_event
end
emit_event(rm_event, x, y, z)
end
if not list and is_register and is_updating then
list = {}
list.event = block.name(id) .. ".blocktick"
list.tps = 20 / (block.properties[id]["tick-interval"] or 1)
list.delta = 1.0 / list.tps
list.timer = 0.0
list.pointer = 0
updating_blocks[id] = list
end
if is_register and is_present then
local present_queue = present_queues[id]
if not present_queue then
present_queue = {}
present_queue.event = block.name(id) .. ".blockpresent"
present_queue.tps = 20 / (block.properties[id]["tick-interval"] or 1)
present_queue.delta = 1.0 / present_queue.tps
present_queue.timer = 0.0
present_queue.pointer = 0
present_queue.updating = is_updating
present_queues[id] = present_queue
end
table.insert(present_queue, x)
table.insert(present_queue, y)
table.insert(present_queue, z)
goto continue
end
if not is_updating then
goto continue
end
if is_register then
table.insert(list, x)
table.insert(list, y)
table.insert(list, z)
else
if not list then
goto continue
end
for j=1, #list, 3 do
if list[j] == x and list[j + 1] == y and list[j + 2] == z then
for k=1,3 do
table.remove(list, j)
end
j = j - 3
end
end
end
::continue::
end
end

View File

@ -357,6 +357,10 @@ function __vc_on_world_tick(tps)
time.schedules.world:tick(1.0 / tps)
end
function __vc_process_before_quit()
block.__process_register_events()
end
function __vc_on_world_save()
local rule_values = {}
for name, rule in pairs(rules.rules) do

View File

@ -87,7 +87,7 @@ std::shared_ptr<UINode> create_debug_panel(
fpsMax = fps;
});
panel->listenInterval(1.0f, [&engine, &gui]() {
panel->listenInterval(1.0f, [&engine]() {
const auto& network = engine.getNetwork();
size_t totalDownload = network.getTotalDownload();
size_t totalUpload = network.getTotalUpload();

View File

@ -109,6 +109,7 @@ LevelScreen::~LevelScreen() {
scripting::on_frontend_close();
// unblock all bindings
input.getBindings().enableAll();
playerController->getPlayer()->chunks->saveAndClear();
controller->onWorldQuit();
engine.getPaths().setCurrentWorldFolder("");
}
@ -278,5 +279,6 @@ void LevelScreen::onEngineShutdown() {
if (hud->isInventoryOpen()) {
hud->closeInventory();
}
controller->processBeforeQuit();
controller->saveWorld();
}

View File

@ -15,6 +15,7 @@
#include "voxels/Chunks.hpp"
#include "voxels/GlobalChunks.hpp"
#include "world/Level.hpp"
#include "world/LevelEvents.hpp"
#include "world/World.hpp"
#include "world/generator/WorldGenerator.hpp"
@ -172,13 +173,12 @@ void ChunksController::createChunk(const Player& player, int x, int z) const {
auto chunk = level.chunks->create(x, z);
player.chunks->putChunk(chunk);
auto& chunkFlags = chunk->flags;
if (!chunkFlags.loaded) {
generator->generate(chunk->voxels, x, z);
chunkFlags.unsaved = true;
}
chunk->updateHeights();
level.events->trigger(LevelEventType::CHUNK_PRESENT, chunk.get());
if (!chunkFlags.loadedLights) {
Lighting::prebuildSkyLight(*chunk, *level.content.getIndices());
}

View File

@ -366,7 +366,9 @@ void EngineController::reconfigPacks(
);
}
} else {
auto world = controller->getLevel()->getWorld();
auto level = controller->getLevel();
auto world = level->getWorld();
controller->processBeforeQuit();
controller->saveWorld();
auto names = PacksManager::getNames(world->getPacks());

View File

@ -27,7 +27,8 @@ LevelController::LevelController(
: settings(engine->getSettings()),
level(std::move(levelPtr)),
chunks(std::make_unique<ChunksController>(*level)),
playerTickClock(20, 3) {
playerTickClock(20, 3),
localPlayer(clientPlayer) {
level->events->listen(LevelEventType::CHUNK_PRESENT, [](auto, Chunk* chunk) {
scripting::on_chunk_present(*chunk, chunk->flags.loaded);
@ -121,6 +122,13 @@ void LevelController::update(float delta, bool pause) {
level->entities->clean();
}
void LevelController::processBeforeQuit() {
if (localPlayer) {
localPlayer->chunks->saveAndClear();
}
scripting::process_before_quit();
}
void LevelController::saveWorld() {
auto world = level->getWorld();
if (world->isNameless()) {

View File

@ -20,6 +20,7 @@ class LevelController {
std::unique_ptr<ChunksController> chunks;
util::Clock playerTickClock;
Player* localPlayer;
public:
LevelController(Engine* engine, std::unique_ptr<Level> level, Player* clientPlayer);
@ -27,6 +28,7 @@ public:
/// @param pause is world and player simulation paused
void update(float delta, bool pause);
void processBeforeQuit();
void saveWorld();
void onWorldQuit();

View File

@ -730,7 +730,7 @@ static int l_pull_register_events(lua::State* L) {
lua::createtable(L, events.size() * 4, 0);
for (int i = 0; i < events.size(); i++) {
const auto& event = events[i];
lua::pushinteger(L, static_cast<int>(event.type) | event.id << 16);
lua::pushinteger(L, static_cast<int>(event.bits) | event.id << 16);
lua::rawseti(L, i * 4 + 1);
for (int j = 0; j < 3; j++) {

View File

@ -119,6 +119,7 @@ static int l_close_world(lua::State* L) {
if (controller == nullptr) {
throw std::runtime_error("no world open");
}
controller->processBeforeQuit();
bool save_world = lua::toboolean(L, 1);
if (save_world) {
controller->saveWorld();

View File

@ -72,6 +72,7 @@ void scripting::initialize(Engine* engine) {
load_script(io::path("stdlib.lua"), true);
load_script(io::path("classes.lua"), true);
load_script(io::path("internal_events.lua"), true);
}
class LuaCoroutine : public Process {
@ -340,6 +341,13 @@ void scripting::on_world_save() {
}
}
void scripting::process_before_quit() {
auto L = lua::get_main_state();
if (lua::getglobal(L, "__vc_process_before_quit")) {
lua::call_nothrow(L, 0, 0);
}
}
void scripting::on_world_quit() {
auto L = lua::get_main_state();
for (auto& pack : content_control->getAllContentPacks()) {
@ -674,6 +682,10 @@ void scripting::load_content_script(
register_event(env, "on_block_tick", prefix + ".blocktick");
funcsset.onblockstick =
register_event(env, "on_blocks_tick", prefix + ".blockstick");
funcsset.onblockpresent =
register_event(env, "on_block_present", prefix + ".blockpresent");
funcsset.onblockremoved =
register_event(env, "on_block_removed", prefix + ".blockremoved");
}
void scripting::load_content_script(

View File

@ -81,6 +81,7 @@ namespace scripting {
void on_world_load(LevelController* controller);
void on_world_tick(int tps);
void on_world_save();
void process_before_quit();
void on_world_quit();
void cleanup(const std::vector<std::string>& nonReset);
void on_blocks_tick(const Block& block, int tps);

View File

@ -5,8 +5,6 @@
#include <queue>
#include <vector>
#include "typedefs.hpp"
namespace util {
/// @brief Thread-safe pool of same-sized buffers
/// @tparam T array type

64
src/util/ObjectsPool.hpp Normal file
View File

@ -0,0 +1,64 @@
#pragma once
#include <memory>
#include <mutex>
#include <vector>
#include <queue>
#if defined(_WIN32)
#include <malloc.h>
#endif
namespace util {
struct AlignedDeleter {
void operator()(void* p) const {
#if defined(_WIN32)
_aligned_free(p);
#else
std::free(p);
#endif
}
};
template <class T>
class ObjectsPool {
public:
ObjectsPool(size_t preallocated = 0) {
for (size_t i = 0; i < preallocated; i++) {
allocateNew();
}
}
template<typename... Args>
std::shared_ptr<T> create(Args&&... args) {
std::lock_guard lock(mutex);
if (freeObjects.empty()) {
allocateNew();
}
auto ptr = freeObjects.front();
freeObjects.pop();
new (ptr)T(std::forward<Args>(args)...);
return std::shared_ptr<T>(reinterpret_cast<T*>(ptr), [this](T* ptr) {
ptr->~T();
std::lock_guard lock(mutex);
freeObjects.push(ptr);
});
}
private:
std::vector<std::unique_ptr<void, AlignedDeleter>> objects;
std::queue<void*> freeObjects;
std::mutex mutex;
void allocateNew() {
std::unique_ptr<void, AlignedDeleter> ptr(
#if defined(_WIN32)
_aligned_malloc(sizeof(T), alignof(T))
#else
std::aligned_alloc(alignof(T), sizeof(T))
#endif
);
freeObjects.push(ptr.get());
objects.push_back(std::move(ptr));
}
};
}

View File

@ -50,6 +50,8 @@ struct BlockFuncsSet {
bool randupdate : 1;
bool onblocktick : 1;
bool onblockstick : 1;
bool onblockpresent : 1;
bool onblockremoved : 1;
};
struct CoordSystem {

View File

@ -2,22 +2,23 @@
#include <algorithm>
#include "content/Content.hpp"
#include "Block.hpp"
#include "Chunk.hpp"
#include "coders/json.hpp"
#include "content/Content.hpp"
#include "debug/Logger.hpp"
#include "world/files/WorldFiles.hpp"
#include "items/Inventories.hpp"
#include "lighting/Lightmap.hpp"
#include "maths/voxmaths.hpp"
#include "objects/Entities.hpp"
#include "objects/Entity.hpp"
#include "voxels/blocks_agent.hpp"
#include "typedefs.hpp"
#include "world/LevelEvents.hpp"
#include "util/ObjectsPool.hpp"
#include "voxels/blocks_agent.hpp"
#include "world/files/WorldFiles.hpp"
#include "world/Level.hpp"
#include "world/LevelEvents.hpp"
#include "world/World.hpp"
#include "Block.hpp"
#include "Chunk.hpp"
static debug::Logger logger("chunks-storage");
@ -89,13 +90,15 @@ static inline auto load_inventories(
return invs;
}
static util::ObjectsPool<Chunk> chunks_pool(1'024);
std::shared_ptr<Chunk> GlobalChunks::create(int x, int z) {
const auto& found = chunksMap.find(keyfrom(x, z));
if (found != chunksMap.end()) {
return found->second;
}
auto chunk = std::make_shared<Chunk>(x, z);
auto chunk = chunks_pool.create(x, z);
chunksMap[keyfrom(x, z)] = chunk;
World& world = *level.getWorld();
@ -127,8 +130,6 @@ std::shared_ptr<Chunk> GlobalChunks::create(int x, int z) {
chunk->flags.loadedLights = true;
}
chunk->blocksMetadata = regions.getBlocksData(chunk->x, chunk->z);
level.events->trigger(LevelEventType::CHUNK_PRESENT, chunk.get());
return chunk;
}

View File

@ -14,39 +14,58 @@ std::vector<BlockRegisterEvent> blocks_agent::pull_register_events() {
return events;
}
static uint8_t get_events_bits(const Block& def) {
uint8_t bits = 0;
auto funcsset = def.rt.funcsset;
bits |= BlockRegisterEvent::UPDATING_BIT * funcsset.onblocktick;
bits |= BlockRegisterEvent::PRESENT_EVENT_BIT * funcsset.onblockpresent;
bits |= BlockRegisterEvent::REMOVED_EVENT_BIT * funcsset.onblockremoved;
return bits;
}
static void on_chunk_register_event(
const ContentIndices& indices,
const Chunk& chunk,
BlockRegisterEvent::Type type
bool present
) {
for (int i = 0; i < CHUNK_VOL; i++) {
const auto& def =
indices.blocks.require(chunk.voxels[i].id);
if (def.rt.funcsset.onblocktick) {
int x = i % CHUNK_W + chunk.x * CHUNK_W;
int z = (i / CHUNK_W) % CHUNK_D + chunk.z * CHUNK_D;
int y = (i / CHUNK_W / CHUNK_D);
block_register_events.push_back(BlockRegisterEvent {
type, def.rt.id, {x, y, z}
});
const auto& voxels = chunk.voxels;
int totalBegin = chunk.bottom * (CHUNK_W * CHUNK_D);
int totalEnd = chunk.top * (CHUNK_W * CHUNK_D);
uint8_t flagsCache[1024] {};
for (int i = totalBegin; i <= totalEnd; i++) {
blockid_t id = voxels[i].id;
uint8_t bits = id < sizeof(flagsCache) ? flagsCache[id] : 0;
if ((bits & 0x80) == 0) {
const auto& def = indices.blocks.require(id);
bits = get_events_bits(def);
flagsCache[id] = bits | 0x80;
}
bits &= 0x7F;
if (bits == 0) {
continue;
}
int x = i % CHUNK_W + chunk.x * CHUNK_W;
int z = (i / CHUNK_W) % CHUNK_D + chunk.z * CHUNK_D;
int y = (i / CHUNK_W / CHUNK_D);
block_register_events.push_back(BlockRegisterEvent {
static_cast<uint8_t>(bits | (present ? 1 : 0)), id, {x, y, z}
});
}
}
void blocks_agent::on_chunk_present(
const ContentIndices& indices, const Chunk& chunk
) {
on_chunk_register_event(
indices, chunk, BlockRegisterEvent::Type::REGISTER_UPDATING
);
on_chunk_register_event(indices, chunk, true);
}
void blocks_agent::on_chunk_remove(
const ContentIndices& indices, const Chunk& chunk
) {
on_chunk_register_event(
indices, chunk, BlockRegisterEvent::Type::UNREGISTER_UPDATING
);
on_chunk_register_event(indices, chunk, false);
}
template <class Storage>
@ -101,11 +120,14 @@ static void finalize_block(
chunk.flags.blocksData = true;
}
}
if (def.rt.funcsset.onblocktick) {
block_register_events.push_back(BlockRegisterEvent {
BlockRegisterEvent::Type::UNREGISTER_UPDATING, def.rt.id, {x, y, z}
});
uint8_t bits = get_events_bits(def);
if (bits == 0) {
return;
}
block_register_events.push_back(BlockRegisterEvent {
bits, def.rt.id, {x, y, z}
});
}
template <class Storage>
@ -131,9 +153,17 @@ static void initialize_block(
refresh_chunk_heights(chunk, id == BLOCK_AIR, y);
mark_neighboirs_modified(chunks, cx, cz, lx, lz);
uint8_t bits = get_events_bits(def);
if (bits == 0) {
return;
}
block_register_events.push_back(BlockRegisterEvent {
static_cast<uint8_t>(bits | 1), def.rt.id, {x, y, z}
});
if (def.rt.funcsset.onblocktick) {
block_register_events.push_back(BlockRegisterEvent {
BlockRegisterEvent::Type::REGISTER_UPDATING, def.rt.id, {x, y, z}
bits, def.rt.id, {x, y, z}
});
}
}

View File

@ -25,11 +25,11 @@ struct AABB;
namespace blocks_agent {
struct BlockRegisterEvent {
enum class Type : uint16_t {
REGISTER_UPDATING,
UNREGISTER_UPDATING,
};
Type type;
static inline constexpr uint8_t REGISTER_BIT = 0x1;
static inline constexpr uint8_t UPDATING_BIT = 0x2;
static inline constexpr uint8_t PRESENT_EVENT_BIT = 0x4;
static inline constexpr uint8_t REMOVED_EVENT_BIT = 0x8;
uint8_t bits;
blockid_t id;
glm::ivec3 coord;
};