1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-09-30 19:22:14 +00:00

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.
This commit is contained in:
lhofhansl 2025-09-21 13:19:30 -07:00 committed by GitHub
parent afd681d013
commit 08b7870c79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 245 additions and 47 deletions

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@
#include "mapblock.h"
#include <memory>
#include <sstream>
#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; i<nodecount; i++) {
if (n != data[i]) {
is_mono_block = false;
break;
}
}
if (is_mono_block) {
reallocate(1, n);
m_is_air = n.getContent() == CONTENT_AIR;
m_is_air_expired = false;
}
}
void MapBlock::expandNodesIfNeeded()
{
if (m_is_mono_block) {
reallocate(nodecount, data[0]);
}
}
void MapBlock::actuallyUpdateIsAir()
@ -233,6 +289,10 @@ void MapBlock::actuallyUpdateIsAir()
// Running this function un-expires m_is_air
m_is_air_expired = false;
if (m_is_mono_block) {
m_is_air = data[0].getContent() == CONTENT_AIR;
return;
}
bool only_air = true;
for (u32 i = 0; i < nodecount; i++) {
MapNode &n = data[i];
@ -260,12 +320,12 @@ void MapBlock::expireIsAirCache()
// Note that there's no technical reason why we *have to* renumber the IDs,
// but we do it anyway as it also helps compressability.
void MapBlock::getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes,
const NodeDefManager *nodedef)
u32 count, const NodeDefManager *nodedef)
{
IdIdMapping &mapping = IdIdMapping::giveClearedThreadLocalInstance();
content_t id_counter = 0;
for (u32 i = 0; i < MapBlock::nodecount; i++) {
for (u32 i = 0; i < count; i++) {
content_t global_id = nodes[i].getContent();
content_t id = CONTENT_IGNORE;
@ -378,13 +438,13 @@ void MapBlock::serialize(std::ostream &os_compressed, u8 version, bool disk, int
const u8 params_width = 2;
if(disk)
{
MapNode *tmp_nodes = new MapNode[nodecount];
memcpy(tmp_nodes, data, nodecount * sizeof(MapNode));
getBlockNodeIdMapping(&nimap, tmp_nodes, m_gamedef->ndef());
const size_t size = m_is_mono_block ? 1 : nodecount;
std::unique_ptr<MapNode[]> 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 "<<getPos()<<std::endl);
m_is_air_expired = true;
expandNodesIfNeeded();
if(version <= 21)
{
@ -593,9 +654,13 @@ void MapBlock::deSerialize(std::istream &in_compressed, u8 version, bool disk)
m_node_timers.deSerialize(is, version);
}
u16 dummy;
m_is_air = nimap.size() == 1 && nimap.getId("air", dummy);
m_is_air_expired = false;
if (nimap.size() == 1) {
tryShrinkNodes();
u16 dummy;
m_is_air = nimap.getId("air", dummy);
m_is_air_expired = false;
}
}
TRACESTREAM(<<"MapBlock::deSerialize "<<getPos()

View file

@ -22,6 +22,7 @@ class IGameDef;
class MapBlockMesh;
class VoxelManipulator;
class NameIdMapping;
class TestMapBlock;
#define BLOCK_TIMESTAMP_UNDEFINED 0xffffffff
@ -30,7 +31,7 @@ class NameIdMapping;
////
enum ModReason : u32 {
MOD_REASON_REALLOCATE = 1 << 0,
// UNUSED = 1 << 0,
MOD_REASON_SET_IS_UNDERGROUND = 1 << 1,
MOD_REASON_SET_LIGHTING_COMPLETE = 1 << 2,
MOD_REASON_SET_GENERATED = 1 << 3,
@ -74,13 +75,6 @@ public:
m_orphan = true;
}
void reallocate()
{
for (u32 i = 0; i < nodecount; i++)
data[i] = MapNode(CONTENT_IGNORE);
raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_REALLOCATE);
}
////
//// Modification tracking methods
////
@ -234,7 +228,7 @@ public:
if (!*valid_position)
return {CONTENT_IGNORE};
return data[z * zstride + y * ystride + x];
return data[m_is_mono_block ? 0 : z * zstride + y * ystride + x];
}
inline MapNode getNode(v3s16 p, bool *valid_position)
@ -253,6 +247,7 @@ public:
if (!isValidPosition(x, y, z))
throw InvalidPositionException();
expandNodesIfNeeded();
data[z * zstride + y * ystride + x] = n;
raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_SET_NODE);
}
@ -268,7 +263,7 @@ public:
inline MapNode getNodeNoCheck(s16 x, s16 y, s16 z)
{
return data[z * zstride + y * ystride + x];
return data[m_is_mono_block ? 0 : z * zstride + y * ystride + x];
}
inline MapNode getNodeNoCheck(v3s16 p)
@ -278,6 +273,7 @@ public:
inline void setNodeNoCheck(s16 x, s16 y, s16 z, MapNode n)
{
expandNodesIfNeeded();
data[z * zstride + y * ystride + x] = n;
raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_SET_NODE);
}
@ -431,14 +427,25 @@ private:
static const u32 nodecount = MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE;
private:
#if BUILD_UNITTESTS
// access to data, tryConvertToMonoBlock, deconvertMonoblock
friend class TestMapBlock;
#endif
/*
Private methods
*/
void deSerialize_pre22(std::istream &is, u8 version, bool disk);
// check if all nodes are identical, if so convert to monoblock
void tryShrinkNodes();
// if a monoblock, expand storage back to the full array
void expandNodesIfNeeded();
void reallocate(u32 count, MapNode n);
static void getBlockNodeIdMapping(NameIdMapping *nimap, MapNode *nodes,
const NodeDefManager *nodedef);
u32 count, const NodeDefManager *nodedef);
static void correctBlockNodeIds(const NameIdMapping *nimap, MapNode *nodes,
IGameDef *gamedef);
@ -475,11 +482,11 @@ private:
short m_refcount = 0;
/*
* Note that this is not an inline array because that has implications for
* heap fragmentation (the array is exactly 16K), CPU caches and/or
* optimizability of algorithms working on this array.
* Note that this is not an inline array because that has implications for heap
* fragmentation (the array is exactly 16K, or exactly 4 bytes for a "monoblock"),
* CPU caches and/or optimizability of algorithms working on this array.
*/
MapNode *const data; // of `nodecount` elements
MapNode *data = nullptr;
// provides the item and node definitions
IGameDef *m_gamedef;
@ -490,6 +497,11 @@ private:
*/
float m_usage_timer = 0;
/*
* For "monoblocks", the whole block is filled with the same node, only this node is stored.
* (For reduced memory usage)
*/
bool m_is_mono_block;
public:
//// ABM optimizations ////
// True if we never want to cache content types for this block

View file

@ -589,7 +589,7 @@ void MapNode::deSerialize(const u8 *source, u8 version)
Buffer<u8> 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<u8> 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;
}

View file

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

View file

@ -319,6 +319,7 @@ public:
std::list<std::string> *log);
// IGameDef interface
bool isClient() override { return false; }
// Under envlock
virtual IItemDefManager* getItemDefManager();
virtual const NodeDefManager* getNodeDefManager();

View file

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

View file

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

View file

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