From bbeec3478149b550fbcbcdadf8d7ad87c8e1d034 Mon Sep 17 00:00:00 2001 From: Michael Howard Date: Thu, 23 Apr 2026 16:21:36 -0500 Subject: [PATCH] Fix batching logic and audio state management - v1.7.1 --- src/main.cpp | 204 +++++++++++++++++++++++---------------------------- 1 file changed, 90 insertions(+), 114 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index eca9c51..5550530 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -67,10 +67,12 @@ struct ChunkPosHash { struct Chunk { int blocks[CHUNK_SIZE][CHUNK_HEIGHT][CHUNK_SIZE]; - bool generated; - bool modified; - int maxY; // Highest non-air Y in this chunk; limits the render loop - Chunk() : generated(false), modified(false), maxY(0) {} + int maxY = 0; + bool generated = false; + bool modified = false; + bool dirty = true; + std::vector renderLists[16]; + Chunk() : generated(false), modified(false), maxY(0), dirty(true) {} }; // Global variables for persistence @@ -78,6 +80,12 @@ std::unordered_map worldChunks; static unsigned int globalSeedHash = 0; static std::string currentWorldName = ""; +// Forward Declarations +bool IsExposedOptimized(int lx, int ly, int lz, Chunk* chunk, Chunk* nxM, Chunk* nxP, Chunk* nzM, Chunk* nzP); +void RebuildChunkRenderList(Chunk* chunk, int cx, int cz); +void GenerateChunk(int cx, int cz); +void SetBlock(int x, int y, int z, int type); + // ---- Inventory System ---- struct InventorySlot { int blockType = AIR; @@ -170,6 +178,37 @@ int GetBlock(int x, int y, int z) { return 0; // Air if chunk not loaded } +void RebuildChunkRenderList(Chunk* chunk, int cx, int cz) { + for (int i = 0; i < 16; i++) chunk->renderLists[i].clear(); + + // Neighbors for exposure check + auto itNM = worldChunks.find({cx-1, cz}); + Chunk* nxM = (itNM != worldChunks.end()) ? itNM->second : nullptr; + auto itNP = worldChunks.find({cx+1, cz}); + Chunk* nxP = (itNP != worldChunks.end()) ? itNP->second : nullptr; + auto itZM = worldChunks.find({cx, cz-1}); + Chunk* nzM = (itZM != worldChunks.end()) ? itZM->second : nullptr; + auto itZP = worldChunks.find({cx, cz+1}); + Chunk* nzP = (itZP != worldChunks.end()) ? itZP->second : nullptr; + + int worldX = cx * CHUNK_SIZE; + int worldZ = cz * CHUNK_SIZE; + + for (int lx = 0; lx < CHUNK_SIZE; lx++) { + for (int ly = 0; ly <= chunk->maxY; ly++) { + for (int lz = 0; lz < CHUNK_SIZE; lz++) { + int bt = chunk->blocks[lx][ly][lz]; + if (bt == AIR) continue; + + if (IsExposedOptimized(lx, ly, lz, chunk, nxM, nxP, nzM, nzP)) { + chunk->renderLists[bt].push_back((Vector3){(float)(worldX+lx), (float)ly, (float)(worldZ+lz)}); + } + } + } + } + chunk->dirty = false; +} + void SetBlock(int x, int y, int z, int type) { if (y < 0 || y >= CHUNK_HEIGHT) return; int cx = (int)floorf((float)x / CHUNK_SIZE); @@ -180,9 +219,16 @@ void SetBlock(int x, int y, int z, int type) { int lz = z - (cz * CHUNK_SIZE); worldChunks[key]->blocks[lx][y][lz] = type; worldChunks[key]->modified = true; - // Keep maxY up to date when placing blocks above the current max - if (type != AIR && y > worldChunks[key]->maxY) - worldChunks[key]->maxY = y; + worldChunks[key]->dirty = true; + + // Keep maxY up to date + if (type != AIR && y > worldChunks[key]->maxY) worldChunks[key]->maxY = y; + + // Mark neighbors dirty as well since exposure changes + if (lx == 0) { auto n = worldChunks.find({cx-1, cz}); if (n != worldChunks.end()) n->second->dirty = true; } + if (lx == CHUNK_SIZE-1) { auto n = worldChunks.find({cx+1, cz}); if (n != worldChunks.end()) n->second->dirty = true; } + if (lz == 0) { auto n = worldChunks.find({cx, cz-1}); if (n != worldChunks.end()) n->second->dirty = true; } + if (lz == CHUNK_SIZE-1) { auto n = worldChunks.find({cx, cz+1}); if (n != worldChunks.end()) n->second->dirty = true; } } } @@ -768,12 +814,15 @@ int main(void) { // Update //---------------------------------------------------------------------------------- - // Update ONLY active music streams to save CPU + // Update ONLY active music streams and ensure others are fully stopped if (currentState == MAIN_MENU || currentState == WORLD_SELECT || currentState == CREATE_WORLD) { UpdateMusicStream(titleMusic); + if (IsMusicStreamPlaying(gameplayMusic)) StopMusicStream(gameplayMusic); + if (IsMusicStreamPlaying(nightMusic)) StopMusicStream(nightMusic); } else { UpdateMusicStream(gameplayMusic); UpdateMusicStream(nightMusic); + if (IsMusicStreamPlaying(titleMusic)) StopMusicStream(titleMusic); } // Handle title music loop fading @@ -1099,8 +1148,8 @@ int main(void) DrawTexturePro(titleTexture, sourceRec, destRec, origin, 0.0f, WHITE); EndMode2D(); - // Show Version Number (v1.7.0) in Red - DrawTextEx(customFont, "v1.7.0", (Vector2){ (float)currentWidth - 140, (float)currentHeight - 40 }, 22, 1.0f, RED); + // Show Version Number (v1.7.1) in Red + DrawTextEx(customFont, "v1.7.1", (Vector2){ (float)currentWidth - 140, (float)currentHeight - 40 }, 22, 1.0f, RED); } Vector2 mousePos = GetMousePosition(); @@ -1574,124 +1623,51 @@ int main(void) Vector3 camForward = Vector3Normalize(Vector3Subtract(camera3D.target, camera3D.position)); - // Optimized Render Loop: Pre-fetch neighbor chunks and apply Frustum Culling + // --- PERFORMANCE OPTIMIZED BATCHED RENDER LOOP --- + // 1. Identify chunks to render and rebuild dirty ones + std::vector visibleChunks; + for (int cx = playerCX - RENDER_DISTANCE; cx <= playerCX + RENDER_DISTANCE; cx++) { + for (int cz = playerCZ - RENDER_DISTANCE; cz <= playerCZ + RENDER_DISTANCE; cz++) { + // FRUSTUM CULLING + Vector3 chunkCenter = { (float)(cx * CHUNK_SIZE + 8), 32.0f, (float)(cz * CHUNK_SIZE + 8) }; + Vector3 toChunk = Vector3Normalize(Vector3Subtract(chunkCenter, camera3D.position)); + if (Vector3Length(Vector3Subtract(chunkCenter, camera3D.position)) > 24.0f && + Vector3DotProduct(toChunk, camForward) < 0.4f) continue; + + auto it = worldChunks.find({cx, cz}); + if (it == worldChunks.end()) continue; + Chunk* chunk = it->second; + + if (chunk->dirty) RebuildChunkRenderList(chunk, cx, cz); + visibleChunks.push_back(chunk); + } + } + + // 2. Batch render per type for (int renderType = 1; renderType <= 13; renderType++) { if (renderType == GRASS) { - for (int cx = playerCX - RENDER_DISTANCE; cx <= playerCX + RENDER_DISTANCE; cx++) { - for (int cz = playerCZ - RENDER_DISTANCE; cz <= playerCZ + RENDER_DISTANCE; cz++) { - // FRUSTUM CULLING: Skip chunks not in view - Vector3 chunkCenter = { (float)(cx * CHUNK_SIZE + 8), 32.0f, (float)(cz * CHUNK_SIZE + 8) }; - Vector3 toChunk = Vector3Normalize(Vector3Subtract(chunkCenter, camera3D.position)); - if (Vector3Length(Vector3Subtract(chunkCenter, camera3D.position)) > 24.0f && - Vector3DotProduct(toChunk, camForward) < 0.4f) continue; - - auto it = worldChunks.find({cx, cz}); - if (it == worldChunks.end()) continue; - Chunk* chunk = it->second; - - auto itNM = worldChunks.find({cx-1, cz}); - Chunk* nxM = (itNM != worldChunks.end()) ? itNM->second : nullptr; - auto itNP = worldChunks.find({cx+1, cz}); - Chunk* nxP = (itNP != worldChunks.end()) ? itNP->second : nullptr; - auto itZM = worldChunks.find({cx, cz-1}); - Chunk* nzM = (itZM != worldChunks.end()) ? itZM->second : nullptr; - auto itZP = worldChunks.find({cx, cz+1}); - Chunk* nzP = (itZP != worldChunks.end()) ? itZP->second : nullptr; - - int worldX = cx * CHUNK_SIZE; - int worldZ = cz * CHUNK_SIZE; - for (int lx = 0; lx < CHUNK_SIZE; lx++) { - for (int ly = 0; ly <= chunk->maxY; ly++) { - for (int lz = 0; lz < CHUNK_SIZE; lz++) { - if (chunk->blocks[lx][ly][lz] != GRASS) continue; - - if (IsExposedOptimized(lx, ly, lz, chunk, nxM, nxP, nzM, nzP)) { - DrawGrassBlock((Vector3){(float)(worldX+lx), (float)ly, (float)(worldZ+lz)}, - blockTextures[GRASS].id, grassTopTexture.id, blockTextures[DIRT].id, blockTint); - } - } - } - } + for (Chunk* chunk : visibleChunks) { + for (auto& pos : chunk->renderLists[GRASS]) { + DrawGrassBlock(pos, blockTextures[GRASS].id, grassTopTexture.id, blockTextures[DIRT].id, blockTint); } } } else if (renderType == CRAFTING_TABLE) { - for (int cx = playerCX - RENDER_DISTANCE; cx <= playerCX + RENDER_DISTANCE; cx++) { - for (int cz = playerCZ - RENDER_DISTANCE; cz <= playerCZ + RENDER_DISTANCE; cz++) { - // FRUSTUM CULLING - Vector3 chunkCenter = { (float)(cx * CHUNK_SIZE + 8), 32.0f, (float)(cz * CHUNK_SIZE + 8) }; - Vector3 toChunk = Vector3Normalize(Vector3Subtract(chunkCenter, camera3D.position)); - if (Vector3Length(Vector3Subtract(chunkCenter, camera3D.position)) > 24.0f && - Vector3DotProduct(toChunk, camForward) < 0.4f) continue; - - auto it = worldChunks.find({cx, cz}); - if (it == worldChunks.end()) continue; - Chunk* chunk = it->second; - - auto itNM = worldChunks.find({cx-1, cz}); - Chunk* nxM = (itNM != worldChunks.end()) ? itNM->second : nullptr; - auto itNP = worldChunks.find({cx+1, cz}); - Chunk* nxP = (itNP != worldChunks.end()) ? itNP->second : nullptr; - auto itZM = worldChunks.find({cx, cz-1}); - Chunk* nzM = (itZM != worldChunks.end()) ? itZM->second : nullptr; - auto itZP = worldChunks.find({cx, cz+1}); - Chunk* nzP = (itZP != worldChunks.end()) ? itZP->second : nullptr; - - int worldX = cx * CHUNK_SIZE; - int worldZ = cz * CHUNK_SIZE; - for (int lx = 0; lx < CHUNK_SIZE; lx++) { - for (int ly = 0; ly <= chunk->maxY; ly++) { - for (int lz = 0; lz < CHUNK_SIZE; lz++) { - if (chunk->blocks[lx][ly][lz] != CRAFTING_TABLE) continue; - if (!IsExposedOptimized(lx, ly, lz, chunk, nxM, nxP, nzM, nzP)) continue; - DrawCraftingTable((Vector3){(float)(worldX+lx), (float)ly, (float)(worldZ+lz)}, - craftingSideTexture.id, craftingTopTexture.id, blockTextures[DIRT].id, blockTint); - } - } - } + for (Chunk* chunk : visibleChunks) { + for (auto& pos : chunk->renderLists[CRAFTING_TABLE]) { + DrawCraftingTable(pos, craftingSideTexture.id, craftingTopTexture.id, blockTextures[DIRT].id, blockTint); } } } else { - unsigned int currentTexId = blockTextures[renderType].id; - rlSetTexture(currentTexId); + rlSetTexture(blockTextures[renderType].id); rlBegin(RL_QUADS); rlColor4ub(blockTint.r, blockTint.g, blockTint.b, 255); if (renderType == LEAVES) { - // Combined green and day/night tint rlColor4ub((unsigned char)(blockTint.r * 0.2f), (unsigned char)(blockTint.g * 0.6f), (unsigned char)(blockTint.b * 0.2f), 255); } - for (int cx = playerCX - RENDER_DISTANCE; cx <= playerCX + RENDER_DISTANCE; cx++) { - for (int cz = playerCZ - RENDER_DISTANCE; cz <= playerCZ + RENDER_DISTANCE; cz++) { - // FRUSTUM CULLING - Vector3 chunkCenter = { (float)(cx * CHUNK_SIZE + 8), 32.0f, (float)(cz * CHUNK_SIZE + 8) }; - Vector3 toChunk = Vector3Normalize(Vector3Subtract(chunkCenter, camera3D.position)); - if (Vector3Length(Vector3Subtract(chunkCenter, camera3D.position)) > 24.0f && - Vector3DotProduct(toChunk, camForward) < 0.4f) continue; - - auto it = worldChunks.find({cx, cz}); - if (it == worldChunks.end()) continue; - Chunk* chunk = it->second; - - auto itNM = worldChunks.find({cx-1, cz}); - Chunk* nxM = (itNM != worldChunks.end()) ? itNM->second : nullptr; - auto itNP = worldChunks.find({cx+1, cz}); - Chunk* nxP = (itNP != worldChunks.end()) ? itNP->second : nullptr; - auto itZM = worldChunks.find({cx, cz-1}); - Chunk* nzM = (itZM != worldChunks.end()) ? itZM->second : nullptr; - auto itZP = worldChunks.find({cx, cz+1}); - Chunk* nzP = (itZP != worldChunks.end()) ? itZP->second : nullptr; - - int worldX = cx * CHUNK_SIZE; - int worldZ = cz * CHUNK_SIZE; - for (int lx = 0; lx < CHUNK_SIZE; lx++) { - for (int ly = 0; ly <= chunk->maxY; ly++) { - for (int lz = 0; lz < CHUNK_SIZE; lz++) { - if (chunk->blocks[lx][ly][lz] != renderType) continue; - if (!IsExposedOptimized(lx, ly, lz, chunk, nxM, nxP, nzM, nzP)) continue; - DrawCubeVertices((float)(worldX+lx), (float)ly, (float)(worldZ+lz), 1.0f, 1.0f, 1.0f); - } - } - } + for (Chunk* chunk : visibleChunks) { + for (auto& pos : chunk->renderLists[renderType]) { + DrawCubeVertices(pos.x, pos.y, pos.z, 1.0f, 1.0f, 1.0f); } } rlEnd();