diff --git a/Minecraft.Client/Chunk.cpp b/Minecraft.Client/Chunk.cpp index 5ffab3168..4d0d3c1f3 100644 --- a/Minecraft.Client/Chunk.cpp +++ b/Minecraft.Client/Chunk.cpp @@ -22,24 +22,21 @@ int Chunk::updates = 0; #ifdef _LARGE_WORLDS -DWORD Chunk::tlsIdx = TlsAlloc(); +static thread_local unsigned char s_tlsTileIds[16 * 16 * Level::maxBuildHeight]; void Chunk::CreateNewThreadStorage() { - unsigned char *tileIds = new unsigned char[16 * 16 * Level::maxBuildHeight]; - TlsSetValue(tlsIdx, tileIds); + // No-op: thread_local handles per-thread allocation automatically } void Chunk::ReleaseThreadStorage() { - unsigned char *tileIds = static_cast(TlsGetValue(tlsIdx)); - delete tileIds; + // No-op: thread_local handles per-thread cleanup automatically } unsigned char *Chunk::GetTileIdsStorage() { - unsigned char *tileIds = static_cast(TlsGetValue(tlsIdx)); - return tileIds; + return s_tlsTileIds; } #else // 4J Stu - Don't want this when multi-threaded @@ -234,33 +231,50 @@ void Chunk::rebuild() static unsigned char tileIds[16 * 16 * Level::maxBuildHeight]; #endif byteArray tileArray = byteArray(tileIds, 16 * 16 * Level::maxBuildHeight); - level->getChunkAt(x,z)->getBlockData(tileArray); // 4J - TODO - now our data has been re-arranged, we could just extra the vertical slice of this chunk rather than the whole thing - - LevelSource *region = new Region(level, x0 - r, y0 - r, z0 - r, x1 + r, y1 + r, z1 + r, r); - TileRenderer *tileRenderer = new TileRenderer(region, this->x, this->y, this->z, tileIds); - - // AP - added a caching system for Chunk::rebuild to take advantage of - // Basically we're storing of copy of the tileIDs array inside the region so that calls to Region::getTile can grab data - // more quickly from this array rather than calling CompressedTileStorage. On the Vita the total thread time spent in - // Region::getTile went from 20% to 4%. -#ifdef __PSVITA__ - int xc = x >> 4; - int zc = z >> 4; - ((Region*)region)->setCachedTiles(tileIds, xc, zc); -#endif + LevelChunk *sourceChunk = level->getChunkAt(x,z); + if( sourceChunk == nullptr ) + { + // Level chunk not loaded yet - treat as empty + for (int currentLayer = 0; currentLayer < 2; currentLayer++) + { + levelRenderer->setGlobalChunkFlag(this->x, this->y, this->z, level, LevelRenderer::CHUNK_FLAG_EMPTY0, currentLayer); + RenderManager.CBuffClear(lists + currentLayer); + } + levelRenderer->setGlobalChunkFlag(this->x, this->y, this->z, level, LevelRenderer::CHUNK_FLAG_NOTSKYLIT); + levelRenderer->setGlobalChunkFlag(this->x, this->y, this->z, level, LevelRenderer::CHUNK_FLAG_COMPILED); + PIXEndNamedEvent(); + PIXEndNamedEvent(); + return; + } + sourceChunk->getBlockData(tileArray); // 4J - TODO - now our data has been re-arranged, we could just extra the vertical slice of this chunk rather than the whole thing // We now go through the vertical section of this level chunk that we are interested in and try and establish // (1) if it is completely empty // (2) if any of the tiles can be quickly determined to not need rendering because they are in the middle of other tiles and // so can't be seen. A large amount (> 60% in tests) of tiles that call tesselateInWorld in the unoptimised version // of this function fall into this category. By far the largest category of these are tiles in solid regions of rock. - // Build a lookup table for occluder tile IDs to replace repeated 4-way comparisons with a single table lookup per neighbor, eliminating ~24 branch comparisons per interior tile. - bool isOccluder[256]; - std::memset(isOccluder, 0, sizeof(isOccluder)); - isOccluder[Tile::stone_Id] = true; - isOccluder[Tile::dirt_Id] = true; - isOccluder[Tile::unbreakable_Id] = true; - isOccluder[255] = true; + // Build the occluder lookup from Tile::isSolidRender() so that ALL opaque full-cube blocks act as occluders, + // not just stone/dirt/bedrock. This catches sand, gravel, ores, cobblestone, sandstone, netherrack, wool, etc. + // which dramatically reduces tesselation work for underground/terrain-heavy chunks. + // + // thread_local: the table is built once per thread and reused across all subsequent rebuild() calls on that + // thread. Without it, a plain local would redo 255 virtual dispatches (isSolidRender) every single call - + // rebuild() runs up to MAX_CONCURRENT_CHUNK_REBUILDS times per updateDirtyChunks(), ~10 times per frame, + // so hundreds of redundant vtable lookups per second for data that never changes at runtime. A plain static + // would avoid that but introduces a data race: multiple rebuild threads run concurrently and the C++ static + // init guard (mutex) would serialize them on every entry just to check the "already initialized" flag. + // thread_local gives each thread its own copy with zero synchronization after the first call. + static thread_local auto isOccluder = [] { + std::array table{}; + for (int id = 1; id < 256; id++) + { + Tile *tile = Tile::tiles[id]; + if (tile != nullptr && tile->isSolidRender()) + table[id] = true; + } + table[255] = true; // already-marked-invisible sentinel + return table; + }(); bool empty = true; for( int yy = y0; yy < y1; yy++ ) @@ -330,14 +344,31 @@ void Chunk::rebuild() levelRenderer->setGlobalChunkFlag(this->x, this->y, this->z, level, LevelRenderer::CHUNK_FLAG_EMPTY0, currentLayer); RenderManager.CBuffClear(lists + currentLayer); } + levelRenderer->setGlobalChunkFlag(this->x, this->y, this->z, level, LevelRenderer::CHUNK_FLAG_NOTSKYLIT); + levelRenderer->setGlobalChunkFlag(this->x, this->y, this->z, level, LevelRenderer::CHUNK_FLAG_COMPILED); - delete region; - delete tileRenderer; + PIXEndNamedEvent(); // match "Rebuilding chunk" event return; } // 4J - optimisation ends //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Construct Region and TileRenderer only for non-empty chunks. + // This is deferred to here so that empty chunks (common after teleport) skip + // the Region construction (9 chunk lookups) and TileRenderer 128KB cache memset. + Region region(level, x0 - r, y0 - r, z0 - r, x1 + r, y1 + r, z1 + r, r); + TileRenderer tileRenderer(®ion, this->x, this->y, this->z, tileIds); + + // AP - added a caching system for Chunk::rebuild to take advantage of + // Basically we're storing of copy of the tileIDs array inside the region so that calls to Region::getTile can grab data + // more quickly from this array rather than calling CompressedTileStorage. On the Vita the total thread time spent in + // Region::getTile went from 20% to 4%. +#ifdef __PSVITA__ + int xc = x >> 4; + int zc = z >> 4; + region.setCachedTiles(tileIds, xc, zc); +#endif + PIXBeginNamedEvent(0,"Rebuild section C"); Tesselator::Bounds bounds; // 4J MGH - added { @@ -374,7 +405,7 @@ void Chunk::rebuild() } // 4J - get tile from those copied into our local array in earlier optimisation - unsigned char tileId = tileIds[ offset + ( ( ( x - x0 ) << 11 ) | ( ( z - z0 ) << 7 ) | indexY) ]; + const unsigned char tileId = tileIds[ offset + ( ( ( x - x0 ) << 11 ) | ( ( z - z0 ) << 7 ) | indexY) ]; // If flagged as not visible, drop out straight away if( tileId == 0xff ) [[unlikely]] continue; // int tileId = region->getTile(x,y,z); @@ -406,7 +437,7 @@ void Chunk::rebuild() Tile *tile = Tile::tiles[tileId]; if (currentLayer == 0 && tile->isEntityTile()) { - shared_ptr et = region->getTileEntity(x, y, z); + shared_ptr et = region.getTileEntity(x, y, z); if (TileEntityRenderDispatcher::instance->hasRenderer(et)) { renderableTileEntities.push_back(et); @@ -420,7 +451,7 @@ void Chunk::rebuild() } else if (renderLayer == currentLayer) { - rendered |= tileRenderer->tesselateInWorld(tile, x, y, z); + rendered |= tileRenderer.tesselateInWorld(tile, x, y, z); } } } @@ -480,9 +511,6 @@ void Chunk::rebuild() bounds.boundingBox[3], bounds.boundingBox[4], bounds.boundingBox[5]); } - delete tileRenderer; - delete region; - PIXEndNamedEvent(); PIXBeginNamedEvent(0,"Rebuild section D"); // 4J - have rewritten the way that tile entities are stored globally to make it work more easily with split screen. Chunks are now diff --git a/Minecraft.Client/Chunk.h b/Minecraft.Client/Chunk.h index e0ae016ef..379aa08ae 100644 --- a/Minecraft.Client/Chunk.h +++ b/Minecraft.Client/Chunk.h @@ -30,9 +30,8 @@ public: static LevelRenderer *levelRenderer; private: #ifndef _LARGE_WORLDS - static Tesselator *t; + static Tesselator *t; #else - static DWORD tlsIdx; public: static void CreateNewThreadStorage(); static void ReleaseThreadStorage(); diff --git a/Minecraft.Client/LevelRenderer.cpp b/Minecraft.Client/LevelRenderer.cpp index 5baf23daf..c8c04dfdb 100644 --- a/Minecraft.Client/LevelRenderer.cpp +++ b/Minecraft.Client/LevelRenderer.cpp @@ -1943,7 +1943,11 @@ bool LevelRenderer::updateDirtyChunks() 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 + // Find nearest chunk that is dirty. + // Rewritten for performance: linear scan of clip chunks (cache-friendly), with dirty flag + // checked BEFORE computing distances. The old triple-nested x/z/y loop computed distances + // for every chunk (~20K) regardless of dirty state. Now only dirty chunks (typically <1%) + // pay the cost of distance computation and nearest-selection. for( int p = 0; p < XUSER_MAX_COUNT; p++ ) { // It's possible that the localplayers member can be set to nullptr on the main thread when a player chooses to exit the game @@ -1957,96 +1961,91 @@ bool LevelRenderer::updateDirtyChunks() int py = static_cast(player->y); int pz = static_cast(player->z); - // app.DebugPrintf("!! %d %d %d, %d %d %d {%d,%d} ",px,py,pz,stackChunkDirty,nonStackChunkDirty,onlyRebuild, xChunks, zChunks); - - int considered = 0; - int wouldBeNearButEmpty = 0; - for( int x = 0; x < xChunks; x++ ) + int numClipChunks = static_cast(chunks[p].length); + ClipChunk *pClipChunk = chunks[p].data; + for( int i = 0; i < numClipChunks; i++, pClipChunk++ ) { - for( int z = 0; z < zChunks; z++ ) + // Fast reject: skip non-dirty chunks immediately before any distance work. + // globalIdx can be -1 for unassigned chunks. + const int gIdx = pClipChunk->globalIdx; + if (gIdx < 0) + continue; + const unsigned char flags = globalChunkFlags[gIdx]; + if (!(flags & CHUNK_FLAG_DIRTY)) + continue; + + // Batch-clear empty chunks upfront. After teleport, thousands of sky/void chunks + // are dirty. Clearing them all in one scan (rather than ~8 per call) dramatically + // reduces the dirty set for subsequent iterations. + Chunk *chunk = pClipChunk->chunk; + if (chunk == nullptr) + continue; + const int ySlice = (pClipChunk->ym - (CHUNK_SIZE / 2)) / CHUNK_SIZE; + LevelChunk *lc = level[p]->getChunkAt(chunk->x, chunk->z); + if (lc == nullptr || lc->isRenderChunkEmpty(ySlice * 16)) { - for( int y = 0; y < CHUNK_Y_COUNT; y++ ) + chunk->clearDirty(); + globalChunkFlags[gIdx] |= CHUNK_FLAG_EMPTYBOTH; + continue; + } + + // Non-empty dirty chunk — now compute distance + const int xd = pClipChunk->xm - px; + const int yd = pClipChunk->ym - py; + const int zd = pClipChunk->zm - pz; + const int distSq = xd * xd + yd * yd + zd * zd; + const int distSqWeighted = distSq + 3 * yd * yd; // Extra y weighting to prioritise things in same x/z plane as player first + + if( (!onlyRebuild) || + (flags & CHUNK_FLAG_COMPILED) || + ( distSq < 96 * 96 ) ) // Always rebuild really near things or else building (say) at tower up into empty blocks when we are low on memory will not create render data + { + // Is this chunk nearer than our nearest? +#ifdef _LARGE_WORLDS + bool isNearer = nearestClipChunks.empty(); + auto itNearest = nearestClipChunks.begin(); + for(; itNearest != nearestClipChunks.end(); ++itNearest) { - ClipChunk *pClipChunk = &chunks[p][(z * yChunks + y) * xChunks + x]; - // Get distance to this chunk - deliberately not calling the chunk's method of doing this to avoid overheads (passing entitie, type conversion etc.) that this involves - int xd = pClipChunk->xm - px; - int yd = pClipChunk->ym - py; - int zd = pClipChunk->zm - pz; - int distSq = xd * xd + yd * yd + zd * zd; - int distSqWeighted = xd * xd + yd * yd * 4 + zd * zd; // Weighting against y to prioritise things in same x/z plane as player first + isNearer = distSqWeighted < itNearest->second; + if(isNearer) break; + } + isNearer = isNearer || (nearestClipChunks.size() < maxNearestChunks); +#else + bool isNearer = distSqWeighted < minDistSq; +#endif - if( globalChunkFlags[ pClipChunk->globalIdx ] & CHUNK_FLAG_DIRTY ) +#ifdef _CRITICAL_CHUNKS + // AP - this will make sure that if a deferred grouping has started, only critical chunks go into that + // grouping, even if a non-critical chunk is closer. + if( (!veryNearCount && isNearer) || + (distSq < 20 * 20 && (flags & CHUNK_FLAG_CRITICAL)) ) +#else + if( isNearer ) +#endif + { + // Non-empty (already confirmed above), add to nearest set + nearChunk = pClipChunk; + minDistSq = distSqWeighted; +#ifdef _LARGE_WORLDS + nearestClipChunks.insert(itNearest, std::make_pair(nearChunk, minDistSq) ); + if(nearestClipChunks.size() > maxNearestChunks) { - if( (!onlyRebuild) || - globalChunkFlags[ pClipChunk->globalIdx ] & CHUNK_FLAG_COMPILED || - ( distSq < 96 * 96 ) ) // Always rebuild really near things or else building (say) at tower up into empty blocks when we are low on memory will not create render data - { // distSq adjusted from 20 * 20 to 96 * 96 - updated by detectiveren - considered++; - // Is this chunk nearer than our nearest? -#ifdef _LARGE_WORLDS - bool isNearer = nearestClipChunks.empty(); - auto itNearest = nearestClipChunks.begin(); - for(; itNearest != nearestClipChunks.end(); ++itNearest) - { - isNearer = distSqWeighted < itNearest->second; - if(isNearer) break; - } - isNearer = isNearer || (nearestClipChunks.size() < maxNearestChunks); -#else - bool isNearer = distSqWeighted < minDistSq; -#endif - -#ifdef _CRITICAL_CHUNKS - // AP - this will make sure that if a deferred grouping has started, only critical chunks go into that - // grouping, even if a non-critical chunk is closer. - if( (!veryNearCount && isNearer) || - (distSq < 20 * 20 && (globalChunkFlags[ pClipChunk->globalIdx ] & CHUNK_FLAG_CRITICAL)) ) -#else - if( isNearer ) -#endif - { - // At this point we've got a chunk that we would like to consider for rendering, at least based on its proximity to the player(s). - // Its *quite* quick to generate empty render data for render chunks, but if we let the rebuilding do that then the after rebuilding we will have - // to start searching for the next nearest chunk from scratch again. Instead, its better to detect empty chunks at this stage, flag them up as not dirty - // (and empty), and carry on. The levelchunk's isRenderChunkEmpty method can be quite optimal as it can make use of the chunk's data compression to detect - // emptiness without actually testing as many data items as uncompressed data would. - Chunk *chunk = pClipChunk->chunk; - LevelChunk *lc = level[p]->getChunkAt(chunk->x,chunk->z); - if( !lc->isRenderChunkEmpty(y * 16) ) - { - nearChunk = pClipChunk; - minDistSq = distSqWeighted; -#ifdef _LARGE_WORLDS - nearestClipChunks.insert(itNearest, std::make_pair(nearChunk, minDistSq) ); - if(nearestClipChunks.size() > maxNearestChunks) - { - nearestClipChunks.pop_back(); - } -#endif - } - else - { - chunk->clearDirty(); - globalChunkFlags[ pClipChunk->globalIdx ] |= CHUNK_FLAG_EMPTYBOTH; - wouldBeNearButEmpty++; - } - } - -#ifdef _CRITICAL_CHUNKS - // AP - is the chunk near and also critical - if( distSq < 20 * 20 && ((globalChunkFlags[ pClipChunk->globalIdx ] & CHUNK_FLAG_CRITICAL)) ) -#else - if( distSq < 20 * 20 ) -#endif - { - veryNearCount++; - } - } + nearestClipChunks.pop_back(); } +#endif + } + +#ifdef _CRITICAL_CHUNKS + // AP - is the chunk near and also critical + if( distSq < 20 * 20 && (flags & CHUNK_FLAG_CRITICAL) ) +#else + if( distSq < 20 * 20 ) +#endif + { + veryNearCount++; } } } - // app.DebugPrintf("[%d,%d,%d]\n",nearestClipChunks.empty(),considered,wouldBeNearButEmpty); } #endif // __PS3__ PIXEndNamedEvent(); @@ -2193,7 +2192,11 @@ bool LevelRenderer::updateDirtyChunks() return true; } - if( nearChunk ) destroyedTileManager->updatedChunkAt(chunk->level, chunk->x, chunk->y, chunk->z, veryNearCount ); + if( nearChunk ) + { + destroyedTileManager->updatedChunkAt(chunk->level, chunk->x, chunk->y, chunk->z, veryNearCount ); + return dirtyChunkPresent; + } return false; } diff --git a/Minecraft.Client/TileRenderer.cpp b/Minecraft.Client/TileRenderer.cpp index b77729ab5..ab9783288 100644 --- a/Minecraft.Client/TileRenderer.cpp +++ b/Minecraft.Client/TileRenderer.cpp @@ -142,6 +142,9 @@ int TileRenderer::getLightColor( Tile *tt, LevelSource *level, int x, int y, int return tt->getLightColor(level, x, y, z); } +static constexpr int TILE_RENDERER_CACHE_SIZE = 32 * 32 * 32; +static thread_local unsigned int s_tlsCache[TILE_RENDERER_CACHE_SIZE]; + TileRenderer::TileRenderer( LevelSource* level, int xMin, int yMin, int zMin, unsigned char *tileIds ) { this->level = level; @@ -153,14 +156,11 @@ TileRenderer::TileRenderer( LevelSource* level, int xMin, int yMin, int zMin, un this->yMin2 = yMin-2; this->zMin2 = zMin-2; this->tileIds = tileIds; - cache = new unsigned int[32*32*32]; - XMemSet(cache,0,32*32*32*sizeof(unsigned int)); + cache = s_tlsCache; + std::memset(cache, 0, TILE_RENDERER_CACHE_SIZE * sizeof(unsigned int)); } -TileRenderer::~TileRenderer() -{ - delete cache; -} +TileRenderer::~TileRenderer() = default; TileRenderer::TileRenderer( LevelSource* level ) { diff --git a/Minecraft.Client/stdafx.h b/Minecraft.Client/stdafx.h index 04a18b13d..30e8c7f9d 100644 --- a/Minecraft.Client/stdafx.h +++ b/Minecraft.Client/stdafx.h @@ -132,6 +132,7 @@ typedef XUID GameSessionUID; #include #include #include +#include #include #include #include diff --git a/Minecraft.World/Region.cpp b/Minecraft.World/Region.cpp index 1e8acaf36..93b36ad55 100644 --- a/Minecraft.World/Region.cpp +++ b/Minecraft.World/Region.cpp @@ -12,14 +12,7 @@ Region::~Region() { - for(unsigned int i = 0; i < chunks->length; ++i) - { - LevelChunkArray *lca = (*chunks)[i]; - delete [] lca->data; - delete lca; - } - delete [] chunks->data; - delete chunks; + // flatChunksHeap automatically freed by unique_ptr // AP - added a caching system for Chunk::rebuild to take advantage of if( CachedTiles ) @@ -37,7 +30,21 @@ Region::Region(Level *level, int x1, int y1, int z1, int x2, int y2, int z2, int int xc2 = (x2 + r) >> 4; int zc2 = (z2 + r) >> 4; - chunks = new LevelChunk2DArray(xc2 - xc1 + 1, zc2 - zc1 + 1); + chunksDimX = xc2 - xc1 + 1; + chunksDimZ = zc2 - zc1 + 1; + int totalChunks = chunksDimX * chunksDimZ; + + // Use stack buffer for common small regions (render chunks), heap for rare large ones (pathfinding) + if( totalChunks <= MAX_STACK_CHUNKS ) + { + flatChunks = flatChunks_stack; + } + else + { + flatChunksHeap = std::make_unique(totalChunks); + flatChunks = flatChunksHeap.get(); + } + std::fill_n(flatChunks, totalChunks, nullptr); allEmpty = true; for (int xc = xc1; xc <= xc2; xc++) @@ -47,8 +54,7 @@ Region::Region(Level *level, int x1, int y1, int z1, int x2, int y2, int z2, int LevelChunk *chunk = level->getChunk(xc, zc); if(chunk != nullptr) { - LevelChunkArray *lca = (*chunks)[xc - xc1]; - lca->data[zc - zc1] = chunk; + flatChunks[(xc - xc1) * chunksDimZ + (zc - zc1)] = chunk; } } } @@ -56,8 +62,7 @@ Region::Region(Level *level, int x1, int y1, int z1, int x2, int y2, int z2, int { for (int zc = (z1 >> 4); zc <= (z2 >> 4); zc++) { - LevelChunkArray *lca = (*chunks)[xc - xc1]; - LevelChunk *chunk = lca->data[zc - zc1]; + LevelChunk *chunk = flatChunks[(xc - xc1) * chunksDimZ + (zc - zc1)]; if (chunk != nullptr) { if (!chunk->isYSpaceEmpty(y1, y2)) @@ -105,12 +110,12 @@ int Region::getTile(int x, int y, int z) xc -= xc1; zc -= zc1; - if (xc < 0 || xc >= static_cast(chunks->length) || zc < 0 || zc >= static_cast((*chunks)[xc]->length)) + if (xc < 0 || xc >= chunksDimX || zc < 0 || zc >= chunksDimZ) { return 0; } - LevelChunk *lc = (*chunks)[xc]->data[zc]; + LevelChunk *lc = flatChunks[xc * chunksDimZ + zc]; if (lc == nullptr) return 0; return lc->getTile(x & 15, y, z & 15); @@ -137,12 +142,12 @@ LevelChunk* Region::getLevelChunk(int x, int y, int z) int xc = (x >> 4) - xc1; int zc = (z >> 4) - zc1; - if (xc < 0 || xc >= static_cast(chunks->length) || zc < 0 || zc >= static_cast((*chunks)[xc]->length)) + if (xc < 0 || xc >= chunksDimX || zc < 0 || zc >= chunksDimZ) { return nullptr; } - LevelChunk *lc = (*chunks)[xc]->data[zc]; + LevelChunk *lc = flatChunks[xc * chunksDimZ + zc]; return lc; } @@ -153,7 +158,15 @@ shared_ptr Region::getTileEntity(int x, int y, int z) int xc = (x >> 4) - xc1; int zc = (z >> 4) - zc1; - return (*chunks)[xc]->data[zc]->getTileEntity(x & 15, y, z & 15); + if (xc < 0 || xc >= chunksDimX || zc < 0 || zc >= chunksDimZ) + { + return nullptr; + } + + LevelChunk *lc = flatChunks[xc * chunksDimZ + zc]; + if (lc == nullptr) return nullptr; + + return lc->getTileEntity(x & 15, y, z & 15); } int Region::getLightColor(int x, int y, int z, int emitt, int tileId/*=-1*/) @@ -228,7 +241,15 @@ int Region::getRawBrightness(int x, int y, int z, bool propagate) int xc = (x >> 4) - xc1; int zc = (z >> 4) - zc1; - return (*chunks)[xc]->data[zc]->getRawBrightness(x & 15, y, z & 15, level->skyDarken); + if (xc < 0 || xc >= chunksDimX || zc < 0 || zc >= chunksDimZ) + { + return 0; + } + + LevelChunk *lc = flatChunks[xc * chunksDimZ + zc]; + if (lc == nullptr) return 0; + + return lc->getRawBrightness(x & 15, y, z & 15, level->skyDarken); } @@ -239,7 +260,15 @@ int Region::getData(int x, int y, int z) int xc = (x >> 4) - xc1; int zc = (z >> 4) - zc1; - return (*chunks)[xc]->data[zc]->getData(x & 15, y, z & 15); + if (xc < 0 || xc >= chunksDimX || zc < 0 || zc >= chunksDimZ) + { + return 0; + } + + LevelChunk *lc = flatChunks[xc * chunksDimZ + zc]; + if (lc == nullptr) return 0; + + return lc->getData(x & 15, y, z & 15); } Material *Region::getMaterial(int x, int y, int z) @@ -321,7 +350,7 @@ int Region::getBrightnessPropagate(LightLayer::variety layer, int x, int y, int // 4J Stu - The java LightLayer was an enum class type with a member "surrounding" which is what we // were returning here. Surrounding has the same value as the enum value in our C++ code, so just cast // it to an int - return (int)layer; + return static_cast(layer); } if (layer == LightLayer::Sky && level->dimension->hasCeiling) { @@ -346,7 +375,15 @@ int Region::getBrightnessPropagate(LightLayer::variety layer, int x, int y, int int xc = (x >> 4) - xc1; int zc = (z >> 4) - zc1; - return (*chunks)[xc]->data[zc]->getBrightness(layer, x & 15, y, z & 15); + if (xc < 0 || xc >= chunksDimX || zc < 0 || zc >= chunksDimZ) + { + return static_cast(layer); + } + + LevelChunk *lc = flatChunks[xc * chunksDimZ + zc]; + if (lc == nullptr) return static_cast(layer); + + return lc->getBrightness(layer, x & 15, y, z & 15); } // 4J - brought forward from 1.8.2 @@ -359,12 +396,20 @@ int Region::getBrightness(LightLayer::variety layer, int x, int y, int z) // 4J Stu - The java LightLayer was an enum class type with a member "surrounding" which is what we // were returning here. Surrounding has the same value as the enum value in our C++ code, so just cast // it to an int - return (int)layer; + return static_cast(layer); } int xc = (x >> 4) - xc1; int zc = (z >> 4) - zc1; - return (*chunks)[xc]->data[zc]->getBrightness(layer, x & 15, y, z & 15); + if (xc < 0 || xc >= chunksDimX || zc < 0 || zc >= chunksDimZ) + { + return static_cast(layer); + } + + LevelChunk *lc = flatChunks[xc * chunksDimZ + zc]; + if (lc == nullptr) return static_cast(layer); + + return lc->getBrightness(layer, x & 15, y, z & 15); } int Region::getMaxBuildHeight() diff --git a/Minecraft.World/Region.h b/Minecraft.World/Region.h index 7d6cdc11d..b8ede937e 100644 --- a/Minecraft.World/Region.h +++ b/Minecraft.World/Region.h @@ -9,8 +9,12 @@ class BiomeSource; class Region : public LevelSource { private: + static constexpr int MAX_STACK_CHUNKS = 16; // 4x4 covers the common render-chunk case (r=1) int xc1, zc1; - LevelChunk2DArray *chunks; + LevelChunk *flatChunks_stack[MAX_STACK_CHUNKS]; + LevelChunk **flatChunks; // non-owning: points to flatChunks_stack or flatChunksHeap's buffer + std::unique_ptr flatChunksHeap; // owns heap allocation for large regions + int chunksDimX, chunksDimZ; Level *level; bool allEmpty; @@ -21,6 +25,13 @@ private: public: Region(Level *level, int x1, int y1, int z1, int x2, int y2, int z2, int r); virtual ~Region(); + + // Non-copyable/movable: flatChunks may point to internal stack buffer + Region(const Region&) = delete; + Region& operator=(const Region&) = delete; + Region(Region&&) = delete; + Region& operator=(Region&&) = delete; + bool isAllEmpty(); int getTile(int x, int y, int z); shared_ptr getTileEntity(int x, int y, int z);