ARTICLE AD BOX
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 <cstdint> #include <unordered_map> #include <unordered_set> #include <queue> #include <vector> #include <memory> #include <functional> #include <thread> #include <condition_variable> #include <mutex> #include <atomic> #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<Mxm::Vec2i, std::shared_ptr<Chunk>, Vec2iHash> _chunks; std::vector<std::thread> _workers; mutable std::mutex _mutex; std::condition_variable _cv; struct ChunkTask final { std::function<void()> func; int priority; bool operator<(const ChunkTask& other) const noexcept { return priority > other.priority; } }; std::priority_queue<ChunkTask> _tasks; std::atomic<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<Mxm::Vec2i, std::shared_ptr<Chunk>, Vec2iHash>& getChunks() noexcept { return _chunks; } const std::unordered_map<Mxm::Vec2i, std::shared_ptr<Chunk>, Vec2iHash>& getChunks() const noexcept { return _chunks; } }; #endifWorld.cpp:
#include "World.h" #include "../Utility/MathUtils.h" #include "ChunkMeshBuilder.h" #include <cmath> #include <utility> 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<std::mutex> 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<std::mutex> 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<Mxm::Vec2i, Vec2iHash> 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<std::mutex> lock(_mutex); if (!_chunks.count(chunkPos)) { canGenerate = true; } } if (canGenerate) { { std::lock_guard<std::mutex> lock(_mutex); _tasks.emplace([this, chunkPos]() { auto chunk = std::make_shared<Chunk>(); generateChunk(chunkPos, *chunk); chunk->state = ChunkState::MESHING; std::lock_guard<std::mutex> lock(_mutex); _chunks[chunkPos] = chunk; }, distToPlayer); } _cv.notify_one(); } } } { std::lock_guard<std::mutex> 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> chunk = chunkIt.second; if (chunk->state != ChunkState::MESHING) continue; { std::lock_guard<std::mutex> 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<std::mutex> 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<VertexArray>(); if (!chunk.mesh.vbo) chunk.mesh.vbo = std::make_unique<Buffer>(GL_ARRAY_BUFFER); if (!chunk.mesh.ebo) chunk.mesh.ebo = std::make_unique<Buffer>(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 <atomic> 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<ChunkState> state; }; #endifThis partially solves the problem by preventing the program from crashing, but leaves partitions between some chunks:
for (auto& chunkIt : _chunks) { std::shared_ptr<Chunk> chunk = chunkIt.second; if (chunk->state != ChunkState::MESHING) continue; ChunkMeshBuilder::buildMeshForChunk(*this, *chunk, chunkIt.first); chunk->state = ChunkState::READY; }