From 66de5f731a8a9f043dfe0ed8dea6da95bac0274c Mon Sep 17 00:00:00 2001 From: eldek Date: Mon, 16 Mar 2026 23:30:26 -0300 Subject: [PATCH] Added threaded chunks and fps --- main.cpp | 1070 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 609 insertions(+), 461 deletions(-) diff --git a/main.cpp b/main.cpp index 102707f..f4ef9cf 100644 --- a/main.cpp +++ b/main.cpp @@ -10,28 +10,40 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include // Build: // mkdir build && cd build && cmake .. && make -j$(nproc) // ./minicraft // ─── Constants ─────────────────────────────────────────────────────────────── -static const int SCREEN_W = 1280; -static const int SCREEN_H = 720; -static const int CHUNK_SIZE = 16; -static const int WORLD_H = 64; -static const int RENDER_DIST = 6; -static const float MOVE_SPEED = 6.0f; -static const float MOUSE_SENS = 0.12f; -static const float GRAVITY = -22.0f; -static const float JUMP_VEL = 9.0f; -static const float HW = 0.3f; -static const float PH = 1.8f; -static const int HOTBAR_SIZE = 8; +static const int SCREEN_W = 1280; +static const int SCREEN_H = 720; +static const int CHUNK_SIZE = 16; +static const int WORLD_H = 64; +static const int RENDER_DIST = 16; // sensible default; easily changed +static const float MOVE_SPEED = 6.0f; +static const float MOUSE_SENS = 0.12f; +static const float GRAVITY = -22.0f; +static const float JUMP_VEL = 9.0f; +static const float HW = 0.3f; +static const float PH = 1.8f; +static const int HOTBAR_SIZE = 8; +// How many chunk meshes we build per frame (tune up/down for perf vs pop-in) +static const int MESH_BUILDS_PER_FRAME = 4; +// Worker threads for chunk generation +static const int GEN_THREADS = (int)std::max(1u, std::thread::hardware_concurrency() - 1); // ─── Block types ───────────────────────────────────────────────────────────── enum BlockType : uint8_t { AIR=0, GRASS, DIRT, STONE, WOOD, LEAVES, SAND, SNOW, WATER }; @@ -40,14 +52,14 @@ static const int NUM_BLOCK_TYPES = 9; struct BlockColor { float r,g,b; }; static const BlockColor BLOCK_COLORS[] = { {0,0,0}, - {0.40f,0.72f,0.24f}, // GRASS - {0.55f,0.40f,0.22f}, // DIRT - {0.50f,0.50f,0.50f}, // STONE - {0.45f,0.30f,0.15f}, // WOOD - {0.22f,0.55f,0.18f}, // LEAVES - {0.88f,0.83f,0.52f}, // SAND - {0.92f,0.95f,0.98f}, // SNOW - {0.18f,0.42f,0.88f}, // WATER + {0.40f,0.72f,0.24f}, + {0.55f,0.40f,0.22f}, + {0.50f,0.50f,0.50f}, + {0.45f,0.30f,0.15f}, + {0.22f,0.55f,0.18f}, + {0.88f,0.83f,0.52f}, + {0.92f,0.95f,0.98f}, + {0.18f,0.42f,0.88f}, }; static const char* BLOCK_NAMES[] = { "Air","Grass","Dirt","Stone","Wood","Leaves","Sand","Snow","Water" @@ -72,9 +84,9 @@ static float noise2(float x,float y){ return lerp(lerp(grad(aa,xf,yf),grad(ba,xf-1,yf),u),lerp(grad(ab,xf,yf-1),grad(bb,xf-1,yf-1),u),v); } static float fbm(float x,float y,int oct=6){ - float val=0,amp=1,freq=1,max=0; - for(int i=0;i()((long long)k.x<<32|(unsigned)k.z);}}; -using ChunkMap=std::unordered_map,ChunkKeyHash>; +struct ChunkKey{ + int x,z; + bool operator==(const ChunkKey& o)const{return x==o.x&&z==o.z;} +}; +struct ChunkKeyHash{ + size_t operator()(const ChunkKey& k)const{ + return std::hash()((long long)(unsigned)k.x<<32|(unsigned)k.z); + } +}; +using ChunkMap = std::unordered_map,ChunkKeyHash>; + +// ─── Thread pool ───────────────────────────────────────────────────────────── +struct ThreadPool { + std::vector workers; + std::deque> tasks; + std::mutex mtx; + std::condition_variable cv; + std::atomic stop{false}; + std::atomic pending{0}; + + void start(int n){ + for(int i=0;i task; + { std::unique_lock lk(mtx); + cv.wait(lk,[this]{return stop.load()||!tasks.empty();}); + if(stop&&tasks.empty()) return; + task=std::move(tasks.front()); tasks.pop_front(); } + task(); --pending; + } + }); + } + void enqueue(std::function f){ + { std::lock_guard lk(mtx); ++pending; tasks.push_back(std::move(f)); } + cv.notify_one(); + } + void shutdown(){ + stop=true; cv.notify_all(); + for(auto& w:workers) w.join(); + } +} POOL; + +// ─── Global state ───────────────────────────────────────────────────────────── static ChunkMap CHUNKS; -static void generateChunk(Chunk& c){ - int wx0=c.cx*CHUNK_SIZE,wz0=c.cz*CHUNK_SIZE; +// Finished-generation queue: workers push here, main thread drains +struct FinishedChunk { int cx,cz; std::unique_ptr chunk; }; +static std::queue FINISHED_Q; +static std::mutex FINISHED_MTX; + +// In-flight set: prevents double-submitting +static std::unordered_set IN_FLIGHT; +static std::mutex IN_FLIGHT_MTX; + +// Dirty mesh queue (ordered set so we don't double-process) +static std::unordered_set DIRTY_SET; + +// ─── Chunk generation (worker thread, no GL) ────────────────────────────────── +static void generateChunkData(Chunk& c){ + int wx0=c.cx*CHUNK_SIZE, wz0=c.cz*CHUNK_SIZE; for(int x=0;x38); + float wx=(wx0+x)*0.008f, wz=(wz0+z)*0.008f; + float cont = fbm(wx*0.4f, wz*0.4f, 4); + float detail = fbm(wx*2.0f+5.3f, wz*2.0f+1.7f, 5); + float ridge = 1.0f-fabsf(fbm(wx*0.8f+3.1f, wz*0.8f+8.9f, 4)); + float h = 12.0f + cont*18.0f + detail*6.0f + ridge*14.0f*std::max(0.0f,cont); + int top = std::max(2, std::min(WORLD_H-2, (int)h)); + bool isSand=(top<16), isSnow=(top>38); for(int y=0;y=0&&nx=0&&nz=WORLD_H) return AIR; - int cx=(int)floorf((float)wx/CHUNK_SIZE),cz=(int)floorf((float)wz/CHUNK_SIZE); - auto it=CHUNKS.find({cx,cz}); if(it==CHUNKS.end()) return STONE; - return it->second->blocks[wx-cx*CHUNK_SIZE][wy][wz-cz*CHUNK_SIZE]; -} -static void setBlock(int wx,int wy,int wz,uint8_t val){ - if(wy<0||wy>=WORLD_H) return; - int cx=(int)floorf((float)wx/CHUNK_SIZE),cz=(int)floorf((float)wz/CHUNK_SIZE); - auto it=CHUNKS.find({cx,cz}); if(it==CHUNKS.end()) return; - int lx=wx-cx*CHUNK_SIZE,lz=wz-cz*CHUNK_SIZE; - it->second->blocks[lx][wy][lz]=val; it->second->meshDirty=true; - if(lx==0){auto n=CHUNKS.find({cx-1,cz});if(n!=CHUNKS.end())n->second->meshDirty=true;} - if(lx==CHUNK_SIZE-1){auto n=CHUNKS.find({cx+1,cz});if(n!=CHUNKS.end())n->second->meshDirty=true;} - if(lz==0){auto n=CHUNKS.find({cx,cz-1});if(n!=CHUNKS.end())n->second->meshDirty=true;} - if(lz==CHUNK_SIZE-1){auto n=CHUNKS.find({cx,cz+1});if(n!=CHUNKS.end())n->second->meshDirty=true;} +static void submitChunkGen(int cx,int cz){ + ChunkKey k{cx,cz}; + { std::lock_guard lk(IN_FLIGHT_MTX); + if(IN_FLIGHT.count(k)) return; + IN_FLIGHT.insert(k); } + POOL.enqueue([cx,cz,k]{ + auto c=std::make_unique(cx,cz); + generateChunkData(*c); + { std::lock_guard lk(FINISHED_MTX); + FINISHED_Q.push({cx,cz,std::move(c)}); } + { std::lock_guard lk(IN_FLIGHT_MTX); + IN_FLIGHT.erase(k); } + }); } -// ─── World mesh ────────────────────────────────────────────────────────────── -struct Vertex{float x,y,z,r,g,b;}; -static bool shouldDrawFace(uint8_t cur,int wx,int wy,int wz){ - uint8_t nb=getBlock(wx,wy,wz); - if(nb==AIR) return true; +// ─── Block accessors (main thread only) ────────────────────────────────────── +// Returns the block at world coords, using only the chunk's own data when +// called during mesh building (no cross-chunk queries that might be stale). +static uint8_t getBlock(int wx,int wy,int wz){ + if(wy<0) return STONE; if(wy>=WORLD_H) return AIR; + int cx=(int)floorf((float)wx/CHUNK_SIZE); + int cz=(int)floorf((float)wz/CHUNK_SIZE); + auto it=CHUNKS.find({cx,cz}); + if(it==CHUNKS.end()||!it->second->generated) return AIR; // treat unknown as AIR so border faces show + int lx=wx-cx*CHUNK_SIZE, lz=wz-cz*CHUNK_SIZE; + return it->second->blocks[lx][wy][lz]; +} + +static void setBlock(int wx,int wy,int wz,uint8_t val){ + if(wy<0||wy>=WORLD_H) return; + int cx=(int)floorf((float)wx/CHUNK_SIZE); + int cz=(int)floorf((float)wz/CHUNK_SIZE); + auto it=CHUNKS.find({cx,cz}); if(it==CHUNKS.end()) return; + int lx=wx-cx*CHUNK_SIZE, lz=wz-cz*CHUNK_SIZE; + it->second->blocks[lx][wy][lz]=val; + DIRTY_SET.insert({cx,cz}); + // Also dirty the four face-adjacent neighbours + static const int NX[]={-1,1,0,0}, NZ[]={0,0,-1,1}; + static const int BX[]={ 0,CHUNK_SIZE-1,lx,lx}, BZ[]={lz,lz,0,CHUNK_SIZE-1}; + (void)BX; (void)BZ; + if(lx==0) DIRTY_SET.insert({cx-1,cz}); + if(lx==CHUNK_SIZE-1) DIRTY_SET.insert({cx+1,cz}); + if(lz==0) DIRTY_SET.insert({cx,cz-1}); + if(lz==CHUNK_SIZE-1) DIRTY_SET.insert({cx,cz+1}); + (void)NX; (void)NZ; +} + +// ─── Mesh building (GL, main thread) ───────────────────────────────────────── +struct Vertex { float x,y,z,r,g,b; }; + +// When building a mesh we need to peek at neighbouring chunks for face culling. +// Key insight: we query getBlock() which returns AIR for unloaded chunks. +// This means border faces toward unloaded chunks will be SHOWN (not culled), +// which is correct — you can see the world edge. Once the neighbour loads, +// its arrival marks this chunk dirty and it gets rebuilt with proper culling. +static bool shouldDrawFace(uint8_t cur, int wx, int wy, int wz){ + uint8_t nb = getBlock(wx,wy,wz); + if(nb==AIR) return true; if(nb==WATER||nb==LEAVES) return cur!=nb; return false; } -static void addFace(std::vector& verts,std::vector& idx, - glm::vec3 v0,glm::vec3 v1,glm::vec3 v2,glm::vec3 v3,float r,float g,float b,float shade){ - GLuint base=(GLuint)verts.size(); float sr=r*shade,sg=g*shade,sb=b*shade; - verts.push_back({v0.x,v0.y,v0.z,sr,sg,sb}); verts.push_back({v1.x,v1.y,v1.z,sr,sg,sb}); - verts.push_back({v2.x,v2.y,v2.z,sr,sg,sb}); verts.push_back({v3.x,v3.y,v3.z,sr,sg,sb}); + +static void addFace(std::vector& verts, std::vector& idx, + glm::vec3 v0,glm::vec3 v1,glm::vec3 v2,glm::vec3 v3, + float r,float g,float b,float shade) +{ + GLuint base=(GLuint)verts.size(); + float sr=r*shade, sg=g*shade, sb=b*shade; + verts.push_back({v0.x,v0.y,v0.z,sr,sg,sb}); + verts.push_back({v1.x,v1.y,v1.z,sr,sg,sb}); + verts.push_back({v2.x,v2.y,v2.z,sr,sg,sb}); + verts.push_back({v3.x,v3.y,v3.z,sr,sg,sb}); idx.insert(idx.end(),{base,base+1,base+2,base,base+2,base+3}); } + static void buildChunkMesh(Chunk& c){ - std::vector verts; std::vector idx; - int wx0=c.cx*CHUNK_SIZE,wz0=c.cz*CHUNK_SIZE; - for(int x=0;x verts; + std::vector idx; + verts.reserve(4096); + idx.reserve(6144); + + int wx0=c.cx*CHUNK_SIZE, wz0=c.cz*CHUNK_SIZE; + + for(int x=0;x lk(FINISHED_MTX); + if(FINISHED_Q.empty()) break; + fc=std::move(FINISHED_Q.front()); FINISHED_Q.pop(); } + + ChunkKey k{fc.cx,fc.cz}; + CHUNKS[k]=std::move(fc.chunk); + + // Schedule this chunk AND its four face-neighbours for meshing. + // The neighbours need to be rebuilt so their border faces toward this + // newly arrived chunk get properly culled. + DIRTY_SET.insert(k); + DIRTY_SET.insert({fc.cx-1,fc.cz}); + DIRTY_SET.insert({fc.cx+1,fc.cz}); + DIRTY_SET.insert({fc.cx,fc.cz-1}); + DIRTY_SET.insert({fc.cx,fc.cz+1}); + } +} + +// Process up to N chunks from the dirty set each frame. +// Only build a chunk if it AND all four face-neighbours are generated — +// this ensures the border-face culling is correct on the first build. +static void processDirtyMeshes(int maxPerFrame, int pcx, int pcz){ + if(DIRTY_SET.empty()) return; + + // Sort dirty chunks by distance to player for closest-first pop-in + std::vector sorted(DIRTY_SET.begin(),DIRTY_SET.end()); + std::sort(sorted.begin(),sorted.end(),[&](const ChunkKey& a,const ChunkKey& b){ + int da=(a.x-pcx)*(a.x-pcx)+(a.z-pcz)*(a.z-pcz); + int db=(b.x-pcx)*(b.x-pcx)+(b.z-pcz)*(b.z-pcz); + return da=maxPerFrame) break; + + auto it=CHUNKS.find(k); + if(it==CHUNKS.end()){ DIRTY_SET.erase(k); continue; } // evicted + + Chunk& c=*it->second; + if(!c.generated){ continue; } // still generating + + // Check all four face-neighbours exist and are generated. + // If a neighbour doesn't exist yet, keep this chunk dirty and skip — + // it will be rebuilt properly when the neighbour arrives. + bool allNeighbours=true; + const int dx[]={-1,1,0,0}, dz[]={0,0,-1,1}; + for(int i=0;i<4;i++){ + auto nit=CHUNKS.find({k.x+dx[i],k.z+dz[i]}); + if(nit==CHUNKS.end()||!nit->second->generated){ + allNeighbours=false; break; + } + } + if(!allNeighbours) continue; // wait for neighbours + + buildChunkMesh(c); + DIRTY_SET.erase(k); + ++built; + } } // ─── Shaders ───────────────────────────────────────────────────────────────── -static const char* VS_SRC=R"( +static const char* VS_SRC = R"( #version 330 core layout(location=0) in vec3 aPos; layout(location=1) in vec3 aColor; -out vec3 vColor; out float vFog; -uniform mat4 uMVP; uniform vec3 uCamPos; +out vec3 vColor; +out float vFog; +uniform mat4 uMVP; +uniform vec3 uCamPos; void main(){ - gl_Position=uMVP*vec4(aPos,1.0); vColor=aColor; - float dist=length(aPos-uCamPos); - vFog=clamp((dist-60.0)/40.0,0.0,1.0); -})"; -static const char* FS_SRC=R"( -#version 330 core -in vec3 vColor; in float vFog; out vec4 fragColor; -void main(){ - vec3 fogColor=vec3(0.53,0.81,0.98); - fragColor=vec4(mix(vColor,fogColor,vFog),1.0); + gl_Position = uMVP * vec4(aPos,1.0); + vColor = aColor; + float dist = length(aPos - uCamPos); + vFog = clamp((dist - 80.0) / 60.0, 0.0, 1.0); })"; -static const char* VS_UI=R"( +static const char* FS_SRC = R"( +#version 330 core +in vec3 vColor; +in float vFog; +out vec4 fragColor; +void main(){ + vec3 fogColor = vec3(0.53, 0.81, 0.98); + fragColor = vec4(mix(vColor, fogColor, vFog), 1.0); +})"; + +static const char* VS_UI = R"( #version 330 core layout(location=0) in vec2 aPos; layout(location=1) in vec3 aColor; out vec3 vColor; uniform mat4 uProj; -void main(){ gl_Position=uProj*vec4(aPos,0.0,1.0); vColor=aColor; })"; -static const char* FS_UI=R"( -#version 330 core -in vec3 vColor; out vec4 fragColor; -uniform float uAlpha; -void main(){ fragColor=vec4(vColor,uAlpha); })"; +void main(){ gl_Position = uProj*vec4(aPos,0.0,1.0); vColor = aColor; })"; -static GLuint compileShader(GLenum type,const char* src){ +static const char* FS_UI = R"( +#version 330 core +in vec3 vColor; +out vec4 fragColor; +uniform float uAlpha; +void main(){ fragColor = vec4(vColor, uAlpha); })"; + +static GLuint compileShader(GLenum type, const char* src){ GLuint s=glCreateShader(type); glShaderSource(s,1,&src,nullptr); glCompileShader(s); GLint ok; glGetShaderiv(s,GL_COMPILE_STATUS,&ok); - if(!ok){char buf[512];glGetShaderInfoLog(s,512,nullptr,buf);std::cerr<<"Shader err: "< - {0x02,0x01,0x51,0x09,0x06}, // ? - {0x32,0x49,0x79,0x41,0x3E}, // @ - {0x7E,0x11,0x11,0x11,0x7E}, // A - {0x7F,0x49,0x49,0x49,0x36}, // B - {0x3E,0x41,0x41,0x41,0x22}, // C - {0x7F,0x41,0x41,0x22,0x1C}, // D - {0x7F,0x49,0x49,0x49,0x41}, // E - {0x7F,0x09,0x09,0x09,0x01}, // F - {0x3E,0x41,0x49,0x49,0x7A}, // G - {0x7F,0x08,0x08,0x08,0x7F}, // H - {0x00,0x41,0x7F,0x41,0x00}, // I - {0x20,0x40,0x41,0x3F,0x01}, // J - {0x7F,0x08,0x14,0x22,0x41}, // K - {0x7F,0x40,0x40,0x40,0x40}, // L - {0x7F,0x02,0x04,0x02,0x7F}, // M - {0x7F,0x04,0x08,0x10,0x7F}, // N - {0x3E,0x41,0x41,0x41,0x3E}, // O - {0x7F,0x09,0x09,0x09,0x06}, // P - {0x3E,0x41,0x51,0x21,0x5E}, // Q - {0x7F,0x09,0x19,0x29,0x46}, // R - {0x46,0x49,0x49,0x49,0x31}, // S - {0x01,0x01,0x7F,0x01,0x01}, // T - {0x3F,0x40,0x40,0x40,0x3F}, // U - {0x1F,0x20,0x40,0x20,0x1F}, // V - {0x3F,0x40,0x38,0x40,0x3F}, // W - {0x63,0x14,0x08,0x14,0x63}, // X - {0x07,0x08,0x70,0x08,0x07}, // Y - {0x61,0x51,0x49,0x45,0x43}, // Z - {0x00,0x7F,0x41,0x41,0x00}, // [ - {0x02,0x04,0x08,0x10,0x20}, // backslash - {0x00,0x41,0x41,0x7F,0x00}, // ] - {0x04,0x02,0x01,0x02,0x04}, // ^ - {0x40,0x40,0x40,0x40,0x40}, // _ - {0x00,0x01,0x02,0x04,0x00}, // ` - {0x20,0x54,0x54,0x54,0x78}, // a - {0x7F,0x48,0x44,0x44,0x38}, // b - {0x38,0x44,0x44,0x44,0x20}, // c - {0x38,0x44,0x44,0x48,0x7F}, // d - {0x38,0x54,0x54,0x54,0x18}, // e - {0x08,0x7E,0x09,0x01,0x02}, // f - {0x0C,0x52,0x52,0x52,0x3E}, // g - {0x7F,0x08,0x04,0x04,0x78}, // h - {0x00,0x44,0x7D,0x40,0x00}, // i - {0x20,0x40,0x44,0x3D,0x00}, // j - {0x7F,0x10,0x28,0x44,0x00}, // k - {0x00,0x41,0x7F,0x40,0x00}, // l - {0x7C,0x04,0x18,0x04,0x78}, // m - {0x7C,0x08,0x04,0x04,0x78}, // n - {0x38,0x44,0x44,0x44,0x38}, // o - {0x7C,0x14,0x14,0x14,0x08}, // p - {0x08,0x14,0x14,0x18,0x7C}, // q - {0x7C,0x08,0x04,0x04,0x08}, // r - {0x48,0x54,0x54,0x54,0x20}, // s - {0x04,0x3F,0x44,0x40,0x20}, // t - {0x3C,0x40,0x40,0x20,0x7C}, // u - {0x1C,0x20,0x40,0x20,0x1C}, // v - {0x3C,0x40,0x30,0x40,0x3C}, // w - {0x44,0x28,0x10,0x28,0x44}, // x - {0x0C,0x50,0x50,0x50,0x3C}, // y - {0x44,0x64,0x54,0x4C,0x44}, // z - {0x00,0x08,0x36,0x41,0x00}, // { - {0x00,0x00,0x7F,0x00,0x00}, // | - {0x00,0x41,0x36,0x08,0x00}, // } - {0x10,0x08,0x08,0x10,0x08}, // ~ -}; - static void drawText(const char* text,float x,float y,float scale, float r,float g,float b,float a=1.0f){ - float cx=x; for(int i=0;text[i];i++){ int c=text[i]-32; - if(c<0||c>=(int)(sizeof(FONT5x7)/sizeof(FONT5x7[0]))){cx+=(6*scale);continue;} + if(c<0||c>=(int)(sizeof(FONT5x7)/sizeof(FONT5x7[0]))){x+=6*scale;continue;} for(int col=0;col<5;col++){ uint8_t bits=FONT5x7[c][col]; - for(int row=0;row<7;row++){ - if(bits&(1<0?(float)ready/total:0; + UI.rect(bx,by,bw,bh,0.2f,0.2f,0.2f); + UI.rect(bx,by,bw*pct,bh,0.3f,0.7f,0.3f); + UI.outline(bx,by,bw,bh,0.5f,0.5f,0.5f,2.f); + + char buf[48]; snprintf(buf,48,"%d / %d chunks",ready,total); + drawText(buf,(SCREEN_W-strlen(buf)*6*1.5f)*0.5f,by+bh+8,1.5f,0.7f,0.7f,0.7f); + + UI.end(); + SDL_GL_SwapWindow(win); +} + // ─── Inventory ─────────────────────────────────────────────────────────────── struct Inventory { uint8_t hotbar[HOTBAR_SIZE]={GRASS,DIRT,STONE,WOOD,LEAVES,SAND,SNOW,WATER}; - int selected=0; - bool open=false; + int selected=0; bool open=false; uint8_t selectedBlock()const{return hotbar[selected];} }; - static void drawInventory(Inventory& inv){ - const float SLOT=52.0f; - const float GAP=6.0f; - const float BORDER=4.0f; - int n=HOTBAR_SIZE; - float totalW=n*(SLOT+GAP)-GAP; - float hx=(SCREEN_W-totalW)*0.5f; - float hy=SCREEN_H-SLOT-16.0f; - - // Background bar - UI.rect(hx-8,hy-8,totalW+16,SLOT+16,0.15f,0.15f,0.15f,0.75f); - UI.outline(hx-8,hy-8,totalW+16,SLOT+16,0.0f,0.0f,0.0f); - - for(int i=0;i=55?0.3f:fps>=30?1.f:1.f, + fps>=55?1.0f:fps>=30?0.8f:0.3f, + fps>=55?0.3f:fps>=30?0.1f:0.1f); + + char cbuf[48]; snprintf(cbuf,48,"Chunks: %d vis / %d load",visChunks,bgPending); + drawText(cbuf,9,35,1.5f,.7f,.7f,.7f); + + if(thirdPerson) drawText("[F5] 3rd person",9,49,1.5f,.8f,.8f,.5f); + + // Loading badge (top-right) + if(bgPending>0){ + char lb[40]; snprintf(lb,40,"Loading %d...",bgPending); + float lw=strlen(lb)*9+8; + UI.rect(SCREEN_W-lw-4,5,lw,16,0,0,0,.6f); + drawText(lb,SCREEN_W-lw,8,1.5f,.4f,1,.4f); + } } -// ─── Camera / Player ───────────────────────────────────────────────────────── +// ─── Camera ────────────────────────────────────────────────────────────────── struct Camera { - glm::vec3 pos{0,40,0}; - float yaw=0,pitch=0; - glm::vec3 vel{0,0,0}; - bool onGround=false; - + glm::vec3 pos{0,40,0}; float yaw=0,pitch=0; + glm::vec3 vel{0,0,0}; bool onGround=false; glm::vec3 forward()const{ float y=glm::radians(yaw),p=glm::radians(pitch); return glm::normalize(glm::vec3(cosf(p)*cosf(y),sinf(p),cosf(p)*sinf(y))); @@ -519,16 +653,14 @@ struct Camera { glm::vec3 right()const{return glm::normalize(glm::cross(forward(),{0,1,0}));} glm::mat4 firstPersonView()const{return glm::lookAt(pos,pos+forward(),{0,1,0});} glm::mat4 thirdPersonView()const{ - glm::vec3 fwd=forward(); - glm::vec3 eye=pos-fwd*6.0f+glm::vec3(0,2,0); - return glm::lookAt(eye,pos,{0,1,0}); + return glm::lookAt(pos-forward()*6.f+glm::vec3(0,2,0),pos,{0,1,0}); } }; - static bool solidAt(float fx,float fy,float fz){ - int x=(int)floorf(fx),y=(int)floorf(fy),z=(int)floorf(fz); - if(y<0) return true; if(y>=WORLD_H) return false; - uint8_t b=getBlock(x,y,z); return b!=AIR&&b!=WATER&&b!=LEAVES; + if((int)floorf(fy)<0) return true; + if((int)floorf(fy)>=WORLD_H) return false; + uint8_t b=getBlock((int)floorf(fx),(int)floorf(fy),(int)floorf(fz)); + return b!=AIR&&b!=WATER&&b!=LEAVES; } static bool aabbSolid(float px,float py,float pz){ int x0=(int)floorf(px-HW),x1=(int)floorf(px+HW); @@ -544,75 +676,60 @@ static void moveCamera(Camera& cam,const glm::vec3& move,float dt){ cam.vel.y+=GRAVITY*dt; cam.pos.y+=cam.vel.y*dt; if(cam.vel.y<0){ if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)){ - cam.pos.y=floorf(cam.pos.y-PH)+1.0f+PH+0.001f; - cam.vel.y=0; cam.onGround=true; + cam.pos.y=floorf(cam.pos.y-PH)+1.f+PH+.001f; cam.vel.y=0; cam.onGround=true; } else cam.onGround=false; } else { if(aabbSolid(cam.pos.x,cam.pos.y,cam.pos.z)){ - cam.pos.y=floorf(cam.pos.y+0.05f)-0.05f-0.001f; cam.vel.y=0; + cam.pos.y=floorf(cam.pos.y+.05f)-.05f-.001f; cam.vel.y=0; } cam.onGround=false; } } -// ─── Player model (3rd person) ─────────────────────────────────────────────── +// ─── Player model ───────────────────────────────────────────────────────────── struct PlayerModel { GLuint VAO=0,VBO=0,EBO=0; - std::vector verts; - std::vector idx; + std::vector verts; std::vector idx; int indexCount=0; - - void addBox(float x0,float y0,float z0,float x1,float y1,float z1, - float r,float g,float b){ - auto face=[&](glm::vec3 v0,glm::vec3 v1,glm::vec3 v2,glm::vec3 v3,float shade){ + void addBox(float x0,float y0,float z0,float x1,float y1,float z1,float r,float g,float b){ + auto face=[&](glm::vec3 a,glm::vec3 b_,glm::vec3 c,glm::vec3 d,float s){ GLuint base=(GLuint)verts.size(); - float sr=r*shade,sg=g*shade,sb=b*shade; - verts.push_back({v0.x,v0.y,v0.z,sr,sg,sb}); verts.push_back({v1.x,v1.y,v1.z,sr,sg,sb}); - verts.push_back({v2.x,v2.y,v2.z,sr,sg,sb}); verts.push_back({v3.x,v3.y,v3.z,sr,sg,sb}); + verts.push_back({a.x,a.y,a.z,r*s,g*s,b*s}); verts.push_back({b_.x,b_.y,b_.z,r*s,g*s,b*s}); + verts.push_back({c.x,c.y,c.z,r*s,g*s,b*s}); verts.push_back({d.x,d.y,d.z,r*s,g*s,b*s}); idx.insert(idx.end(),{base,base+1,base+2,base,base+2,base+3}); }; - face({x0,y1,z0},{x1,y1,z0},{x1,y1,z1},{x0,y1,z1},1.0f); // Top - face({x0,y0,z1},{x1,y0,z1},{x1,y0,z0},{x0,y0,z0},0.5f); // Bottom - face({x0,y0,z1},{x1,y0,z1},{x1,y1,z1},{x0,y1,z1},0.8f); // Front - face({x1,y0,z0},{x0,y0,z0},{x0,y1,z0},{x1,y1,z0},0.7f); // Back - face({x0,y0,z0},{x0,y0,z1},{x0,y1,z1},{x0,y1,z0},0.65f); // Left - face({x1,y0,z1},{x1,y0,z0},{x1,y1,z0},{x1,y1,z1},0.65f); // Right + face({x0,y1,z0},{x1,y1,z0},{x1,y1,z1},{x0,y1,z1},1.f); + face({x0,y0,z1},{x1,y0,z1},{x1,y0,z0},{x0,y0,z0},.5f); + face({x0,y0,z1},{x1,y0,z1},{x1,y1,z1},{x0,y1,z1},.8f); + face({x1,y0,z0},{x0,y0,z0},{x0,y1,z0},{x1,y1,z0},.7f); + face({x0,y0,z0},{x0,y0,z1},{x0,y1,z1},{x0,y1,z0},.65f); + face({x1,y0,z1},{x1,y0,z0},{x1,y1,z0},{x1,y1,z1},.65f); } - void build(){ verts.clear(); idx.clear(); - float sk=0.90f,sg_=0.72f,sb_=0.55f; - float tr=0.25f,tg=0.45f,tb=0.75f; - float lr=0.20f,lg=0.20f,lb=0.60f; - float hr=0.30f,hg=0.18f,hb=0.08f; - - addBox(-0.3f,1.55f,-0.3f, 0.3f,2.15f,0.3f, sk,sg_,sb_); // Head - addBox(-0.32f,1.9f,-0.32f, 0.32f,2.17f,0.32f, hr,hg,hb); // Hair - addBox(-0.3f,0.7f,-0.2f, 0.3f,1.5f,0.2f, tr,tg,tb); // Body - addBox(-0.55f,0.7f,-0.15f, -0.32f,1.45f,0.15f, sk,sg_,sb_);// Left arm - addBox( 0.32f,0.7f,-0.15f, 0.55f,1.45f,0.15f, sk,sg_,sb_);// Right arm - addBox(-0.28f,0.0f,-0.15f, -0.05f,0.7f,0.15f, lr,lg,lb); // Left leg - addBox( 0.05f,0.0f,-0.15f, 0.28f,0.7f,0.15f, lr,lg,lb); // Right leg - + addBox(-.3f,1.55f,-.3f,.3f,2.15f,.3f,.9f,.72f,.55f); + addBox(-.32f,1.9f,-.32f,.32f,2.17f,.32f,.3f,.18f,.08f); + addBox(-.3f,.7f,-.2f,.3f,1.5f,.2f,.25f,.45f,.75f); + addBox(-.55f,.7f,-.15f,-.32f,1.45f,.15f,.9f,.72f,.55f); + addBox(.32f,.7f,-.15f,.55f,1.45f,.15f,.9f,.72f,.55f); + addBox(-.28f,0,-.15f,-.05f,.7f,.15f,.2f,.2f,.6f); + addBox(.05f,0,-.15f,.28f,.7f,.15f,.2f,.2f,.6f); indexCount=(int)idx.size(); - if(!VAO){glGenVertexArrays(1,&VAO);glGenBuffers(1,&VBO);glGenBuffers(1,&EBO);} glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER,VBO); - glBufferData(GL_ARRAY_BUFFER,verts.size()*sizeof(Vertex),verts.data(),GL_STATIC_DRAW); + glBufferData(GL_ARRAY_BUFFER,(GLsizeiptr)(verts.size()*sizeof(Vertex)),verts.data(),GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO); - glBufferData(GL_ELEMENT_ARRAY_BUFFER,idx.size()*sizeof(GLuint),idx.data(),GL_STATIC_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER,(GLsizeiptr)(idx.size()*sizeof(GLuint)),idx.data(),GL_STATIC_DRAW); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Vertex),(void*)(3*sizeof(float))); glEnableVertexAttribArray(1); glBindVertexArray(0); } - void draw(GLuint prog,GLint mvpLoc,GLint camLoc, - const glm::mat4& proj,const glm::mat4& view, - const Camera& cam) const { + const glm::mat4& proj,const glm::mat4& view,const Camera& cam)const{ glm::vec3 feet=cam.pos-glm::vec3(0,PH,0); - glm::mat4 model=glm::translate(glm::mat4(1),feet); - model=glm::rotate(model,glm::radians(cam.yaw+180.0f),glm::vec3(0,1,0)); + glm::mat4 model=glm::rotate(glm::translate(glm::mat4(1),feet), + glm::radians(cam.yaw+180.f),glm::vec3(0,1,0)); glm::mat4 mvp=proj*view*model; glUseProgram(prog); glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp)); @@ -621,15 +738,13 @@ struct PlayerModel { glDrawElements(GL_TRIANGLES,(GLsizei)indexCount,GL_UNSIGNED_INT,0); glBindVertexArray(0); } -}; - -static PlayerModel PLAYER_MODEL; +} PLAYER_MODEL; // ─── Raycast ───────────────────────────────────────────────────────────────── static bool raycast(const Camera& cam,glm::ivec3& hit,glm::ivec3& prev){ glm::vec3 dir=cam.forward(),p=cam.pos; glm::ivec3 last{(int)floorf(p.x),(int)floorf(p.y),(int)floorf(p.z)}; - for(float t=0;t<8.0f;t+=0.05f){ + for(float t=0;t<8.f;t+=0.05f){ glm::vec3 rp=p+dir*t; glm::ivec3 bp{(int)floorf(rp.x),(int)floorf(rp.y),(int)floorf(rp.z)}; if(bp.y<0||bp.y>=WORLD_H){last=bp;continue;} @@ -640,23 +755,28 @@ static bool raycast(const Camera& cam,glm::ivec3& hit,glm::ivec3& prev){ return false; } -// ─── Chunk streaming ───────────────────────────────────────────────────────── -static void updateChunks(int pcx,int pcz){ +// ─── Chunk range management ─────────────────────────────────────────────────── +static void requestChunks(int pcx,int pcz){ + struct E{int dx,dz,d2;}; + std::vector needed; for(int dx=-RENDER_DIST;dx<=RENDER_DIST;dx++) for(int dz=-RENDER_DIST;dz<=RENDER_DIST;dz++){ ChunkKey k{pcx+dx,pcz+dz}; - if(CHUNKS.find(k)==CHUNKS.end()){ - auto c=std::make_unique(k.x,k.z); generateChunk(*c); - CHUNKS[k]=std::move(c); - for(auto& [nk,nc]:CHUNKS) if(abs(nk.x-k.x)+abs(nk.z-k.z)==1) nc->meshDirty=true; - } + if(CHUNKS.count(k)) continue; + needed.push_back({dx,dz,dx*dx+dz*dz}); } - std::vector toRemove; - for(auto& [k,c]:CHUNKS) if(abs(k.x-pcx)>RENDER_DIST+1||abs(k.z-pcz)>RENDER_DIST+1) toRemove.push_back(k); - for(auto& k:toRemove) CHUNKS.erase(k); + std::sort(needed.begin(),needed.end(),[](auto& a,auto& b){return a.d2 rem; + for(auto& [k,c]:CHUNKS) + if(abs(k.x-pcx)>RENDER_DIST+2||abs(k.z-pcz)>RENDER_DIST+2) rem.push_back(k); + for(auto& k:rem){ CHUNKS.erase(k); DIRTY_SET.erase(k); } +} + +// ─── Main ───────────────────────────────────────────────────────────────────── int main(){ SDL_Init(SDL_INIT_VIDEO); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3); @@ -673,7 +793,6 @@ int main(){ glewExperimental=GL_TRUE; glewInit(); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); - glClearColor(0.53f,0.81f,0.98f,1.0f); GLuint prog3d=buildProg(VS_SRC,FS_SRC); GLint mvpLoc=glGetUniformLocation(prog3d,"uMVP"); @@ -682,146 +801,175 @@ int main(){ UI.init(); initNoise(12345); - std::cout<<"Generating world...\n"; - updateChunks(0,0); - for(auto& [k,c]:CHUNKS) if(c->generated) buildChunkMesh(*c); + std::cout<<"Using "<(dx,dz); + generateChunkData(*c); + CHUNKS[{dx,dz}]=std::move(c); + } + // Build their meshes (all neighbours present so culling is correct) + for(auto& [k,c]:CHUNKS) buildChunkMesh(*c); + + // Submit the rest async + requestChunks(0,0); + + int totalChunks=(2*RENDER_DIST+1)*(2*RENDER_DIST+1); + int minToPlay=9; // 3×3 inner zone Camera cam; for(int y=WORLD_H-1;y>=0;y--){ - uint8_t b=getBlock(0,y,0); - if(b!=AIR&&b!=WATER){cam.pos.y=(float)y+1+PH+0.1f;break;} + if(getBlock(0,y,0)!=AIR&&getBlock(0,y,0)!=WATER){ + cam.pos.y=y+1+PH+0.1f; break; + } } cam.pos.x=0.5f; cam.pos.z=0.5f; - PLAYER_MODEL.build(); - glm::mat4 proj=glm::perspective(glm::radians(70.0f),(float)SCREEN_W/SCREEN_H,0.05f,500.0f); + glm::mat4 proj=glm::perspective(glm::radians(70.f),(float)SCREEN_W/SCREEN_H,0.05f,1000.f); SDL_SetRelativeMouseMode(SDL_TRUE); Inventory inv; - bool thirdPerson=false; - bool running=true; + bool thirdPerson=false, running=true, loading=true; Uint64 last=SDL_GetPerformanceCounter(); - int lastPcx=INT_MAX,lastPcz=INT_MAX; + int lastPcx=INT_MAX, lastPcz=INT_MAX; - std::cout<<"=== MiniCraft ===\n" - <<"WASD Move\n" - <<"Space Jump\n" - <<"Mouse Look\n" - <<"LMB Break block\n" - <<"RMB Place block\n" - <<"1-8 Hotbar slot\n" - <<"E Open/close inventory\n" - <<"F5 Toggle 3rd person\n" - <<"ESC Quit\n"; + // FPS tracking + int fpsCounter=0; + float fpsTimer=0.f; + int fpsDisplay=0; + + std::cout<<"WASD=move Space=jump LMB=break RMB=place\n" + "1-8=hotbar E=inv F5=3rd-person ESC=quit\n"; while(running){ + // ── Delta time ── Uint64 now=SDL_GetPerformanceCounter(); float dt=(float)(now-last)/SDL_GetPerformanceFrequency(); if(dt>0.1f) dt=0.1f; last=now; + // ── FPS ── + fpsCounter++; + fpsTimer+=dt; + if(fpsTimer>=1.0f){ fpsDisplay=fpsCounter; fpsCounter=0; fpsTimer-=1.0f; } + + // ── Drain finished chunks ── + drainFinishedChunks(); + int readyChunks=(int)CHUNKS.size(); + if(loading&&readyChunks>=minToPlay) loading=false; + + // ── Events ── SDL_Event e; while(SDL_PollEvent(&e)){ if(e.type==SDL_QUIT) running=false; + if(e.type==SDL_KEYDOWN&&e.key.keysym.sym==SDLK_ESCAPE) running=false; + if(loading) continue; + if(e.type==SDL_KEYDOWN){ switch(e.key.keysym.sym){ - case SDLK_ESCAPE: running=false; break; - case SDLK_e: - inv.open=!inv.open; - SDL_SetRelativeMouseMode(inv.open?SDL_FALSE:SDL_TRUE); - break; + case SDLK_e: inv.open=!inv.open; + SDL_SetRelativeMouseMode(inv.open?SDL_FALSE:SDL_TRUE); break; case SDLK_F5: thirdPerson=!thirdPerson; break; - case SDLK_1: inv.selected=0; break; - case SDLK_2: inv.selected=1; break; - case SDLK_3: inv.selected=2; break; - case SDLK_4: inv.selected=3; break; - case SDLK_5: inv.selected=4; break; - case SDLK_6: inv.selected=5; break; - case SDLK_7: inv.selected=6; break; - case SDLK_8: inv.selected=7; break; + case SDLK_1: inv.selected=0; break; case SDLK_2: inv.selected=1; break; + case SDLK_3: inv.selected=2; break; case SDLK_4: inv.selected=3; break; + case SDLK_5: inv.selected=4; break; case SDLK_6: inv.selected=5; break; + case SDLK_7: inv.selected=6; break; case SDLK_8: inv.selected=7; break; } } - if(e.type==SDL_MOUSEWHEEL){ + if(e.type==SDL_MOUSEWHEEL) inv.selected=(inv.selected-e.wheel.y+HOTBAR_SIZE)%HOTBAR_SIZE; - } if(!inv.open){ if(e.type==SDL_MOUSEMOTION){ cam.yaw+=e.motion.xrel*MOUSE_SENS; - cam.pitch-=e.motion.yrel*MOUSE_SENS; - if(cam.pitch>89)cam.pitch=89; if(cam.pitch<-89)cam.pitch=-89; + cam.pitch=std::max(-89.f,std::min(89.f,cam.pitch-e.motion.yrel*MOUSE_SENS)); } if(e.type==SDL_MOUSEBUTTONDOWN){ glm::ivec3 hit,prev; if(raycast(cam,hit,prev)){ - if(e.button.button==SDL_BUTTON_LEFT) - setBlock(hit.x,hit.y,hit.z,AIR); + if(e.button.button==SDL_BUTTON_LEFT) setBlock(hit.x,hit.y,hit.z,AIR); else if(e.button.button==SDL_BUTTON_RIGHT) setBlock(prev.x,prev.y,prev.z,inv.selectedBlock()); } } - } else { - if(e.type==SDL_MOUSEBUTTONDOWN&&e.button.button==SDL_BUTTON_LEFT){ - const int COLS=4; const float SLOT=52.0f,GAP=6.0f; - float iw=COLS*(SLOT+GAP)-GAP+32.0f; - float ih=((NUM_BLOCK_TYPES-1+COLS-1)/COLS)*(SLOT+GAP)-GAP+60.0f; - float ix=(SCREEN_W-iw)*0.5f,iy=(SCREEN_H-ih)*0.5f; - int mx=e.button.x,my=e.button.y; - for(int bi=1;bi=sx&&mx<=sx+SLOT&&my>=sy&&my<=sy+SLOT){ - inv.hotbar[inv.selected]=(uint8_t)bi; - } - } + } else if(e.type==SDL_MOUSEBUTTONDOWN&&e.button.button==SDL_BUTTON_LEFT){ + const int COLS=4; const float SL=52,GL=6; + float iw=COLS*(SL+GL)-GL+32,ih=((NUM_BLOCK_TYPES-1+COLS-1)/COLS)*(SL+GL)-GL+60; + float ix=(SCREEN_W-iw)*.5f,iy=(SCREEN_H-ih)*.5f; + for(int bi=1;bi=sx&&e.button.x<=sx+SL&&e.button.y>=sy&&e.button.y<=sy+SL) + inv.hotbar[inv.selected]=(uint8_t)bi; } } } + // ── Loading screen ── + if(loading){ + drawLoadingScreen(win,readyChunks,totalChunks); + SDL_Delay(16); + continue; + } + + // ── Player movement ── if(!inv.open){ const Uint8* keys=SDL_GetKeyboardState(nullptr); glm::vec3 fwd=cam.forward(); fwd.y=0; if(glm::length(fwd)>0) fwd=glm::normalize(fwd); glm::vec3 rgt=cam.right(); rgt.y=0; if(glm::length(rgt)>0) rgt=glm::normalize(rgt); - glm::vec3 move{0,0,0}; - if(keys[SDL_SCANCODE_W]) move+=fwd*MOVE_SPEED; - if(keys[SDL_SCANCODE_S]) move-=fwd*MOVE_SPEED; - if(keys[SDL_SCANCODE_D]) move+=rgt*MOVE_SPEED; - if(keys[SDL_SCANCODE_A]) move-=rgt*MOVE_SPEED; + glm::vec3 mv{0,0,0}; + if(keys[SDL_SCANCODE_W]) mv+=fwd*MOVE_SPEED; + if(keys[SDL_SCANCODE_S]) mv-=fwd*MOVE_SPEED; + if(keys[SDL_SCANCODE_D]) mv+=rgt*MOVE_SPEED; + if(keys[SDL_SCANCODE_A]) mv-=rgt*MOVE_SPEED; if(keys[SDL_SCANCODE_SPACE]&&cam.onGround) cam.vel.y=JUMP_VEL; - moveCamera(cam,move,dt); + moveCamera(cam,mv,dt); } + // ── Chunk streaming ── int pcx=(int)floorf(cam.pos.x/CHUNK_SIZE); int pcz=(int)floorf(cam.pos.z/CHUNK_SIZE); - if(pcx!=lastPcx||pcz!=lastPcz){ updateChunks(pcx,pcz); lastPcx=pcx; lastPcz=pcz; } - int rebuilt=0; - for(auto& [k,c]:CHUNKS) - if(c->meshDirty&&c->generated){buildChunkMesh(*c);if(++rebuilt>=4)break;} + if(pcx!=lastPcx||pcz!=lastPcz){ + requestChunks(pcx,pcz); + evictChunks(pcx,pcz); + lastPcx=pcx; lastPcz=pcz; + } - // ── Render 3D world ── + // ── Build dirty meshes ── + processDirtyMeshes(MESH_BUILDS_PER_FRAME, pcx, pcz); + + // ── 3D render ── + glClearColor(0.53f,0.81f,0.98f,1.0f); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glm::mat4 view=thirdPerson?cam.thirdPersonView():cam.firstPersonView(); glm::mat4 mvp=proj*view; - glUseProgram(prog3d); glUniformMatrix4fv(mvpLoc,1,GL_FALSE,glm::value_ptr(mvp)); glUniform3fv(camLoc,1,glm::value_ptr(cam.pos)); - for(auto& [k,c]:CHUNKS) - if(c->indexCount>0){glBindVertexArray(c->VAO);glDrawElements(GL_TRIANGLES,c->indexCount,GL_UNSIGNED_INT,0);} - // ── Player model (3rd person only) ── - if(thirdPerson) - PLAYER_MODEL.draw(prog3d,mvpLoc,camLoc,proj,view,cam); + int visChunks=0; + for(auto& [k,c]:CHUNKS) + if(c->meshReady&&c->indexCount>0){ + glBindVertexArray(c->VAO); + glDrawElements(GL_TRIANGLES,c->indexCount,GL_UNSIGNED_INT,0); + ++visChunks; + } + + if(thirdPerson) PLAYER_MODEL.draw(prog3d,mvpLoc,camLoc,proj,view,cam); // ── 2D UI ── UI.begin(); - drawHUD(cam.pos,inv.selectedBlock(),thirdPerson); + drawHUD(cam.pos,thirdPerson,POOL.pending.load(),fpsDisplay,visChunks); drawInventory(inv); UI.end(); SDL_GL_SwapWindow(win); } + POOL.shutdown(); CHUNKS.clear(); glDeleteProgram(prog3d); glDeleteProgram(UI.prog);