From a3f7f7d03cff7a8b10573e33f0de43cad79ea9e8 Mon Sep 17 00:00:00 2001 From: MatthewBeshay <92357869+MatthewBeshay@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:52:55 +1100 Subject: [PATCH] perf(render): improve chunk scheduling and refine rebuild profiling --- Minecraft.Client/Rendering/Chunk.cpp | 176 +++++++++--------- .../EntityRenderers/TileRenderer.cpp | 27 ++- Minecraft.Client/Rendering/LevelRenderer.cpp | 80 ++++---- Minecraft.Client/Utils/FrameProfiler.cpp | 5 + Minecraft.Client/Utils/FrameProfiler.h | 5 + 5 files changed, 171 insertions(+), 122 deletions(-) diff --git a/Minecraft.Client/Rendering/Chunk.cpp b/Minecraft.Client/Rendering/Chunk.cpp index 4df99432b..7980f241e 100644 --- a/Minecraft.Client/Rendering/Chunk.cpp +++ b/Minecraft.Client/Rendering/Chunk.cpp @@ -7,6 +7,7 @@ #include "../../Minecraft.World/Headers/net.minecraft.world.level.tile.h" #include "../../Minecraft.World/Headers/net.minecraft.world.level.tile.entity.h" #include "LevelRenderer.h" +#include "../Utils/FrameProfiler.h" #ifdef __PS3__ #include "../Platform/PS3/SPU_Tasks/ChunkUpdate/ChunkRebuildData.h" @@ -252,94 +253,97 @@ void Chunk::rebuild() { // into this category. By far the largest category of these are tiles in // solid regions of rock. bool empty = true; - for (int yy = y0; yy < y1; yy++) { - for (int zz = 0; zz < 16; zz++) { - for (int xx = 0; xx < 16; xx++) { - // 4J Stu - tile data is ordered in 128 blocks of full width, - // lower 128 then upper 128 - int indexY = yy; - int offset = 0; - if (indexY >= Level::COMPRESSED_CHUNK_SECTION_HEIGHT) { - indexY -= Level::COMPRESSED_CHUNK_SECTION_HEIGHT; - offset = Level::COMPRESSED_CHUNK_SECTION_TILES; - } - - unsigned char tileId = - tileIds[offset + (((xx + 0) << 11) | ((zz + 0) << 7) | - (indexY + 0))]; - if (tileId > 0) empty = false; - - // Don't bother trying to work out neighbours for this tile if - // we are at the edge of the chunk - apart from the very bottom - // of the world where we shouldn't ever be able to see - if (yy == (Level::maxBuildHeight - 1)) continue; - if ((xx == 0) || (xx == 15)) continue; - if ((zz == 0) || (zz == 15)) continue; - - // Establish whether this tile and its neighbours are all made - // of rock, dirt, unbreakable tiles, or have already been - // determined to meet this criteria themselves and have a tile - // of 255 set. - if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || - (tileId == Tile::unbreakable_Id) || (tileId == 255))) - continue; - tileId = tileIds[offset + (((xx - 1) << 11) | ((zz + 0) << 7) | - (indexY + 0))]; - if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || - (tileId == Tile::unbreakable_Id) || (tileId == 255))) - continue; - tileId = tileIds[offset + (((xx + 1) << 11) | ((zz + 0) << 7) | - (indexY + 0))]; - if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || - (tileId == Tile::unbreakable_Id) || (tileId == 255))) - continue; - tileId = tileIds[offset + (((xx + 0) << 11) | ((zz - 1) << 7) | - (indexY + 0))]; - if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || - (tileId == Tile::unbreakable_Id) || (tileId == 255))) - continue; - tileId = tileIds[offset + (((xx + 0) << 11) | ((zz + 1) << 7) | - (indexY + 0))]; - if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || - (tileId == Tile::unbreakable_Id) || (tileId == 255))) - continue; - // Treat the bottom of the world differently - we shouldn't ever - // be able to look up at this, so consider tiles as invisible if - // they are surrounded on sides other than the bottom - if (yy > 0) { - int indexYMinusOne = yy - 1; - int yMinusOneOffset = 0; - if (indexYMinusOne >= - Level::COMPRESSED_CHUNK_SECTION_HEIGHT) { - indexYMinusOne -= - Level::COMPRESSED_CHUNK_SECTION_HEIGHT; - yMinusOneOffset = Level::COMPRESSED_CHUNK_SECTION_TILES; + { + FRAME_PROFILE_SCOPE(ChunkPrepass); + for (int yy = y0; yy < y1; yy++) { + for (int zz = 0; zz < 16; zz++) { + for (int xx = 0; xx < 16; xx++) { + // 4J Stu - tile data is ordered in 128 blocks of full width, + // lower 128 then upper 128 + int indexY = yy; + int offset = 0; + if (indexY >= Level::COMPRESSED_CHUNK_SECTION_HEIGHT) { + indexY -= Level::COMPRESSED_CHUNK_SECTION_HEIGHT; + offset = Level::COMPRESSED_CHUNK_SECTION_TILES; } - tileId = tileIds[yMinusOneOffset + (((xx + 0) << 11) | - ((zz + 0) << 7) | - indexYMinusOne)]; - if (!((tileId == Tile::stone_Id) || - (tileId == Tile::dirt_Id) || + + unsigned char tileId = + tileIds[offset + (((xx + 0) << 11) | ((zz + 0) << 7) | + (indexY + 0))]; + if (tileId > 0) empty = false; + + // Don't bother trying to work out neighbours for this tile if + // we are at the edge of the chunk - apart from the very bottom + // of the world where we shouldn't ever be able to see + if (yy == (Level::maxBuildHeight - 1)) continue; + if ((xx == 0) || (xx == 15)) continue; + if ((zz == 0) || (zz == 15)) continue; + + // Establish whether this tile and its neighbours are all made + // of rock, dirt, unbreakable tiles, or have already been + // determined to meet this criteria themselves and have a tile + // of 255 set. + if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || + (tileId == Tile::unbreakable_Id) || (tileId == 255))) + continue; + tileId = tileIds[offset + (((xx - 1) << 11) | ((zz + 0) << 7) | + (indexY + 0))]; + if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || + (tileId == Tile::unbreakable_Id) || (tileId == 255))) + continue; + tileId = tileIds[offset + (((xx + 1) << 11) | ((zz + 0) << 7) | + (indexY + 0))]; + if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || + (tileId == Tile::unbreakable_Id) || (tileId == 255))) + continue; + tileId = tileIds[offset + (((xx + 0) << 11) | ((zz - 1) << 7) | + (indexY + 0))]; + if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || + (tileId == Tile::unbreakable_Id) || (tileId == 255))) + continue; + tileId = tileIds[offset + (((xx + 0) << 11) | ((zz + 1) << 7) | + (indexY + 0))]; + if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || + (tileId == Tile::unbreakable_Id) || (tileId == 255))) + continue; + // Treat the bottom of the world differently - we shouldn't ever + // be able to look up at this, so consider tiles as invisible if + // they are surrounded on sides other than the bottom + if (yy > 0) { + int indexYMinusOne = yy - 1; + int yMinusOneOffset = 0; + if (indexYMinusOne >= + Level::COMPRESSED_CHUNK_SECTION_HEIGHT) { + indexYMinusOne -= + Level::COMPRESSED_CHUNK_SECTION_HEIGHT; + yMinusOneOffset = Level::COMPRESSED_CHUNK_SECTION_TILES; + } + tileId = tileIds[yMinusOneOffset + (((xx + 0) << 11) | + ((zz + 0) << 7) | + indexYMinusOne)]; + if (!((tileId == Tile::stone_Id) || + (tileId == Tile::dirt_Id) || + (tileId == Tile::unbreakable_Id) || (tileId == 255))) + continue; + } + int indexYPlusOne = yy + 1; + int yPlusOneOffset = 0; + if (indexYPlusOne >= Level::COMPRESSED_CHUNK_SECTION_HEIGHT) { + indexYPlusOne -= Level::COMPRESSED_CHUNK_SECTION_HEIGHT; + yPlusOneOffset = Level::COMPRESSED_CHUNK_SECTION_TILES; + } + tileId = + tileIds[yPlusOneOffset + (((xx + 0) << 11) | + ((zz + 0) << 7) | indexYPlusOne)]; + if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || (tileId == Tile::unbreakable_Id) || (tileId == 255))) continue; - } - int indexYPlusOne = yy + 1; - int yPlusOneOffset = 0; - if (indexYPlusOne >= Level::COMPRESSED_CHUNK_SECTION_HEIGHT) { - indexYPlusOne -= Level::COMPRESSED_CHUNK_SECTION_HEIGHT; - yPlusOneOffset = Level::COMPRESSED_CHUNK_SECTION_TILES; - } - tileId = - tileIds[yPlusOneOffset + (((xx + 0) << 11) | - ((zz + 0) << 7) | indexYPlusOne)]; - if (!((tileId == Tile::stone_Id) || (tileId == Tile::dirt_Id) || - (tileId == Tile::unbreakable_Id) || (tileId == 255))) - continue; - // This tile is surrounded. Flag it as not requiring to be - // rendered by setting its id to 255. - tileIds[offset + (((xx + 0) << 11) | ((zz + 0) << 7) | - (indexY + 0))] = 0xff; + // This tile is surrounded. Flag it as not requiring to be + // rendered by setting its id to 255. + tileIds[offset + (((xx + 0) << 11) | ((zz + 0) << 7) | + (indexY + 0))] = 0xff; + } } } } @@ -752,7 +756,7 @@ void Chunk::rebuild_SPU() { if (currentLayer == 0 && Tile::tiles[tileId]->isEntityTile()) { std::shared_ptr et = - region.getTileEntity(x, y, z); + region->getTileEntity(x, y, z); if (TileEntityRenderDispatcher::instance ->hasRenderer(et)) { renderableTileEntities.push_back(et); @@ -770,7 +774,7 @@ void Chunk::rebuild_SPU() { } else if (renderLayer == currentLayer) { // if(currentLayer == 0) // numRenderedLayer0++; - rendered |= tileRenderer.tesselateInWorld( + rendered |= tileRenderer->tesselateInWorld( tile, x, y, z); } } diff --git a/Minecraft.Client/Rendering/EntityRenderers/TileRenderer.cpp b/Minecraft.Client/Rendering/EntityRenderers/TileRenderer.cpp index 3ee4ff71a..f1f91018c 100644 --- a/Minecraft.Client/Rendering/EntityRenderers/TileRenderer.cpp +++ b/Minecraft.Client/Rendering/EntityRenderers/TileRenderer.cpp @@ -11,6 +11,7 @@ #include "../../../Minecraft.World/Headers/net.minecraft.h" #include "../../../Minecraft.World/Headers/net.minecraft.world.h" #include "../Tesselator.h" +#include "../../Utils/FrameProfiler.h" #include "EntityTileRenderer.h" #include "../../GameState/Options.h" @@ -267,7 +268,12 @@ bool TileRenderer::tesselateInWorld( { Tesselator* t = Tesselator::getInstance(); int shape = tt->getRenderShape(); - tt->updateShape(level, x, y, z, forceData, forceEntity); + if (shape == Tile::SHAPE_BLOCK) { + FRAME_PROFILE_SCOPE(ChunkBlockShape); + tt->updateShape(level, x, y, z, forceData, forceEntity); + } else { + tt->updateShape(level, x, y, z, forceData, forceEntity); + } // AP - now that the culling is done earlier we don't need to call setShape // until later on (only for SHAPE_BLOCK) if (shape != Tile::SHAPE_BLOCK) { @@ -278,6 +284,11 @@ bool TileRenderer::tesselateInWorld( bool retVal = false; switch (shape) { case Tile::SHAPE_BLOCK: { + { + FRAME_PROFILE_SCOPE(ChunkBlockShape); + setShape(tt); + } + // 4J - added these faceFlags so we can detect whether this block is // going to have no visible faces and early out the original code // checked noCulling and shouldRenderFace directly where faceFlags @@ -290,6 +301,7 @@ bool TileRenderer::tesselateInWorld( if (noCulling) { faceFlags = 0x3f; } else { + FRAME_PROFILE_SCOPE(ChunkBlockFaceCull); // these block types can take advantage of a faster version of // shouldRenderFace there are others but this is an easy check // which covers the majority Note: This now covers rock, grass, @@ -319,9 +331,6 @@ bool TileRenderer::tesselateInWorld( break; } - // now we need to set the shape - setShape(tt); - retVal = tesselateBlockInWorld(tt, x, y, z, faceFlags); } break; case Tile::SHAPE_TREE: @@ -4828,9 +4837,11 @@ bool TileRenderer::tesselateBlockInWorld(Tile* tt, int x, int y, int z) { if (Tile::lightEmission[tt->id] == 0) // 4J - TODO/remove (Minecraft::useAmbientOcclusion()) { + FRAME_PROFILE_SCOPE(ChunkBlockLighting); return tesselateBlockInWorldWithAmbienceOcclusionTexLighting( tt, x, y, z, r, g, b, 0, smoothShapeLighting); } else { + FRAME_PROFILE_SCOPE(ChunkBlockLighting); return tesselateBlockInWorld(tt, x, y, z, r, g, b); } } @@ -4856,9 +4867,11 @@ bool TileRenderer::tesselateBlockInWorld(Tile* tt, int x, int y, int z, if (Tile::lightEmission[tt->id] == 0) // 4J - TODO/remove (Minecraft::useAmbientOcclusion()) { + FRAME_PROFILE_SCOPE(ChunkBlockLighting); return tesselateBlockInWorldWithAmbienceOcclusionTexLighting( tt, x, y, z, r, g, b, faceFlags, smoothShapeLighting); } else { + FRAME_PROFILE_SCOPE(ChunkBlockLighting); return tesselateBlockInWorld(tt, x, y, z, r, g, b); } } @@ -7049,6 +7062,7 @@ bool TileRenderer::tesselateDoorInWorld(Tile* tt, int x, int y, int z) { void TileRenderer::renderFaceDown(Tile* tt, double x, double y, double z, Icon* tex) { + FRAME_PROFILE_SCOPE(ChunkBlockEmit); Tesselator* t = Tesselator::getInstance(); if (hasFixedTexture()) tex = fixedTexture; @@ -7167,6 +7181,7 @@ void TileRenderer::renderFaceDown(Tile* tt, double x, double y, double z, void TileRenderer::renderFaceUp(Tile* tt, double x, double y, double z, Icon* tex) { + FRAME_PROFILE_SCOPE(ChunkBlockEmit); Tesselator* t = Tesselator::getInstance(); if (hasFixedTexture()) tex = fixedTexture; @@ -7286,6 +7301,7 @@ void TileRenderer::renderFaceUp(Tile* tt, double x, double y, double z, void TileRenderer::renderNorth(Tile* tt, double x, double y, double z, Icon* tex) { + FRAME_PROFILE_SCOPE(ChunkBlockEmit); Tesselator* t = Tesselator::getInstance(); if (hasFixedTexture()) tex = fixedTexture; @@ -7410,6 +7426,7 @@ void TileRenderer::renderNorth(Tile* tt, double x, double y, double z, void TileRenderer::renderSouth(Tile* tt, double x, double y, double z, Icon* tex) { + FRAME_PROFILE_SCOPE(ChunkBlockEmit); Tesselator* t = Tesselator::getInstance(); if (hasFixedTexture()) tex = fixedTexture; @@ -7534,6 +7551,7 @@ void TileRenderer::renderSouth(Tile* tt, double x, double y, double z, void TileRenderer::renderWest(Tile* tt, double x, double y, double z, Icon* tex) { + FRAME_PROFILE_SCOPE(ChunkBlockEmit); Tesselator* t = Tesselator::getInstance(); if (hasFixedTexture()) tex = fixedTexture; @@ -7658,6 +7676,7 @@ void TileRenderer::renderWest(Tile* tt, double x, double y, double z, void TileRenderer::renderEast(Tile* tt, double x, double y, double z, Icon* tex) { + FRAME_PROFILE_SCOPE(ChunkBlockEmit); Tesselator* t = Tesselator::getInstance(); if (hasFixedTexture()) tex = fixedTexture; diff --git a/Minecraft.Client/Rendering/LevelRenderer.cpp b/Minecraft.Client/Rendering/LevelRenderer.cpp index 3da404243..177b8e021 100644 --- a/Minecraft.Client/Rendering/LevelRenderer.cpp +++ b/Minecraft.Client/Rendering/LevelRenderer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "../Platform/stdafx.h" #include "LevelRenderer.h" @@ -904,11 +905,12 @@ int LevelRenderer::renderChunks(int from, int to, int layer, double alpha) { int list = chunk->globalIdx * 2 + layer; list += chunkLists; - // 4jcraft: replaced glPushMatrix/glTranslatef/glPopMatrix per chunk - // no more full MVP upload per chunk, can also be bkwards compat - RenderManager.SetChunkOffset((float)chunk->chunk->x, - (float)chunk->chunk->y, - (float)chunk->chunk->z); + // 4jcraft: replaced glPushMatrix/glTranslatef/glPopMatrix per + // chunk no more full MVP upload per chunk, can also be bkwards + // compat + RenderManager.SetChunkOffset((float)chunk->chunk->x, + (float)chunk->chunk->y, + (float)chunk->chunk->z); if (RenderManager.CBuffCall(list, first)) { first = false; @@ -1964,7 +1966,41 @@ void LevelRenderer::renderAdvancedClouds(float alpha) { bool LevelRenderer::updateDirtyChunks() { #ifdef _LARGE_WORLDS - std::list > nearestClipChunks; + struct NearestClipChunkSet { + std::array, MAX_CONCURRENT_CHUNK_REBUILDS> + items; + int count = 0; + + bool empty() const noexcept { return count == 0; } + int size() const noexcept { return count; } + + bool wouldAccept(int distSqWeighted) const noexcept { + return (count < MAX_CONCURRENT_CHUNK_REBUILDS) || + (distSqWeighted < items[count - 1].second); + } + + void insert(ClipChunk* chunk, int distSqWeighted) noexcept { + int pos = 0; + while ((pos < count) && (items[pos].second <= distSqWeighted)) { + ++pos; + } + + if ((count == MAX_CONCURRENT_CHUNK_REBUILDS) && + (pos >= MAX_CONCURRENT_CHUNK_REBUILDS)) { + return; + } + + const int newCount = + (count < MAX_CONCURRENT_CHUNK_REBUILDS) + ? (count + 1) + : MAX_CONCURRENT_CHUNK_REBUILDS; + for (int i = newCount - 1; i > pos; --i) { + items[i] = items[i - 1]; + } + items[pos] = std::pair(chunk, distSqWeighted); + count = newCount; + } + } nearestClipChunks; #endif ClipChunk* nearChunk = NULL; // Nearest chunk that is dirty @@ -2090,11 +2126,6 @@ bool LevelRenderer::updateDirtyChunks() { veryNearCount = g_findNearestChunkDataIn.veryNearCount; #else // __PS3__ -#ifdef _LARGE_WORLDS - int maxNearestChunks = MAX_CONCURRENT_CHUNK_REBUILDS; - // 4J Stu - On XboxOne we should cut this down if in a constrained state - // so the saving threads get more time -#endif // Find nearest chunk that is dirty for (int p = 0; p < XUSER_MAX_COUNT; p++) { // It's possible that the localplayers member can be set to NULL on @@ -2150,17 +2181,9 @@ bool LevelRenderer::updateDirtyChunks() { considered++; // Is this chunk nearer than our nearest? #ifdef _LARGE_WORLDS - bool isNearer = nearestClipChunks.empty(); - AUTO_VAR(itNearest, nearestClipChunks.begin()); - for (; itNearest != nearestClipChunks.end(); - ++itNearest) { - isNearer = - distSqWeighted < itNearest->second; - if (isNearer) break; - } - isNearer = - isNearer || (nearestClipChunks.size() < - maxNearestChunks); + bool isNearer = + nearestClipChunks.wouldAccept( + distSqWeighted); #else bool isNearer = distSqWeighted < minDistSq; #endif @@ -2203,13 +2226,7 @@ bool LevelRenderer::updateDirtyChunks() { minDistSq = distSqWeighted; #ifdef _LARGE_WORLDS nearestClipChunks.insert( - itNearest, - std::pair( - nearChunk, minDistSq)); - if (nearestClipChunks.size() > - maxNearestChunks) { - nearestClipChunks.pop_back(); - } + nearChunk, minDistSq); #endif } else { chunk->clearDirty(); @@ -2249,9 +2266,8 @@ bool LevelRenderer::updateDirtyChunks() { int index = 0; { FRAME_PROFILE_SCOPE(ChunkRebuildSchedule); - for (AUTO_VAR(it, nearestClipChunks.begin()); - it != nearestClipChunks.end(); ++it) { - chunk = it->first->chunk; + for (int i = 0; i < nearestClipChunks.size(); ++i) { + chunk = nearestClipChunks.items[i].first->chunk; // If this chunk is very near, then move the renderer into a // deferred mode. This won't commit any command buffers for // rendering until we call CBuffDeferredModeEnd(), allowing us to diff --git a/Minecraft.Client/Utils/FrameProfiler.cpp b/Minecraft.Client/Utils/FrameProfiler.cpp index d27889c44..89db484eb 100644 --- a/Minecraft.Client/Utils/FrameProfiler.cpp +++ b/Minecraft.Client/Utils/FrameProfiler.cpp @@ -39,6 +39,11 @@ constexpr std::array {Bucket::ChunkDirtyScan, "chunkDirtyScan"}, {Bucket::ChunkRebuildSchedule, "chunkRebuildSchedule"}, {Bucket::ChunkRebuildBody, "chunkRebuildBody"}, + {Bucket::ChunkPrepass, "chunkPrepass"}, + {Bucket::ChunkBlockShape, "chunkBlockShape"}, + {Bucket::ChunkBlockFaceCull, "chunkBlockFaceCull"}, + {Bucket::ChunkBlockLighting, "chunkBlockLighting"}, + {Bucket::ChunkBlockEmit, "chunkBlockEmit"}, {Bucket::Entity, "entities"}, {Bucket::Particle, "particles"}, {Bucket::WeatherSky, "weather"}, diff --git a/Minecraft.Client/Utils/FrameProfiler.h b/Minecraft.Client/Utils/FrameProfiler.h index 5fbb71b12..7abf56255 100644 --- a/Minecraft.Client/Utils/FrameProfiler.h +++ b/Minecraft.Client/Utils/FrameProfiler.h @@ -18,6 +18,11 @@ public: ChunkDirtyScan, ChunkRebuildSchedule, ChunkRebuildBody, + ChunkPrepass, + ChunkBlockShape, + ChunkBlockFaceCull, + ChunkBlockLighting, + ChunkBlockEmit, Entity, Particle, WeatherSky,