From 08b7870c797909e934281679b6c064018fe9f07a Mon Sep 17 00:00:00 2001 From: lhofhansl Date: Sun, 21 Sep 2025 13:19:30 -0700 Subject: [PATCH] Monoblocks: optimize blocks that contain a single type of node (#16293) Reduces memory usage on the server, especially with many user and/or large viewing distances. Currently disabled on the client due to known data races on a block's data. --- src/client/client.h | 1 + src/dummygamedef.h | 1 + src/gamedef.h | 1 + src/mapblock.cpp | 101 ++++++++++++++++++++++++++------ src/mapblock.h | 42 ++++++++----- src/mapnode.cpp | 26 ++++++--- src/mapnode.h | 3 +- src/server.h | 1 + src/unittest/test_mapblock.cpp | 104 +++++++++++++++++++++++++++++++++ src/voxel.cpp | 10 +++- src/voxel.h | 2 +- 11 files changed, 245 insertions(+), 47 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index 303f51fd4f..d08e1e6c89 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -371,6 +371,7 @@ public: scene::ISceneManager *getSceneManager(); // IGameDef interface + bool isClient() override { return true; } IItemDefManager* getItemDefManager() override; const NodeDefManager* getNodeDefManager() override; ICraftDefManager* getCraftDefManager() override; diff --git a/src/dummygamedef.h b/src/dummygamedef.h index 932d8be3df..5b039158ce 100644 --- a/src/dummygamedef.h +++ b/src/dummygamedef.h @@ -30,6 +30,7 @@ public: delete m_itemdef; } + bool isClient() override { return false; } IItemDefManager *getItemDefManager() override { return m_itemdef; } const NodeDefManager *getNodeDefManager() override { return m_nodedef; } NodeDefManager* getWritableNodeDefManager() { return m_nodedef; } diff --git a/src/gamedef.h b/src/gamedef.h index 3b812e684d..07002fd1cf 100644 --- a/src/gamedef.h +++ b/src/gamedef.h @@ -61,4 +61,5 @@ public: virtual bool sendModChannelMessage(const std::string &channel, const std::string &message) = 0; virtual ModChannel *getModChannel(const std::string &channel) = 0; + virtual bool isClient() = 0; }; diff --git a/src/mapblock.cpp b/src/mapblock.cpp index d555f64231..e0d9630e81 100644 --- a/src/mapblock.cpp +++ b/src/mapblock.cpp @@ -4,6 +4,7 @@ #include "mapblock.h" +#include #include #include "map.h" #include "light.h" @@ -101,11 +102,13 @@ static const char *modified_reason_strings[] = { MapBlock::MapBlock(v3s16 pos, IGameDef *gamedef): m_pos(pos), m_pos_relative(pos * MAP_BLOCKSIZE), - data(new MapNode[nodecount]), - m_gamedef(gamedef) + m_gamedef(gamedef), + m_is_mono_block(false) { - reallocate(); - assert(m_modified > MOD_STATE_CLEAN); + // We start with nodecount nodes, because in the vast + // majority of the cases a block is created just before + // it is de-serialized or generated. + reallocate(nodecount, MapNode(CONTENT_IGNORE)); } MapBlock::~MapBlock() @@ -118,7 +121,8 @@ MapBlock::~MapBlock() #endif delete[] data; - porting::TrackFreedMemory(sizeof(MapNode) * nodecount); + if (!m_is_mono_block) + porting::TrackFreedMemory(sizeof(MapNode) * nodecount); } static inline size_t get_max_objects_per_block() @@ -214,7 +218,7 @@ void MapBlock::copyTo(VoxelManipulator &dst) VoxelArea data_area(v3s16(0,0,0), data_size - v3s16(1,1,1)); // Copy from data to VoxelManipulator - dst.copyFrom(data, data_area, v3s16(0,0,0), + dst.copyFrom(data, m_is_mono_block, data_area, v3s16(0,0,0), getPosRelative(), data_size); } @@ -223,9 +227,61 @@ void MapBlock::copyFrom(const VoxelManipulator &src) v3s16 data_size(MAP_BLOCKSIZE, MAP_BLOCKSIZE, MAP_BLOCKSIZE); VoxelArea data_area(v3s16(0,0,0), data_size - v3s16(1,1,1)); + expandNodesIfNeeded(); // Copy from VoxelManipulator to data src.copyTo(data, data_area, v3s16(0,0,0), getPosRelative(), data_size); + tryShrinkNodes(); +} + +void MapBlock::reallocate(u32 count, MapNode n) +{ + assert(count == 1 || count == nodecount); + // For now monoblocks are disabled on the client. + // The client has known data races on the block's data (FIXME). + assert(!m_gamedef->isClient() || count == nodecount); + + delete[] data; + if (data && !m_is_mono_block && count == 1) + porting::TrackFreedMemory(sizeof(MapNode) * nodecount); + + data = new MapNode[count]; + std::fill_n(data, count, n); + + m_is_mono_block = (count == 1); +} + +void MapBlock::tryShrinkNodes() +{ + // For now monoblocks are disabled on the client. + // The client has known data races on the block's data (FIXME). + if (m_gamedef->isClient()) + return; + + if (m_is_mono_block) + return; + + MapNode n = data[0]; + bool is_mono_block = true; + for (u32 i=1; indef()); + const size_t size = m_is_mono_block ? 1 : nodecount; + std::unique_ptr tmp_nodes(new MapNode[size]); + std::copy_n(data, size, tmp_nodes.get()); + getBlockNodeIdMapping(&nimap, tmp_nodes.get(), size, m_gamedef->ndef()); - buf = MapNode::serializeBulk(version, tmp_nodes, nodecount, - content_width, params_width); - delete[] tmp_nodes; + buf = MapNode::serializeBulk(version, tmp_nodes.get(), nodecount, + content_width, params_width, m_is_mono_block); // write timestamp and node/id mapping first if (version >= 29) { @@ -396,7 +456,7 @@ void MapBlock::serialize(std::ostream &os_compressed, u8 version, bool disk, int else { buf = MapNode::serializeBulk(version, data, nodecount, - content_width, params_width); + content_width, params_width, m_is_mono_block); } writeU8(os, content_width); @@ -465,6 +525,7 @@ void MapBlock::deSerialize(std::istream &in_compressed, u8 version, bool disk) TRACESTREAM(<<"MapBlock::deSerialize "< MapNode::serializeBulk(int version, const MapNode *nodes, u32 nodecount, - u8 content_width, u8 params_width) + u8 content_width, u8 params_width, bool is_mono_block) { if (!ser_ver_supported_write(version)) throw VersionMismatchException("ERROR: MapNode format not supported"); @@ -601,14 +601,22 @@ Buffer MapNode::serializeBulk(int version, // Writing to the buffer linearly is faster u8 *p = &databuf[0]; - for (u32 i = 0; i < nodecount; i++, p += 2) - writeU16(p, nodes[i].param0); - - for (u32 i = 0; i < nodecount; i++, p++) - writeU8(p, nodes[i].param1); - - for (u32 i = 0; i < nodecount; i++, p++) - writeU8(p, nodes[i].param2); + if (is_mono_block) { + MapNode n = nodes[0]; + for (u32 i = 0; i < nodecount; i++, p += 2) + writeU16(p, n.param0); + for (u32 i = 0; i < nodecount; i++, p++) + writeU8(p, n.param1); + for (u32 i = 0; i < nodecount; i++, p++) + writeU8(p, n.param2); + } else { + for (u32 i = 0; i < nodecount; i++, p += 2) + writeU16(p, nodes[i].param0); + for (u32 i = 0; i < nodecount; i++, p++) + writeU8(p, nodes[i].param1); + for (u32 i = 0; i < nodecount; i++, p++) + writeU8(p, nodes[i].param2); + } return databuf; } diff --git a/src/mapnode.h b/src/mapnode.h index 5366caea27..1640f2a650 100644 --- a/src/mapnode.h +++ b/src/mapnode.h @@ -305,9 +305,10 @@ struct alignas(u32) MapNode // content_width = the number of bytes of content per node // params_width = the number of bytes of params per node // compressed = true to zlib-compress output + // is_mono_block = if true, nodes is array of size 1 static Buffer serializeBulk(int version, const MapNode *nodes, u32 nodecount, - u8 content_width, u8 params_width); + u8 content_width, u8 params_width, bool is_mono_block = false); static void deSerializeBulk(std::istream &is, int version, MapNode *nodes, u32 nodecount, u8 content_width, u8 params_width); diff --git a/src/server.h b/src/server.h index edb9416924..87c0e4c2a3 100644 --- a/src/server.h +++ b/src/server.h @@ -319,6 +319,7 @@ public: std::list *log); // IGameDef interface + bool isClient() override { return false; } // Under envlock virtual IItemDefManager* getItemDefManager(); virtual const NodeDefManager* getNodeDefManager(); diff --git a/src/unittest/test_mapblock.cpp b/src/unittest/test_mapblock.cpp index 1d26b40a8f..31315ee97c 100644 --- a/src/unittest/test_mapblock.cpp +++ b/src/unittest/test_mapblock.cpp @@ -10,6 +10,7 @@ #include "serialization.h" #include "noise.h" #include "inventory.h" +#include "voxel.h" class TestMapBlock : public TestBase { @@ -33,6 +34,9 @@ public: // Tests loading a non-standard MapBlock void testLoadNonStd(IGameDef *gamedef); + + // Tests blocks with a single recurring node + void testMonoblock(IGameDef *gamedef); }; static TestMapBlock g_test_instance; @@ -45,10 +49,110 @@ void TestMapBlock::runTests(IGameDef *gamedef) TEST(testLoad29, gamedef); TEST(testLoad20, gamedef); TEST(testLoadNonStd, gamedef); + TEST(testMonoblock, gamedef); } //////////////////////////////////////////////////////////////////////////////// +void TestMapBlock::testMonoblock(IGameDef *gamedef) +{ + MapBlock block({}, gamedef); + UASSERT(!block.m_is_mono_block); + + // make the array is expanded + block.expandNodesIfNeeded(); + UASSERT(std::all_of(block.data, block.data + MapBlock::nodecount, [](MapNode &n) { return n == MapNode(CONTENT_IGNORE); })); + + // covert to monoblock + block.tryShrinkNodes(); + UASSERT(block.m_is_mono_block); + UASSERT(block.data[0].param0 == CONTENT_IGNORE); + + block.data[0] = MapNode(CONTENT_AIR); + UASSERT(block.m_is_mono_block); + UASSERT(block.data[0].param0 == CONTENT_AIR); + + block.expandNodesIfNeeded(); + UASSERT(!block.m_is_mono_block); + UASSERT(std::all_of(block.data, block.data + MapBlock::nodecount, [](MapNode &n) { return n == MapNode(CONTENT_AIR); })); + + // covert back to mono block + block.tryShrinkNodes(); + UASSERT(block.m_is_mono_block); + + // deconvert explicitly + block.expandNodesIfNeeded(); + UASSERT(!block.m_is_mono_block); + + // covert back to mono block + block.tryShrinkNodes(); + UASSERT(block.m_is_mono_block); + + static_assert(CONTENT_AIR != 42); + // set a node, should deconvert the block + block.setNode(5,5,5, MapNode(42)); + UASSERT(!block.m_is_mono_block); + + // cannot covert to mono block + block.tryShrinkNodes(); + UASSERT(!block.m_is_mono_block); + + // set all nodes to 42 + for (size_t i = 0; i < MapBlock::nodecount; ++i) { + block.data[i] = MapNode(42); + } + + // can covert to mono block + block.tryShrinkNodes(); + UASSERT(block.m_is_mono_block); + UASSERT(block.data[0].param0 == 42); + + VoxelManipulator vmm; + v3s16 data_size(MAP_BLOCKSIZE, MAP_BLOCKSIZE, MAP_BLOCKSIZE); + vmm.addArea(VoxelArea(block.getPosRelative(), block.getPosRelative() + data_size + v3s16(1,1,1))); + block.copyTo(vmm); + UASSERT(block.m_is_mono_block); + UASSERT(vmm.getNode({5,5,5}).param0 == 42); + + block.setNode(5,5,5,MapNode(23)); + + block.copyFrom(vmm); + UASSERT(block.m_is_mono_block); + UASSERT(block.data[0].param0 == 42); + + vmm.setNode({5,5,5}, MapNode(23)); + block.copyFrom(vmm); + UASSERT(!block.m_is_mono_block); + + vmm.setNode({5,5,5}, MapNode(42,1,0)); + block.copyFrom(vmm); + UASSERT(!block.m_is_mono_block); + + vmm.setNode({5,5,5}, MapNode(42,0,1)); + block.copyFrom(vmm); + UASSERT(!block.m_is_mono_block); + + vmm.setNode({5,5,5}, MapNode(42)); + block.copyFrom(vmm); + UASSERT(block.m_is_mono_block); + + block.setNode(5,5,5,MapNode(23)); + block.tryShrinkNodes(); + UASSERT(!block.m_is_mono_block); + + block.setNode(5,5,5,MapNode(42, 1, 0)); + block.tryShrinkNodes(); + UASSERT(!block.m_is_mono_block); + + block.setNode(5,5,5,MapNode(42, 0, 1)); + block.tryShrinkNodes(); + UASSERT(!block.m_is_mono_block); + + block.setNode(5,5,5,MapNode(42)); + block.tryShrinkNodes(); + UASSERT(block.m_is_mono_block); +} + void TestMapBlock::testSaveLoad(IGameDef *gamedef, const u8 version) { // Use the bottom node ids for this test diff --git a/src/voxel.cpp b/src/voxel.cpp index 5a69b07255..1c6e9bf4de 100644 --- a/src/voxel.cpp +++ b/src/voxel.cpp @@ -177,7 +177,7 @@ void VoxelManipulator::addArea(const VoxelArea &area) delete[] old_flags; } -void VoxelManipulator::copyFrom(MapNode *src, const VoxelArea& src_area, +void VoxelManipulator::copyFrom(MapNode *src, bool is_mono_block, const VoxelArea& src_area, v3s16 from_pos, v3s16 to_pos, const v3s16 &size) { /* The reason for this optimised code is that we're a member function @@ -216,8 +216,12 @@ void VoxelManipulator::copyFrom(MapNode *src, const VoxelArea& src_area, for (s16 z = 0; z < size.Z; z++) { for (s16 y = 0; y < size.Y; y++) { - memcpy(&m_data[i_local], &src[i_src], size.X * sizeof(*m_data)); - memset(&m_flags[i_local], 0, size.X); + if (is_mono_block) { + std::fill_n(m_data + i_local, size.X, src[0]); + } else { + std::copy_n(src + i_src, size.X, m_data + i_local); + } + std::fill_n(m_flags + i_local, size.X, 0); i_src += src_step; i_local += dest_step; } diff --git a/src/voxel.h b/src/voxel.h index 7fbaadc281..2e66e6f8b0 100644 --- a/src/voxel.h +++ b/src/voxel.h @@ -482,7 +482,7 @@ public: Copy data and set flags to 0 dst_area.getExtent() <= src_area.getExtent() */ - void copyFrom(MapNode *src, const VoxelArea& src_area, + void copyFrom(MapNode *src, bool is_mono_block, const VoxelArea& src_area, v3s16 from_pos, v3s16 to_pos, const v3s16 &size); // Copy data