diff --git a/Minecraft.Client/Rendering/Chunk.cpp b/Minecraft.Client/Rendering/Chunk.cpp index 7980f241e..ff0550717 100644 --- a/Minecraft.Client/Rendering/Chunk.cpp +++ b/Minecraft.Client/Rendering/Chunk.cpp @@ -73,6 +73,7 @@ void Chunk::setPos(int x, int y, int z) { clipChunk->globalIdx = LevelRenderer::getGlobalIndexForChunk(x, y, z, level); + levelRenderer->setGlobalChunkConnectivity(clipChunk->globalIdx, ~0ULL); #if 1 // 4J - we're not using offsetted renderlists anymore, so just set the full @@ -358,6 +359,12 @@ void Chunk::rebuild() { RenderManager.CBuffClear(lists + currentLayer); } + int globalIdx = levelRenderer->getGlobalIndexForChunk(this->x, this->y, + this->z, level); + levelRenderer->setGlobalChunkConnectivity(globalIdx, ~0ULL); + levelRenderer->setGlobalChunkFlag(this->x, this->y, this->z, level, + LevelRenderer::CHUNK_FLAG_COMPILED); + delete region; delete tileRenderer; return; @@ -495,6 +502,11 @@ void Chunk::rebuild() { bb = {bounds.boundingBox[0], bounds.boundingBox[1], bounds.boundingBox[2], bounds.boundingBox[3], bounds.boundingBox[4], bounds.boundingBox[5]}; + uint64_t conn = computeConnectivity(tileIds); // pass tileIds + int globalIdx = + levelRenderer->getGlobalIndexForChunk(this->x, this->y, this->z, level); + levelRenderer->setGlobalChunkConnectivity(globalIdx, conn); + delete tileRenderer; delete region; @@ -958,6 +970,142 @@ float Chunk::squishedDistanceToSqr(std::shared_ptr player) { return xd * xd + yd * yd + zd * zd; } +uint64_t Chunk::computeConnectivity(const uint8_t* tileIds) { + const int W = 16; + const int H = 16; + const int VOLUME = W * H * W; + + auto idx = [&](int x, int y, int z) -> int { + return y * W * W + z * W + x; + }; + + auto isOpen = [&](int lx, int ly, int lz) -> bool { + int worldY = this->y + ly; + int offset = 0; + int indexY = worldY; + if (indexY >= Level::COMPRESSED_CHUNK_SECTION_HEIGHT) { + indexY -= Level::COMPRESSED_CHUNK_SECTION_HEIGHT; + offset = Level::COMPRESSED_CHUNK_SECTION_TILES; + } + + uint8_t tileId = tileIds[offset + ((lx << 11) | (lz << 7) | indexY)]; + + if (tileId == 0) return true; // air + if (tileId == 0xFF) return false; // hidden tile (yeah) + + Tile* t = Tile::tiles[tileId]; + return (t == nullptr) || !t->isSolidRender(); + }; + + uint8_t visited[6][512]; + memset(visited, 0, sizeof(visited)); + + static const int FX[6] = {1, -1, 0, 0, 0, 0}; + static const int FY[6] = {0, 0, 1, -1, 0, 0}; + static const int FZ[6] = {0, 0, 0, 0, 1, -1}; + + struct Cell { + int8_t x, y, z; + }; + static thread_local std::vector queue; + + uint64_t result = 0; + + for (int entryFace = 0; entryFace < 6; entryFace++) { + uint8_t* vis = visited[entryFace]; + queue.clear(); + int x0s, x1s, y0s, y1s, z0s, z1s; + switch (entryFace) { + case 0: + x0s = W - 1; + x1s = W - 1; + y0s = 0; + y1s = H - 1; + z0s = 0; + z1s = W - 1; + break; // +X + case 1: + x0s = 0; + x1s = 0; + y0s = 0; + y1s = H - 1; + z0s = 0; + z1s = W - 1; + break; // -X + case 2: + x0s = 0; + x1s = W - 1; + y0s = H - 1; + y1s = H - 1; + z0s = 0; + z1s = W - 1; + break; // +Y + case 3: + x0s = 0; + x1s = W - 1; + y0s = 0; + y1s = 0; + z0s = 0; + z1s = W - 1; + break; // -Y + case 4: + x0s = 0; + x1s = W - 1; + y0s = 0; + y1s = H - 1; + z0s = W - 1; + z1s = W - 1; + break; // +Z + case 5: + x0s = 0; + x1s = W - 1; + y0s = 0; + y1s = H - 1; + z0s = 0; + z1s = 0; + break; // -Z + default: + continue; + } + + for (int sy = y0s; sy <= y1s; sy++) + for (int sz = z0s; sz <= z1s; sz++) + for (int sx = x0s; sx <= x1s; sx++) { + if (!isOpen(sx, sy, sz)) continue; + int i = idx(sx, sy, sz); + if (vis[i >> 3] & (1 << (i & 7))) continue; + vis[i >> 3] |= (1 << (i & 7)); + queue.push_back({(int8_t)sx, (int8_t)sy, (int8_t)sz}); + } + + for (int qi = 0; qi < (int)queue.size(); qi++) { + Cell cur = queue[qi]; + + for (int nb = 0; nb < 6; nb++) { + int nx = cur.x + FX[nb]; + int ny = cur.y + FY[nb]; + int nz = cur.z + FZ[nb]; + + // entry exit conn + if (nx < 0 || nx >= W || ny < 0 || ny >= H || nz < 0 || + nz >= W) { + // nb IS the exit face because FX,FY,FZ are aligned + result |= ((uint64_t)1 << (entryFace * 6 + nb)); + continue; + } + + if (!isOpen(nx, ny, nz)) continue; + + int i = idx(nx, ny, nz); + if (vis[i >> 3] & (1 << (i & 7))) continue; + vis[i >> 3] |= (1 << (i & 7)); + queue.push_back({(int8_t)nx, (int8_t)ny, (int8_t)nz}); + } + } + } + + return result; +} void Chunk::reset() { if (assigned) { EnterCriticalSection(&levelRenderer->m_csDirtyChunks); @@ -1002,7 +1150,11 @@ int Chunk::getList(int layer) { return -1; } -void Chunk::cull(Culler* culler) { clipChunk->visible = culler->isVisible(&bb); } +void Chunk::cull(Culler* culler) { + if (clipChunk->visible) { + clipChunk->visible = culler->isVisible(&bb); + } +} void Chunk::renderBB() { // glCallList(lists + 2); // 4J - removed - TODO put back in diff --git a/Minecraft.Client/Rendering/Chunk.h b/Minecraft.Client/Rendering/Chunk.h index e6d214ab2..c524c69ce 100644 --- a/Minecraft.Client/Rendering/Chunk.h +++ b/Minecraft.Client/Rendering/Chunk.h @@ -48,7 +48,7 @@ public: int xm, ym, zm; AABB bb; ClipChunk* clipChunk; - + uint64_t computeConnectivity(const uint8_t* tileIds); int id; // public: // std::vector > renderableTileEntities; diff --git a/Minecraft.Client/Rendering/LevelRenderer.cpp b/Minecraft.Client/Rendering/LevelRenderer.cpp index 177b8e021..b73ee025c 100644 --- a/Minecraft.Client/Rendering/LevelRenderer.cpp +++ b/Minecraft.Client/Rendering/LevelRenderer.cpp @@ -91,6 +91,9 @@ ResourceLocation LevelRenderer::END_SKY_LOCATION = const unsigned int HALO_RING_RADIUS = 100; +uint64_t* LevelRenderer::globalChunkConnectivity = + nullptr; // bad placement do bettr juicey + #ifdef _LARGE_WORLDS Chunk LevelRenderer::permaChunk[MAX_CONCURRENT_CHUNK_REBUILDS]; C4JThread* LevelRenderer::rebuildThreads[MAX_CHUNK_REBUILD_THREADS]; @@ -189,6 +192,10 @@ LevelRenderer::LevelRenderer(Minecraft* mc, Textures* textures) { globalChunkFlags = new unsigned char[getGlobalChunkCount()]; memset(globalChunkFlags, 0, getGlobalChunkCount()); + globalChunkConnectivity = new uint64_t[getGlobalChunkCount()]; + memset(globalChunkConnectivity, 0xFF, + getGlobalChunkCount() * sizeof(uint64_t)); // 0xFF >> Fully open + starList = MemoryTracker::genLists(4); glPushMatrix(); @@ -801,14 +808,14 @@ int compare(const void* a, const void* b) { #endif +// 4jcraft: removed the vita & ps3 versions because they were SEVERELY annoying +// me it looked so ugly, god. +// if you ever hate me for it, deal with it, the source code is STILL visible if +// you look at someone elses fork. like steamcmd int LevelRenderer::renderChunks(int from, int to, int layer, double alpha) { - int playerIndex = mc->player->GetXboxPad(); // 4J added + int playerIndex = mc->player->GetXboxPad(); + if (chunks[playerIndex].data == NULL) return 0; -#if 1 - // 4J - cut down version, we're not using offsetted render lists, or a - // sorted chunk list, anymore - mc->gameRenderer->turnOnLightLayer( - alpha); // 4J - brought forward from 1.8.2 std::shared_ptr player = mc->cameraTargetPlayer; double xOff = player->xOld + (player->x - player->xOld) * alpha; double yOff = player->yOld + (player->y - player->yOld) * alpha; @@ -817,14 +824,9 @@ int LevelRenderer::renderChunks(int from, int to, int layer, double alpha) { glPushMatrix(); glTranslatef((float)-xOff, (float)-yOff, (float)-zOff); -#ifdef __PSVITA__ - // AP - also set the camera position so we can work out if a chunk is fogged - // or not - RenderManager.SetCameraPosition((float)-xOff, (float)-yOff, (float)-zOff); -#endif - -#if defined __PS3__ && !defined DISABLE_SPU_CODE - // pre- calc'd on the SPU + ClipChunk* pClipChunk = chunks[playerIndex].data; + unsigned char emptyFlag = LevelRenderer::CHUNK_FLAG_EMPTY0 << layer; + bool first = true; int count = 0; waitForCull_SPU(); if (layer == 0) { @@ -905,12 +907,11 @@ 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)pClipChunk->chunk->x, + (float)pClipChunk->chunk->y, + (float)pClipChunk->chunk->z); if (RenderManager.CBuffCall(list, first)) { first = false; @@ -960,61 +961,7 @@ int LevelRenderer::renderChunks(int from, int to, int layer, double alpha) { #endif // __PS3__ glPopMatrix(); - mc->gameRenderer->turnOffLightLayer( - alpha); // 4J - brought forward from 1.8.2 - -#else - _renderChunks.clear(); - // int p = 0; - int count = 0; - for (int i = from; i < to; i++) { - if (layer == 0) { - totalChunks++; - if (sortedChunks[playerIndex]->at(i)->emptyFlagSet(layer)) - emptyChunks++; - else if (!sortedChunks[playerIndex]->at(i)->visible) - offscreenChunks++; - else - renderedChunks++; - } - - // if (!sortedChunks[i].empty[layer] && - // sortedChunks[i].visible && - // (sortedChunks[i].occlusion_visible)) { - if (!(sortedChunks[playerIndex]->at(i)->emptyFlagSet(layer) && - sortedChunks[playerIndex]->at(i)->visible)) { - int list = sortedChunks[playerIndex]->at(i)->getList(layer); - if (list >= 0) { - _renderChunks.push_back(sortedChunks[playerIndex]->at(i)); - count++; - } - } - } - - std::shared_ptr player = mc->cameraTargetPlayer; - double xOff = player->xOld + (player->x - player->xOld) * alpha; - double yOff = player->yOld + (player->y - player->yOld) * alpha; - double zOff = player->zOld + (player->z - player->zOld) * alpha; - - for (int l = 0; l < RENDERLISTS_LENGTH; l++) renderLists[l].clear(); - int lists = 0; - for (auto it = _renderChunks.begin(); it != _renderChunks.end(); it++) { - Chunk* chunk = *it; - int list = -1; - for (int l = 0; l < lists; l++) { - if (renderLists[l].isAt(chunk->xRender, chunk->yRender, - chunk->zRender)) - list = l; - } - if (list < 0) { - list = lists++; - renderLists[list].init(chunk->xRender, chunk->yRender, - chunk->zRender, xOff, yOff, zOff); - } - renderLists[list].add(chunk->getList(layer)); - } - renderSameAsLast(layer, alpha); -#endif + mc->gameRenderer->turnOffLightLayer(alpha); return count; } @@ -1990,10 +1937,9 @@ bool LevelRenderer::updateDirtyChunks() { return; } - const int newCount = - (count < MAX_CONCURRENT_CHUNK_REBUILDS) - ? (count + 1) - : MAX_CONCURRENT_CHUNK_REBUILDS; + 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]; } @@ -2030,8 +1976,8 @@ bool LevelRenderer::updateDirtyChunks() { int index = 0; do { - // See comment on dirtyChunksLockFreeStack.Push() regarding details of - // this casting/subtracting -2. + // See comment on dirtyChunksLockFreeStack.Push() regarding details + // of this casting/subtracting -2. index = (size_t)dirtyChunksLockFreeStack.Pop(); #ifdef _CRITICAL_CHUNKS int oldIndex = index; @@ -2040,9 +1986,9 @@ bool LevelRenderer::updateDirtyChunks() { #endif if (index == 1) dirtyChunkPresent = - true; // 1 is a special value passed to let this thread know - // that a chunk which isn't on this stack has been set to - // dirty + true; // 1 is a special value passed to let this thread + // know that a chunk which isn't on this stack has + // been set to dirty else if (index > 1) { int i2 = index - 2; if (i2 >= DIMENSION_OFFSETS[2]) { @@ -2057,8 +2003,9 @@ bool LevelRenderer::updateDirtyChunks() { setGlobalChunkFlag(index - 2, CHUNK_FLAG_DIRTY); #ifdef _CRITICAL_CHUNKS - if (!(oldIndex & 0x10000000)) // was this chunk not marked as - // non-critical. Ugh double negatives + if (!(oldIndex & + 0x10000000)) // was this chunk not marked as + // non-critical. Ugh double negatives { setGlobalChunkFlag(index - 2, CHUNK_FLAG_CRITICAL); } @@ -2068,20 +2015,21 @@ bool LevelRenderer::updateDirtyChunks() { } } while (index); - // Only bother searching round all the chunks if we have some dirty chunk(s) + // Only bother searching round all the chunks if we have some dirty + // chunk(s) if (dirtyChunkPresent) { lastDirtyChunkFound = System::currentTimeMillis(); PIXBeginNamedEvent(0, "Finding nearest chunk\n"); #if defined __PS3__ && !defined DISABLE_SPU_CODE - // find the nearest chunk with a spu task, copy all the data over here - // for uploading to SPU + // find the nearest chunk with a spu task, copy all the data over + // here for uploading to SPU g_findNearestChunkDataIn.numGlobalChunks = getGlobalChunkCount(); g_findNearestChunkDataIn.pGlobalChunkFlags = globalChunkFlags; g_findNearestChunkDataIn.onlyRebuild = onlyRebuild; g_findNearestChunkDataIn.lowerOffset = (int)&((LevelChunk*)0) - ->lowerBlocks; // dodgy bit of class structure poking, as we - // don't want to try and get the whole of + ->lowerBlocks; // dodgy bit of class structure poking, as + // we don't want to try and get the whole of // LevelChunk copmpiling on SPU g_findNearestChunkDataIn.upperOffset = (int)&((LevelChunk*)0)->upperBlocks; @@ -2107,11 +2055,14 @@ bool LevelRenderer::updateDirtyChunks() { } if (level[i] != NULL) { g_findNearestChunkDataIn.multiplayerChunkCache[i].XZOFFSET = - ((MultiPlayerChunkCache*)(level[i]->chunkSource))->XZOFFSET; + ((MultiPlayerChunkCache*)(level[i]->chunkSource)) + ->XZOFFSET; g_findNearestChunkDataIn.multiplayerChunkCache[i].XZSIZE = - ((MultiPlayerChunkCache*)(level[i]->chunkSource))->XZSIZE; + ((MultiPlayerChunkCache*)(level[i]->chunkSource)) + ->XZSIZE; g_findNearestChunkDataIn.multiplayerChunkCache[i].cache = - (void**)((MultiPlayerChunkCache*)(level[i]->chunkSource)) + (void**)((MultiPlayerChunkCache*)(level[i] + ->chunkSource)) ->cache; } } @@ -2128,20 +2079,22 @@ bool LevelRenderer::updateDirtyChunks() { // 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 - // the main thread when a player chooses to exit the game So take a - // reference to the player object now. As it is a shared_ptr it - // should live as long as we need it + // It's possible that the localplayers member can be set to NULL + // on the main thread when a player chooses to exit the game So + // take a reference to the player object now. As it is a + // shared_ptr it should live as long as we need it std::shared_ptr player = mc->localplayers[p]; if (player == NULL) continue; if (chunks[p].data == NULL) continue; if (level[p] == NULL) continue; - if (chunks[p].length != xChunks * zChunks * CHUNK_Y_COUNT) continue; + if (chunks[p].length != xChunks * zChunks * CHUNK_Y_COUNT) + continue; int px = (int)player->x; int py = (int)player->y; int pz = (int)player->z; - // app.DebugPrintf("!! %d %d %d, %d %d %d {%d,%d} + // app.DebugPrintf("!! %d %d %d, %d %d %d + //{%d,%d} //",px,py,pz,stackChunkDirty,nonStackChunkDirty,onlyRebuild, // xChunks, zChunks); @@ -2152,19 +2105,19 @@ bool LevelRenderer::updateDirtyChunks() { for (int y = 0; y < CHUNK_Y_COUNT; y++) { 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 + // 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 + zd * zd; // Weighting against y to prioritise + // things in same x/z plane as player + // first if (globalChunkFlags[pClipChunk->globalIdx] & CHUNK_FLAG_DIRTY) { @@ -2172,11 +2125,11 @@ bool LevelRenderer::updateDirtyChunks() { globalChunkFlags[pClipChunk->globalIdx] & CHUNK_FLAG_COMPILED || (distSq < - 20 * 20)) // 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 + 20 * 20)) // 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 { considered++; // Is this chunk nearer than our nearest? @@ -2189,33 +2142,36 @@ bool LevelRenderer::updateDirtyChunks() { #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. + // 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] & + (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 + // 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; @@ -2225,8 +2181,8 @@ bool LevelRenderer::updateDirtyChunks() { nearChunk = pClipChunk; minDistSq = distSqWeighted; #ifdef _LARGE_WORLDS - nearestClipChunks.insert( - nearChunk, minDistSq); + nearestClipChunks.insert(nearChunk, + minDistSq); #endif } else { chunk->clearDirty(); @@ -2240,7 +2196,8 @@ bool LevelRenderer::updateDirtyChunks() { #ifdef _CRITICAL_CHUNKS // AP - is the chunk near and also critical if (distSq < 20 * 20 && - ((globalChunkFlags[pClipChunk->globalIdx] & + ((globalChunkFlags[pClipChunk + ->globalIdx] & CHUNK_FLAG_CRITICAL))) #else if (distSq < 20 * 20) @@ -2270,24 +2227,25 @@ bool LevelRenderer::updateDirtyChunks() { 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 - // group any near changes into an atomic unit. This is essential so - // we don't temporarily create any holes in the environment whilst - // updating one chunk and not the neighbours. The "ver near" aspect - // of this is just a cosmetic nicety - exactly the same thing would - // happen further away, but we just don't care about it so much from - // terms of visual impact. + // rendering until we call CBuffDeferredModeEnd(), allowing us + // to group any near changes into an atomic unit. This is + // essential so we don't temporarily create any holes in the + // environment whilst updating one chunk and not the neighbours. + // The "ver near" aspect of this is just a cosmetic nicety - + // exactly the same thing would happen further away, but we just + // don't care about it so much from terms of visual impact. if (veryNearCount > 0) { RenderManager.CBuffDeferredModeStart(); } // Build this chunk & return false to continue processing chunk->clearDirty(); // Take a copy of the details that are required for chunk - // rebuilding, and rebuild That instead of the original chunk data. - // This is done within the m_csDirtyChunks critical section, which - // means that any chunks can't be repositioned whilst we are doing - // this copy. The copy will then be guaranteed to be consistent - // whilst rebuilding takes place outside of that critical section. + // rebuilding, and rebuild That instead of the original chunk + // data. This is done within the m_csDirtyChunks critical + // section, which means that any chunks can't be repositioned + // whilst we are doing this copy. The copy will then be + // guaranteed to be consistent whilst rebuilding takes place + // outside of that critical section. permaChunk[index].makeCopyForRebuild(chunk); ++index; } @@ -2361,25 +2319,26 @@ bool LevelRenderer::updateDirtyChunks() { static Chunk permaChunk; { FRAME_PROFILE_SCOPE(ChunkRebuildSchedule); - // 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 group any near changes - // into an atomic unit. This is essential so we don't temporarily create - // any holes in the environment whilst updating one chunk and not the - // neighbours. The "ver near" aspect of this is just a cosmetic nicety - - // exactly the same thing would happen further away, but we just don't - // care about it so much from terms of visual impact. + // 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 + // group any near changes into an atomic unit. This is essential so + // we don't temporarily create any holes in the environment whilst + // updating one chunk and not the neighbours. The "ver near" aspect + // of this is just a cosmetic nicety - exactly the same thing would + // happen further away, but we just don't care about it so much from + // terms of visual impact. if (veryNearCount > 0) { RenderManager.CBuffDeferredModeStart(); } // Build this chunk & return false to continue processing chunk->clearDirty(); - // Take a copy of the details that are required for chunk rebuilding, - // and rebuild That instead of the original chunk data. This is done - // within the m_csDirtyChunks critical section, which means that any - // chunks can't be repositioned whilst we are doing this copy. The copy - // will then be guaranteed to be consistent whilst rebuilding takes - // place outside of that critical section. + // Take a copy of the details that are required for chunk + // rebuilding, and rebuild That instead of the original chunk data. + // This is done within the m_csDirtyChunks critical section, which + // means that any chunks can't be repositioned whilst we are doing + // this copy. The copy will then be guaranteed to be consistent + // whilst rebuilding takes place outside of that critical section. permaChunk.makeCopyForRebuild(chunk); LeaveCriticalSection(&m_csDirtyChunks); } @@ -2879,13 +2838,16 @@ void LevelRenderer::waitForCull_SPU() { } #endif // __PS3__ +// 4jcraft: optional occlusion culling system, i hope to upgrade it soon +// gives better performances but mostly breaks chunk rendering void LevelRenderer::cull(Culler* culler, float a) { - int playerIndex = mc->player->GetXboxPad(); // 4J added + int playerIndex = mc->player->GetXboxPad(); + if (chunks[playerIndex].data == nullptr) return; #if defined __PS3__ && !defined DISABLE_SPU_CODE cull_SPU(playerIndex, culler, a); return; -#endif // __PS3__ +#endif FrustumCuller* fc = (FrustumCuller*)culler; FrustumData* fd = fc->frustum; @@ -2901,28 +2863,297 @@ void LevelRenderer::cull(Culler* culler, float a) { (fy * -fc->yOff) + (fz * -fc->zOff)); } - ClipChunk* pClipChunk = chunks[playerIndex].data; - int vis = 0; - int total = 0; - int numWrong = 0; +#if defined(OCCLUSION_MODE_NONE) + // just check if chunk is compiled and non-empty for (unsigned int i = 0; i < chunks[playerIndex].length; i++) { - unsigned char flags = pClipChunk->globalIdx == -1 - ? 0 - : globalChunkFlags[pClipChunk->globalIdx]; - - if ((flags & CHUNK_FLAG_COMPILED) && - ((flags & CHUNK_FLAG_EMPTYBOTH) != CHUNK_FLAG_EMPTYBOTH)) { - bool clipres = clip(pClipChunk->aabb, fdraw); - pClipChunk->visible = clipres; - if (pClipChunk->visible) vis++; - total++; - } else { - pClipChunk->visible = false; + ClipChunk* cc = &chunks[playerIndex][i]; + if (cc->globalIdx < 0) { + cc->visible = false; + continue; } - pClipChunk++; - } -} + unsigned char flags = globalChunkFlags[cc->globalIdx]; + bool isCompiled = (flags & CHUNK_FLAG_COMPILED) != 0; + bool isEmptyBoth = + (flags & CHUNK_FLAG_EMPTYBOTH) == CHUNK_FLAG_EMPTYBOTH; + + cc->visible = isCompiled && !isEmptyBoth; + } + +#elif defined(OCCLUSION_MODE_FRUSTUM) + // Just ~~monika~~ frustum culling + for (unsigned int i = 0; i < chunks[playerIndex].length; i++) { + ClipChunk* cc = &chunks[playerIndex][i]; + if (cc->globalIdx < 0) { + cc->visible = false; + continue; + } + + unsigned char flags = globalChunkFlags[cc->globalIdx]; + bool isCompiled = (flags & CHUNK_FLAG_COMPILED) != 0; + bool isEmptyBoth = + (flags & CHUNK_FLAG_EMPTYBOTH) == CHUNK_FLAG_EMPTYBOTH; + + if (isCompiled && !isEmptyBoth) { + float cellBounds[6] = {(float)cc->chunk->x - 0.1f, + (float)cc->chunk->y - 0.1f, + (float)cc->chunk->z - 0.1f, + (float)cc->chunk->x + CHUNK_XZSIZE + 0.1f, + (float)cc->chunk->y + CHUNK_SIZE + 0.1f, + (float)cc->chunk->z + CHUNK_XZSIZE + 0.1f}; + cc->visible = clip(cellBounds, fdraw); + } else { + cc->visible = false; + } + } + +#elif defined(OCCLUSION_MODE_HARDWARE) +// TODO: Hardware occlusion culling using GPU queries +// For now, fall back to frustum culling +#warning \ + "OCCLUSION_MODE_HARDWARE is not implemented yet, falling back to frustum culling" + for (unsigned int i = 0; i < chunks[playerIndex].length; i++) { + ClipChunk* cc = &chunks[playerIndex][i]; + if (cc->globalIdx < 0) { + cc->visible = false; + continue; + } + + unsigned char flags = globalChunkFlags[cc->globalIdx]; + bool isCompiled = (flags & CHUNK_FLAG_COMPILED) != 0; + bool isEmptyBoth = + (flags & CHUNK_FLAG_EMPTYBOTH) == CHUNK_FLAG_EMPTYBOTH; + + if (isCompiled && !isEmptyBoth) { + float cellBounds[6] = {(float)cc->chunk->x - 0.1f, + (float)cc->chunk->y - 0.1f, + (float)cc->chunk->z - 0.1f, + (float)cc->chunk->x + CHUNK_XZSIZE + 0.1f, + (float)cc->chunk->y + CHUNK_SIZE + 0.1f, + (float)cc->chunk->z + CHUNK_XZSIZE + 0.1f}; + cc->visible = clip(cellBounds, fdraw); + } else { + cc->visible = false; + } + } + +#elif defined(OCCLUSION_MODE_BFS) + // Experimental BFS occlusion culling. + // Check https://tomcc.github.io/2014/08/31/visibility-1.html + // And https://tomcc.github.io/2014/08/31/visibility-2.html + // And finally https://en.wikipedia.org/wiki/Breadth-first_search + std::shared_ptr player = mc->cameraTargetPlayer; + float camX = (float)(player->xOld + (player->x - player->xOld) * a); + float camY = (float)(player->yOld + (player->y - player->yOld) * a); + float camZ = (float)(player->zOld + (player->z - player->zOld) * a); + + auto intFloorDiv = [](int v, int div) { + if (v < 0 && v % div != 0) return (v / div) - 1; + return v / div; + }; + + auto floatFloorDiv = [](float v, int div) { + int iv = (int)v; + if (v < 0 && v != iv) iv--; + if (iv < 0 && iv % div != 0) return (iv / div) - 1; + return iv / div; + }; + + int minCx = INT_MAX, minCy = INT_MAX, minCz = INT_MAX; + int maxCx = INT_MIN, maxCy = INT_MIN, maxCz = INT_MIN; + + for (unsigned int i = 0; i < chunks[playerIndex].length; i++) { + ClipChunk* cc = &chunks[playerIndex][i]; + cc->visible = false; + if (cc->globalIdx < 0) continue; + + int cx = intFloorDiv(cc->chunk->x, CHUNK_XZSIZE); + int cy = intFloorDiv(cc->chunk->y, CHUNK_SIZE); + int cz = intFloorDiv(cc->chunk->z, CHUNK_XZSIZE); + + if (cx < minCx) minCx = cx; + if (cy < minCy) minCy = cy; + if (cz < minCz) minCz = cz; + if (cx > maxCx) maxCx = cx; + if (cy > maxCy) maxCy = cy; + if (cz > maxCz) maxCz = cz; + } + + if (minCx > maxCx) return; + + int sizeX = maxCx - minCx + 1; + int sizeY = maxCy - minCy + 1; + int sizeZ = maxCz - minCz + 1; + + std::vector grid(sizeX * sizeY * sizeZ, nullptr); + for (unsigned int i = 0; i < chunks[playerIndex].length; i++) { + ClipChunk* cc = &chunks[playerIndex][i]; + if (cc->globalIdx < 0) continue; + int lx = intFloorDiv(cc->chunk->x, CHUNK_XZSIZE) - minCx; + int ly = intFloorDiv(cc->chunk->y, CHUNK_SIZE) - minCy; + int lz = intFloorDiv(cc->chunk->z, CHUNK_XZSIZE) - minCz; + grid[(lx * sizeY + ly) * sizeZ + lz] = cc; + } + + auto getChunkAt = [&](int cx, int cy, int cz) -> ClipChunk* { + int lx = cx - minCx; + int ly = cy - minCy; + int lz = cz - minCz; + if (lx >= 0 && lx < sizeX && ly >= 0 && ly < sizeY && lz >= 0 && + lz < sizeZ) { + return grid[(lx * sizeY + ly) * sizeZ + lz]; + } + return nullptr; + }; + + int startCx = floatFloorDiv(camX, CHUNK_XZSIZE); + int startCy = floatFloorDiv(camY, CHUNK_SIZE); + int startCz = floatFloorDiv(camZ, CHUNK_XZSIZE); + + if (startCx < minCx) + startCx = minCx; + else if (startCx > maxCx) + startCx = maxCx; + if (startCy < minCy) + startCy = minCy; + else if (startCy > maxCy) + startCy = maxCy; + if (startCz < minCz) + startCz = minCz; + else if (startCz > maxCz) + startCz = maxCz; + + ClipChunk* startChunk = getChunkAt(startCx, startCy, startCz); + + if (!startChunk) { + float minDist = 1e30f; + for (unsigned int i = 0; i < chunks[playerIndex].length; i++) { + ClipChunk* cc = &chunks[playerIndex][i]; + if (cc->globalIdx < 0) continue; + float midX = cc->chunk->x + CHUNK_XZSIZE * 0.5f; + float midY = cc->chunk->y + CHUNK_SIZE * 0.5f; + float midZ = cc->chunk->z + CHUNK_XZSIZE * 0.5f; + float dist = (camX - midX) * (camX - midX) + + (camY - midY) * (camY - midY) + + (camZ - midZ) * (camZ - midZ); + if (dist < minDist) { + minDist = dist; + startChunk = cc; + } + } + } + + if (!startChunk) return; + + struct BFSNode { + ClipChunk* cc; + int incomingFace; + }; + + std::vector q; + q.reserve(chunks[playerIndex].length * 2); + int qHead = 0; + + std::vector visitedFaces(chunks[playerIndex].length, 0); + + q.push_back({startChunk, -1}); + visitedFaces[startChunk - chunks[playerIndex].data] = 0x3F; + + static const int OFFSETS[6][3] = { + {0, -1, 0}, // 0: -Y + {0, 1, 0}, // 1: +Y + {0, 0, -1}, // 2: -Z + {0, 0, 1}, // 3: +Z + {-1, 0, 0}, // 4: -X + {1, 0, 0} // 5: +X + }; + + while (qHead < (int)q.size()) { + BFSNode node = q[qHead++]; + ClipChunk* curr = node.cc; + int incFace = node.incomingFace; + + unsigned char flags = globalChunkFlags[curr->globalIdx]; + bool isCompiled = (flags & CHUNK_FLAG_COMPILED) != 0; + bool isEmptyBoth = + (flags & CHUNK_FLAG_EMPTYBOTH) == CHUNK_FLAG_EMPTYBOTH; + + if (isCompiled && !isEmptyBoth) { + curr->visible = true; + } + + int cx = intFloorDiv(curr->chunk->x, CHUNK_XZSIZE); + int cy = intFloorDiv(curr->chunk->y, CHUNK_SIZE); + int cz = intFloorDiv(curr->chunk->z, CHUNK_XZSIZE); + + uint64_t conn = getGlobalChunkConnectivity(curr->globalIdx); + + for (int i = 0; i < 6; i++) { + int outFace = i; + + bool canGo = false; + float chkX = curr->chunk->x; + float chkY = curr->chunk->y; + float chkZ = curr->chunk->z; + switch (outFace) { + case 0: + canGo = camY >= chkY; + break; + case 1: + canGo = camY <= chkY + CHUNK_SIZE; + break; + case 2: + canGo = camZ >= chkZ; + break; + case 3: + canGo = camZ <= chkZ + CHUNK_XZSIZE; + break; + case 4: + canGo = camX >= chkX; + break; + case 5: + canGo = camX <= chkX + CHUNK_XZSIZE; + break; + } + if (!canGo) continue; + + if (incFace != -1) { + int shift = (incFace * 6) + outFace; + if ((conn & (1ULL << shift)) == 0) continue; + } + + int nx = cx + OFFSETS[i][0]; + int ny = cy + OFFSETS[i][1]; + int nz = cz + OFFSETS[i][2]; + + ClipChunk* neighbor = getChunkAt(nx, ny, nz); + if (!neighbor) continue; + + int nIdx = neighbor - chunks[playerIndex].data; + int nextIncFace = outFace ^ 1; + + if ((visitedFaces[nIdx] & (1 << nextIncFace)) != 0) continue; + + float cellBounds[6] = { + (float)neighbor->chunk->x - 0.1f, + (float)neighbor->chunk->y - 0.1f, + (float)neighbor->chunk->z - 0.1f, + (float)neighbor->chunk->x + CHUNK_XZSIZE + 0.1f, + (float)neighbor->chunk->y + CHUNK_SIZE + 0.1f, + (float)neighbor->chunk->z + CHUNK_XZSIZE + 0.1f}; + + if (!clip(cellBounds, fdraw)) continue; + + visitedFaces[nIdx] |= (1 << nextIncFace); + q.push_back({neighbor, nextIncFace}); + } + } + +#else +#error \ + "Unknown occlusion mode, this should NEVER happen, check meson.build for misconfiguration" +#endif +} void LevelRenderer::playStreamingMusic(const std::wstring& name, int x, int y, int z) { if (name != L"") { @@ -3891,6 +4122,19 @@ void LevelRenderer::setGlobalChunkFlag(int x, int y, int z, Level* level, } } +void LevelRenderer::setGlobalChunkConnectivity(int index, uint64_t conn) { + if (index >= 0 && index < getGlobalChunkCount()) { + globalChunkConnectivity[index] = conn; + } +} + +uint64_t LevelRenderer::getGlobalChunkConnectivity(int index) { + if (index >= 0 && index < getGlobalChunkCount()) { + return globalChunkConnectivity[index]; + } + return ~(uint64_t)0; // out of bounds +} + void LevelRenderer::clearGlobalChunkFlag(int x, int y, int z, Level* level, unsigned char flag, unsigned char shift) { diff --git a/Minecraft.Client/Rendering/LevelRenderer.h b/Minecraft.Client/Rendering/LevelRenderer.h index 58f194a3b..081aa98b9 100644 --- a/Minecraft.Client/Rendering/LevelRenderer.h +++ b/Minecraft.Client/Rendering/LevelRenderer.h @@ -209,7 +209,8 @@ private: emptyChunks; static const int RENDERLISTS_LENGTH = 4; // 4J - added OffsettedRenderList renderLists[RENDERLISTS_LENGTH]; - + void setGlobalChunkConnectivity(int index, uint64_t conn); + uint64_t getGlobalChunkConnectivity(int index); std::unordered_map destroyingBlocks; Icon** breakingTextures; @@ -296,6 +297,8 @@ public: void clearGlobalChunkFlag(int x, int y, int z, Level* level, unsigned char flag, unsigned char shift = 0); + static uint64_t* globalChunkConnectivity; + // Get/set whole byte of flags unsigned char getGlobalChunkFlags(int x, int y, int z, Level* level); void setGlobalChunkFlags(int x, int y, int z, Level* level, diff --git a/Minecraft.Client/meson.build b/Minecraft.Client/meson.build index abbb3b1ef..123320903 100644 --- a/Minecraft.Client/meson.build +++ b/Minecraft.Client/meson.build @@ -85,10 +85,10 @@ if get_option('enable_frame_profiler') endif if get_option('ui_backend') == 'shiggy' - shiggy_dep = dependency( - 'shiggy', - fallback : ['shiggy', 'shiggy_dep'], - ) + shiggy_dep = dependency( + 'shiggy', + fallback: ['shiggy', 'shiggy_dep'], + ) global_cpp_defs += ['-D_ENABLEIGGY'] client_dependencies += shiggy_dep @@ -98,7 +98,18 @@ if get_option('ui_backend') == 'java' global_cpp_defs += '-DENABLE_JAVA_GUIS' endif -client = executable('Minecraft.Client', +occlusion_mode = get_option('occlusion_culling') +if occlusion_mode == 'off' + global_cpp_defs += ['-DOCCLUSION_MODE_NONE'] +elif occlusion_mode == 'frustum' + global_cpp_defs += ['-DOCCLUSION_MODE_FRUSTUM'] +elif occlusion_mode == 'bfs' + global_cpp_defs += ['-DOCCLUSION_MODE_BFS', '-DUSE_OCCLUSION_CULLING'] +elif occlusion_mode == 'hardware' + global_cpp_defs += ['-DOCCLUSION_MODE_HARDWARE', '-DUSE_OCCLUSION_CULLING'] +endif +client = executable( + 'Minecraft.Client', client_sources + platform_sources + localisation[1], include_directories: include_directories('Platform', 'Platform/Linux/Iggy/include'), dependencies: client_dependencies, diff --git a/meson.options b/meson.options index 0290c8005..f92698ac4 100644 --- a/meson.options +++ b/meson.options @@ -27,8 +27,17 @@ option( ) option( +<<<<<<< HEAD 'enable_frame_profiler', type: 'boolean', value: false, description: 'Enable the in-engine frame profiler for render hotspot discovery.', ) +======= + 'occlusion_culling', + type: 'combo', + choices: ['off', 'frustum', 'bfs', 'hardware'], + value: 'frustum', + description: 'Occlusion culling mode. Off disables ALL CULLING (debug only!), Frustum disables offscreen rendering (default), BFS is experimental connectivity culling, hardware uses GPU queries.', +) +>>>>>>> db062d4ba (new culler)