C++ voxel engine: random crashes with multithreaded chunk generation and meshing (possible race condition)
07:44 02 May 2026

I'm new to multithreaded C++ programming, so I've encountered a problem where my program generates several chunks when running, and then immediately crashes. I suspect the problem is a race condition. I've attached the implementations of some of the main classes below. I'm guessing the problem is in the chunk mesh building, because when I bring this process into the main thread, everything starts working right away.

World.h:

#ifndef WORLD_H
#define WORLD_H

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 

#include "../Mxm/Vec2i.h"
#include "../Mxm/Vec3i.h"
#include "../Mxm/Vec3.h"

#include "../Utility/Vec2iHash.h"
#include "Chunk.h"

class World final
{
private:
    std::unordered_map, Vec2iHash> _chunks;

    std::vector _workers;
    mutable std::mutex _mutex;
    std::condition_variable _cv;

    struct ChunkTask final {
        std::function func;
        int priority;

        bool operator<(const ChunkTask& other) const noexcept {
            return priority > other.priority;
        }
    };
    std::priority_queue _tasks;
    bool _ready;

    void markChunkDirty(const Mxm::Vec2i& chunkPos) noexcept;
    void workerLoop();
public:
    World();
    ~World();

    void generateChunk(const Mxm::Vec2i& chunkPos, Chunk& chunk);
    void update(const Mxm::Vec3& playerPos);

    BlockType getBlock(const Mxm::Vec3i& blockPos) const;
    bool isAir(const Mxm::Vec3i& blockPos) const;
    bool isLimitHeight(const Mxm::Vec3i& blockPos) const noexcept;

    std::unordered_map, Vec2iHash>& getChunks() noexcept { return _chunks; }
    const std::unordered_map, Vec2iHash>& getChunks() const noexcept { return _chunks; }
};

#endif

World.cpp:

#include "World.h"
#include "../Utility/MathUtils.h"
#include "ChunkMeshBuilder.h"

#include 
#include 

World::World() {
    _ready = false;

    unsigned int threadsCount = 1;
    for (int i = 0; i < threadsCount; i++) {
        _workers.emplace_back(&World::workerLoop, this);
    }
}
World::~World() {
    _ready = true;
    
    _cv.notify_all();
    for (auto& worker : _workers) {
        if (worker.joinable()) {
            worker.join();
        }
    }
}

void World::workerLoop() {
    while (!_ready) {
        ChunkTask task;
        {
            std::unique_lock lock(_mutex);
            _cv.wait(lock, [this]() { return _ready || !_tasks.empty(); });

            if (_ready && _tasks.empty()) return;
                
            task = std::move(_tasks.top());
            _tasks.pop();
        }
        task.func();
    }
}

void World::markChunkDirty(const Mxm::Vec2i& chunkPos) noexcept {
    std::lock_guard lock(_mutex);
    
    auto chunk = _chunks.find(chunkPos);
    if (chunk != _chunks.end()) { 
        chunk->second->state = ChunkState::MESHING;
    }
}

void World::generateChunk(const Mxm::Vec2i& chunkPos, Chunk& chunk) {
    for (int ix = 0; ix < ChunkConsts::CHUNK_WIDTH; ix++) {
        for (int iz = 0; iz < ChunkConsts::CHUNK_WIDTH; iz++) {
            Mxm::Vec2i worldPos = Mxm::Vec2i(ix + ChunkConsts::CHUNK_WIDTH * chunkPos.x, iz + ChunkConsts::CHUNK_WIDTH * chunkPos.y);
            int height = MathUtils::noise((float)worldPos.x * 0.04f, (float)worldPos.y * 0.04f) * 20.0f + 5.0f;

            for (int iy = 0; iy < ChunkConsts::CHUNK_HEIGHT; iy++) {
                if (iy <= height) {
                    chunk.blocks[ix][iz][iy] = BlockType::COATING;
                } else {
                    chunk.blocks[ix][iz][iy] = BlockType::AIR;
                }
            }
        }
    }
    chunk.state = ChunkState::MESHING;

    markChunkDirty(chunkPos + Mxm::Vec2i(1, 0));
    markChunkDirty(chunkPos + Mxm::Vec2i(-1, 0));
    markChunkDirty(chunkPos + Mxm::Vec2i(0, 1));
    markChunkDirty(chunkPos + Mxm::Vec2i(0, -1));
}

void World::update(const Mxm::Vec3& playerPos) {
    Mxm::Vec2i playerChunk = Mxm::Vec2i(
        (int)std::floor(playerPos.x / ChunkConsts::CHUNK_WIDTH), 
        (int)std::floor(playerPos.z / ChunkConsts::CHUNK_WIDTH));

    static std::unordered_set requiredChunks;
    requiredChunks.clear();
    
    int distance = 12;
    for (int dx = -distance; dx <= distance; dx++) {
        for (int dz = -distance; dz <= distance; dz++) {
            int distToPlayer = dx * dx + dz * dz;
            if (distToPlayer > distance * distance) continue;
            
            Mxm::Vec2i chunkPos = playerChunk + Mxm::Vec2i(dx, dz);
            requiredChunks.insert(chunkPos);

            bool canGenerate = false;
            {
                std::lock_guard lock(_mutex);
                if (!_chunks.count(chunkPos)) {
                    canGenerate = true;
                }
            }

            if (canGenerate) {
                {
                    std::lock_guard lock(_mutex);
                    
                    _tasks.emplace([this, chunkPos]() {
                        auto chunk = std::make_shared();
                
                        generateChunk(chunkPos, *chunk);
                        chunk->state = ChunkState::MESHING;
                
                        std::lock_guard lock(_mutex);
                        _chunks[chunkPos] = chunk;
                    }, distToPlayer);
                }
                _cv.notify_one();
            }
        }
    }

    {
        std::lock_guard lock(_mutex);
    
        auto it = _chunks.begin();
        while (it != _chunks.end()) {
            if (!requiredChunks.count(it->first)) {
                it = _chunks.erase(it);
            } else {
                ++it;
            }
        }
    }

    for (auto& chunkIt : _chunks) {
        std::shared_ptr chunk = chunkIt.second;
        if (chunk->state != ChunkState::MESHING) continue;
        
        {
            std::lock_guard lock(_mutex);
            
            _tasks.push(ChunkTask{[this, chunk, pos = chunkIt.first]() {
                ChunkMeshBuilder::buildMeshForChunk(*this, *chunk, pos);
                chunk->state = ChunkState::READY;
            }, 0});
        }
        _cv.notify_one();
    }
}

BlockType World::getBlock(const Mxm::Vec3i& blockPos) const {
    if (isLimitHeight(blockPos)) return BlockType::AIR;

    std::lock_guard lock(_mutex);
    
    Mxm::Vec2i chunkPos = Mxm::Vec2i(
        MathUtils::fastFloorDiv(blockPos.x, ChunkConsts::CHUNK_WIDTH), 
        MathUtils::fastFloorDiv(blockPos.z, ChunkConsts::CHUNK_WIDTH)
    );

    auto it = _chunks.find(chunkPos);
    if (it == _chunks.end()) {
        return BlockType::AIR;
    }

    Mxm::Vec3i localBlockPos = Mxm::Vec3i(
        blockPos.x - chunkPos.x * ChunkConsts::CHUNK_WIDTH,
        blockPos.y, 
        blockPos.z - chunkPos.y * ChunkConsts::CHUNK_WIDTH
    );
    
    return it->second->blocks[localBlockPos.x][localBlockPos.z][localBlockPos.y];
}
bool World::isAir(const Mxm::Vec3i& blockPos) const {
    return getBlock(blockPos) == BlockType::AIR;
}
bool World::isLimitHeight(const Mxm::Vec3i& blockPos) const noexcept {
    return blockPos.y < 0 || blockPos.y >= ChunkConsts::CHUNK_HEIGHT;
}

ChunkMeshBuilder.cpp:

#include "ChunkMeshBuilder.h"
#include "../Graphics/Renderer.h"

Mxm::Vec2i ChunkMeshBuilder::getPosInAtlas(BlockType blockType, Direction direction) noexcept {
    switch (blockType)
    {
    case BlockType::COATING:
        return Mxm::Vec2i(0, 0);
    case BlockType::BREED:
        return Mxm::Vec2i(1, 0);
    case BlockType::BEDROCK:
        return Mxm::Vec2i(2, 0);
    }

    return Mxm::Vec2i(0, 0);
}

bool ChunkMeshBuilder::isNeedFace(const World& world, const Mxm::Vec3i& blockPos) noexcept {
    return world.isAir(blockPos);
}

void ChunkMeshBuilder::addFace(Chunk& chunk, const Mxm::Vec2i& chunkPos, const Mxm::Vec3i& globalBlockPos, BlockType blockType, Direction direction) {
    Mxm::Vec3 pos = Mxm::Vec3((float)globalBlockPos.x, (float)globalBlockPos.y, (float)globalBlockPos.z);
    
    Vertex vertices[4];

    switch (direction) {
        case Direction::RIGHT:
            vertices[0].pos = Mxm::Vec3(pos.x + 1.0f, pos.y,         pos.z);
            vertices[1].pos = Mxm::Vec3(pos.x + 1.0f, pos.y + 1.0f,  pos.z);
            vertices[2].pos = Mxm::Vec3(pos.x + 1.0f, pos.y + 1.0f,  pos.z + 1.0f);
            vertices[3].pos = Mxm::Vec3(pos.x + 1.0f, pos.y,         pos.z + 1.0f);

            for (int i = 0; i < 4; i++) vertices[i].light = 0.8f;
            break;
            
        case Direction::LEFT:
            vertices[0].pos = Mxm::Vec3(pos.x, pos.y,         pos.z);
            vertices[1].pos = Mxm::Vec3(pos.x, pos.y + 1.0f,  pos.z);
            vertices[2].pos = Mxm::Vec3(pos.x, pos.y + 1.0f,  pos.z + 1.0f);
            vertices[3].pos = Mxm::Vec3(pos.x, pos.y,         pos.z + 1.0f);

            for (int i = 0; i < 4; i++) vertices[i].light = 0.4f;
            break;
            
        case Direction::UP:
            vertices[0].pos = Mxm::Vec3(pos.x,         pos.y + 1.0f, pos.z);
            vertices[1].pos = Mxm::Vec3(pos.x,         pos.y + 1.0f, pos.z + 1.0f);
            vertices[2].pos = Mxm::Vec3(pos.x + 1.0f,  pos.y + 1.0f, pos.z + 1.0f);
            vertices[3].pos = Mxm::Vec3(pos.x + 1.0f,  pos.y + 1.0f, pos.z);

            for (int i = 0; i < 4; i++) vertices[i].light = 0.7f;
            break;
            
        case Direction::BOTTOM:
            vertices[0].pos = Mxm::Vec3(pos.x,         pos.y, pos.z);
            vertices[1].pos = Mxm::Vec3(pos.x,         pos.y, pos.z + 1.0f);
            vertices[2].pos = Mxm::Vec3(pos.x + 1.0f,  pos.y, pos.z + 1.0f);
            vertices[3].pos = Mxm::Vec3(pos.x + 1.0f,  pos.y, pos.z);

            for (int i = 0; i < 4; i++) vertices[i].light = 0.4f;
            break;
            
        case Direction::FRONT:
            vertices[0].pos = Mxm::Vec3(pos.x,         pos.y,         pos.z + 1.0f);
            vertices[1].pos = Mxm::Vec3(pos.x,         pos.y + 1.0f,  pos.z + 1.0f);
            vertices[2].pos = Mxm::Vec3(pos.x + 1.0f,  pos.y + 1.0f,  pos.z + 1.0f);
            vertices[3].pos = Mxm::Vec3(pos.x + 1.0f,  pos.y,         pos.z + 1.0f);
            
            for (int i = 0; i < 4; i++) vertices[i].light = 0.6f;
            break;
            
        case Direction::BACK:
            vertices[0].pos = Mxm::Vec3(pos.x + 1.0f,  pos.y,         pos.z);
            vertices[1].pos = Mxm::Vec3(pos.x + 1.0f,  pos.y + 1.0f,  pos.z);
            vertices[2].pos = Mxm::Vec3(pos.x,         pos.y + 1.0f,  pos.z);
            vertices[3].pos = Mxm::Vec3(pos.x,         pos.y,         pos.z);

            for (int i = 0; i < 4; i++) vertices[i].light = 0.5f;
            break;
    }

    Mxm::Vec2i atlasPos = getPosInAtlas(blockType, direction);
    float u0 = (float)atlasPos.x * UV_OFFSET + UV_EPS;
    float v0 = (float)atlasPos.y * UV_OFFSET + UV_EPS;
    float u1 = (float)atlasPos.x * UV_OFFSET + UV_OFFSET - UV_EPS;
    float v1 = (float)atlasPos.y * UV_OFFSET + UV_OFFSET - UV_EPS;

    vertices[0].uv = Mxm::Vec2(u0, v1);
    vertices[1].uv = Mxm::Vec2(u0, v0);
    vertices[2].uv = Mxm::Vec2(u1, v0);
    vertices[3].uv = Mxm::Vec2(u1, v1);

    auto& verts = chunk.mesh.vertices;
    uint32_t start = verts.size();
    
    verts.push_back(vertices[0]);
    verts.push_back(vertices[1]);
    verts.push_back(vertices[2]);
    verts.push_back(vertices[3]);

    auto& inds = chunk.mesh.indices;
    inds.push_back(start + 0);
    inds.push_back(start + 1);
    inds.push_back(start + 2);
    inds.push_back(start + 0);
    inds.push_back(start + 2);
    inds.push_back(start + 3);
}

void ChunkMeshBuilder::buildMeshForChunk(World& world, Chunk& chunk, const Mxm::Vec2i& chunkPos) {
    for (int ix = 0; ix < ChunkConsts::CHUNK_WIDTH; ix++) {
        for (int iz = 0; iz < ChunkConsts::CHUNK_WIDTH; iz++) {
            for (int iy = 0; iy < ChunkConsts::CHUNK_HEIGHT; iy++) {
                BlockType type = chunk.blocks[ix][iz][iy];
                if (type == BlockType::AIR) continue;

                Mxm::Vec3i globalBlockPos = Mxm::Vec3i(
                    chunkPos.x * ChunkConsts::CHUNK_WIDTH + ix,
                    iy,
                    chunkPos.y * ChunkConsts::CHUNK_WIDTH + iz
                );

                if (isNeedFace(world, globalBlockPos + Mxm::Vec3i(1, 0, 0)))  addFace(chunk, chunkPos, globalBlockPos, type, Direction::RIGHT);
                if (isNeedFace(world, globalBlockPos + Mxm::Vec3i(-1, 0, 0))) addFace(chunk, chunkPos, globalBlockPos, type, Direction::LEFT);
                if (isNeedFace(world, globalBlockPos + Mxm::Vec3i(0, 1, 0)))  addFace(chunk, chunkPos, globalBlockPos, type, Direction::UP);
                if (isNeedFace(world, globalBlockPos + Mxm::Vec3i(0, -1, 0))) addFace(chunk, chunkPos, globalBlockPos, type, Direction::BOTTOM);
                if (isNeedFace(world, globalBlockPos + Mxm::Vec3i(0, 0, 1)))  addFace(chunk, chunkPos, globalBlockPos, type, Direction::FRONT);
                if (isNeedFace(world, globalBlockPos + Mxm::Vec3i(0, 0, -1))) addFace(chunk, chunkPos, globalBlockPos, type, Direction::BACK);
            }
        }
    }
}

void ChunkMeshBuilder::setChunkGeometry(Chunk& chunk) noexcept {
    if (!chunk.mesh.vao) chunk.mesh.vao = std::make_unique();
    if (!chunk.mesh.vbo) chunk.mesh.vbo = std::make_unique(GL_ARRAY_BUFFER);
    if (!chunk.mesh.ebo) chunk.mesh.ebo = std::make_unique(GL_ELEMENT_ARRAY_BUFFER);

    chunk.mesh.vao->bind();

    chunk.mesh.vbo->bind();
    chunk.mesh.vbo->bufferData(chunk.mesh.vertices.size() * sizeof(Vertex), chunk.mesh.vertices.data(), GL_DYNAMIC_DRAW);

    chunk.mesh.ebo->bind();
    chunk.mesh.ebo->bufferData(chunk.mesh.indices.size() * sizeof(uint32_t), chunk.mesh.indices.data(), GL_STATIC_DRAW);

    chunk.mesh.vao->setAttribute(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(offsetof(Vertex, pos)));
    chunk.mesh.vao->setAttribute(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(offsetof(Vertex, uv)));
    chunk.mesh.vao->setAttribute(2, 1, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(offsetof(Vertex, light)));
    
    chunk.mesh.vao->enableAttribute(0);
    chunk.mesh.vao->enableAttribute(1);
    chunk.mesh.vao->enableAttribute(2);
}

void ChunkMeshBuilder::drawAllChunks(World& world, const Renderer& renderer) noexcept {
    for (auto& chunkIt : world.getChunks()) {
        if (chunkIt.second->state == ChunkState::READY) {
            setChunkGeometry(*chunkIt.second);
        }
        renderer.drawChunk(chunkIt.second->mesh);
    }
}

Chunk.h:

#ifndef CHUNK_H
#define CHUNK_H

#include "Block.h"
#include "ChunkMesh.h"

#include 

namespace ChunkConsts {
    constexpr unsigned int CHUNK_WIDTH = 16;
    constexpr unsigned int CHUNK_HEIGHT = 128;
}
enum class ChunkState : uint32_t {
    GENERATION, MESHING, READY
};

struct Chunk final
{
    BlockType blocks[ChunkConsts::CHUNK_WIDTH][ChunkConsts::CHUNK_WIDTH][ChunkConsts::CHUNK_HEIGHT];
    ChunkMesh mesh;
    std::atomic state;
};

#endif
c++ multithreading c++17