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