mirror of
https://github.com/4jcraft/4jcraft.git
synced 2026-08-20 09:57:10 +00:00
feat(profiling): add low-overhead in-engine frame profiler
This commit is contained in:
parent
b5b4fc34fe
commit
47b7d90835
|
|
@ -50,6 +50,7 @@
|
||||||
#include "../Textures/Packs/TexturePackRepository.h"
|
#include "../Textures/Packs/TexturePackRepository.h"
|
||||||
#include "../Textures/Packs/TexturePack.h"
|
#include "../Textures/Packs/TexturePack.h"
|
||||||
#include "../Textures/TextureAtlas.h"
|
#include "../Textures/TextureAtlas.h"
|
||||||
|
#include "../Utils/FrameProfiler.h"
|
||||||
|
|
||||||
bool GameRenderer::anaglyph3d = false;
|
bool GameRenderer::anaglyph3d = false;
|
||||||
int GameRenderer::anaglyphPass = 0;
|
int GameRenderer::anaglyphPass = 0;
|
||||||
|
|
@ -776,6 +777,7 @@ void GameRenderer::renderItemInHand(float a, int eye) {
|
||||||
|
|
||||||
// 4J - change brought forward from 1.8.2
|
// 4J - change brought forward from 1.8.2
|
||||||
void GameRenderer::turnOffLightLayer(double alpha) { // 4J - TODO
|
void GameRenderer::turnOffLightLayer(double alpha) { // 4J - TODO
|
||||||
|
FRAME_PROFILE_SCOPE(Lightmap);
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
if (SharedConstants::TEXTURE_LIGHTING) {
|
if (SharedConstants::TEXTURE_LIGHTING) {
|
||||||
LinuxLogStubLightmapProbe();
|
LinuxLogStubLightmapProbe();
|
||||||
|
|
@ -807,6 +809,7 @@ void GameRenderer::turnOffLightLayer(double alpha) { // 4J - TODO
|
||||||
void GameRenderer::turnOnLightLayer(
|
void GameRenderer::turnOnLightLayer(
|
||||||
double alpha,
|
double alpha,
|
||||||
bool scaleLight) { // 4jcraft: added scaleLight for entity lighting
|
bool scaleLight) { // 4jcraft: added scaleLight for entity lighting
|
||||||
|
FRAME_PROFILE_SCOPE(Lightmap);
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
if (!SharedConstants::TEXTURE_LIGHTING) return;
|
if (!SharedConstants::TEXTURE_LIGHTING) return;
|
||||||
|
|
||||||
|
|
@ -881,6 +884,7 @@ void GameRenderer::tickLightTexture() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameRenderer::updateLightTexture(float a) {
|
void GameRenderer::updateLightTexture(float a) {
|
||||||
|
FRAME_PROFILE_SCOPE(Lightmap);
|
||||||
// 4J-JEV: Now doing light textures on PER PLAYER basis.
|
// 4J-JEV: Now doing light textures on PER PLAYER basis.
|
||||||
// 4J - we *had* added separate light textures for all dimensions, and this
|
// 4J - we *had* added separate light textures for all dimensions, and this
|
||||||
// loop to update them all here
|
// loop to update them all here
|
||||||
|
|
@ -1025,6 +1029,8 @@ int GameRenderer::getLightTexture(int iPad, Level* level) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameRenderer::render(float a, bool bFirst) {
|
void GameRenderer::render(float a, bool bFirst) {
|
||||||
|
FRAME_PROFILE_FRAME_SCOPE();
|
||||||
|
|
||||||
if (_updateLightTexture && bFirst) updateLightTexture(a);
|
if (_updateLightTexture && bFirst) updateLightTexture(a);
|
||||||
if (Display::isActive()) {
|
if (Display::isActive()) {
|
||||||
lastActiveTime = System::currentTimeMillis();
|
lastActiveTime = System::currentTimeMillis();
|
||||||
|
|
@ -1083,6 +1089,7 @@ void GameRenderer::render(float a, bool bFirst) {
|
||||||
lastNsTime = System::nanoTime();
|
lastNsTime = System::nanoTime();
|
||||||
|
|
||||||
if (!mc->options->hideGui || mc->screen != NULL) {
|
if (!mc->options->hideGui || mc->screen != NULL) {
|
||||||
|
FRAME_PROFILE_SCOPE(UIHud);
|
||||||
mc->gui->render(a, mc->screen != NULL, xMouse, yMouse);
|
mc->gui->render(a, mc->screen != NULL, xMouse, yMouse);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1097,6 +1104,7 @@ void GameRenderer::render(float a, bool bFirst) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mc->screen != NULL) {
|
if (mc->screen != NULL) {
|
||||||
|
FRAME_PROFILE_SCOPE(UIHud);
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
glClear(GL_DEPTH_BUFFER_BIT);
|
||||||
mc->screen->render(xMouse, yMouse, a);
|
mc->screen->render(xMouse, yMouse, a);
|
||||||
if (mc->screen != NULL && mc->screen->particles != NULL)
|
if (mc->screen != NULL && mc->screen->particles != NULL)
|
||||||
|
|
@ -1244,6 +1252,8 @@ void GameRenderer::DisableUpdateThread() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameRenderer::renderLevel(float a, int64_t until) {
|
void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
|
FRAME_PROFILE_SCOPE(World);
|
||||||
|
|
||||||
// if (updateLightTexture) updateLightTexture(); // 4J - TODO -
|
// if (updateLightTexture) updateLightTexture(); // 4J - TODO -
|
||||||
// Java 1.0.1 has this line enabled, should check why - don't want to put it
|
// Java 1.0.1 has this line enabled, should check why - don't want to put it
|
||||||
// in now in case it breaks split-screen
|
// in now in case it breaks split-screen
|
||||||
|
|
@ -1296,9 +1306,12 @@ void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
Frustum::getFrustum();
|
Frustum::getFrustum();
|
||||||
if (mc->options->viewDistance < 2) {
|
if (mc->options->viewDistance < 2) {
|
||||||
setupFog(-1, a);
|
setupFog(-1, a);
|
||||||
levelRenderer->renderSky(a);
|
{
|
||||||
if (mc->skins->getSelected()->getId() == 1026)
|
FRAME_PROFILE_SCOPE(WeatherSky);
|
||||||
levelRenderer->renderHaloRing(a);
|
levelRenderer->renderSky(a);
|
||||||
|
if (mc->skins->getSelected()->getId() == 1026)
|
||||||
|
levelRenderer->renderHaloRing(a);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 4jcraft: needs to be enabled for proper transparent texturing on low
|
// 4jcraft: needs to be enabled for proper transparent texturing on low
|
||||||
// render dists this was done in renderSky() for the far and normal
|
// render dists this was done in renderSky() for the far and normal
|
||||||
|
|
@ -1320,7 +1333,10 @@ void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
MemSect(0);
|
MemSect(0);
|
||||||
frustum->prepare(xOff, yOff, zOff);
|
frustum->prepare(xOff, yOff, zOff);
|
||||||
|
|
||||||
mc->levelRenderer->cull(frustum, a);
|
{
|
||||||
|
FRAME_PROFILE_SCOPE(ChunkCull);
|
||||||
|
mc->levelRenderer->cull(frustum, a);
|
||||||
|
}
|
||||||
PIXEndNamedEvent();
|
PIXEndNamedEvent();
|
||||||
|
|
||||||
#ifndef MULTITHREAD_ENABLE
|
#ifndef MULTITHREAD_ENABLE
|
||||||
|
|
@ -1347,6 +1363,7 @@ void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (cameraEntity->y < Level::genDepth) {
|
if (cameraEntity->y < Level::genDepth) {
|
||||||
|
FRAME_PROFILE_SCOPE(WeatherSky);
|
||||||
prepareAndRenderClouds(levelRenderer, a);
|
prepareAndRenderClouds(levelRenderer, a);
|
||||||
}
|
}
|
||||||
Frustum::getFrustum(); // 4J added - re-calculate frustum as rendering
|
Frustum::getFrustum(); // 4J added - re-calculate frustum as rendering
|
||||||
|
|
@ -1385,7 +1402,10 @@ void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
cameraPos.x = cameraPosTemp.x;
|
cameraPos.x = cameraPosTemp.x;
|
||||||
cameraPos.y = cameraPosTemp.y;
|
cameraPos.y = cameraPosTemp.y;
|
||||||
cameraPos.z = cameraPosTemp.z;
|
cameraPos.z = cameraPosTemp.z;
|
||||||
levelRenderer->renderEntities(&cameraPos, frustum, a);
|
{
|
||||||
|
FRAME_PROFILE_SCOPE(Entity);
|
||||||
|
levelRenderer->renderEntities(&cameraPos, frustum, a);
|
||||||
|
}
|
||||||
#ifdef __PSVITA__
|
#ifdef __PSVITA__
|
||||||
// AP - make sure we're using the Alpha cut out effect for particles
|
// AP - make sure we're using the Alpha cut out effect for particles
|
||||||
glEnable(GL_ALPHA_TEST);
|
glEnable(GL_ALPHA_TEST);
|
||||||
|
|
@ -1393,12 +1413,18 @@ void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
PIXEndNamedEvent();
|
PIXEndNamedEvent();
|
||||||
PIXBeginNamedEvent(0, "Particle render");
|
PIXBeginNamedEvent(0, "Particle render");
|
||||||
turnOnLightLayer(a); // 4J - brought forward from 1.8.2
|
turnOnLightLayer(a); // 4J - brought forward from 1.8.2
|
||||||
particleEngine->renderLit(cameraEntity, a,
|
{
|
||||||
ParticleEngine::OPAQUE_LIST);
|
FRAME_PROFILE_SCOPE(Particle);
|
||||||
|
particleEngine->renderLit(cameraEntity, a,
|
||||||
|
ParticleEngine::OPAQUE_LIST);
|
||||||
|
}
|
||||||
Lighting::turnOff();
|
Lighting::turnOff();
|
||||||
setupFog(0, a);
|
setupFog(0, a);
|
||||||
particleEngine->render(cameraEntity, a,
|
{
|
||||||
ParticleEngine::OPAQUE_LIST);
|
FRAME_PROFILE_SCOPE(Particle);
|
||||||
|
particleEngine->render(cameraEntity, a,
|
||||||
|
ParticleEngine::OPAQUE_LIST);
|
||||||
|
}
|
||||||
PIXEndNamedEvent();
|
PIXEndNamedEvent();
|
||||||
turnOffLightLayer(a); // 4J - brought forward from 1.8.2
|
turnOffLightLayer(a); // 4J - brought forward from 1.8.2
|
||||||
|
|
||||||
|
|
@ -1467,12 +1493,18 @@ void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
PIXBeginNamedEvent(0, "Particle render (translucent)");
|
PIXBeginNamedEvent(0, "Particle render (translucent)");
|
||||||
Lighting::turnOn();
|
Lighting::turnOn();
|
||||||
turnOnLightLayer(a); // 4J - brought forward from 1.8.2
|
turnOnLightLayer(a); // 4J - brought forward from 1.8.2
|
||||||
particleEngine->renderLit(cameraEntity, a,
|
{
|
||||||
ParticleEngine::TRANSLUCENT_LIST);
|
FRAME_PROFILE_SCOPE(Particle);
|
||||||
|
particleEngine->renderLit(cameraEntity, a,
|
||||||
|
ParticleEngine::TRANSLUCENT_LIST);
|
||||||
|
}
|
||||||
Lighting::turnOff();
|
Lighting::turnOff();
|
||||||
setupFog(0, a);
|
setupFog(0, a);
|
||||||
particleEngine->render(cameraEntity, a,
|
{
|
||||||
ParticleEngine::TRANSLUCENT_LIST);
|
FRAME_PROFILE_SCOPE(Particle);
|
||||||
|
particleEngine->render(cameraEntity, a,
|
||||||
|
ParticleEngine::TRANSLUCENT_LIST);
|
||||||
|
}
|
||||||
PIXEndNamedEvent();
|
PIXEndNamedEvent();
|
||||||
turnOffLightLayer(a); // 4J - brought forward from 1.8.2
|
turnOffLightLayer(a); // 4J - brought forward from 1.8.2
|
||||||
////////////////////////// End of 4J added section
|
////////////////////////// End of 4J added section
|
||||||
|
|
@ -1503,12 +1535,16 @@ void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
|
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
|
||||||
levelRenderer->renderDestroyAnimation(
|
{
|
||||||
Tesselator::getInstance(),
|
FRAME_PROFILE_SCOPE(WeatherSky);
|
||||||
std::dynamic_pointer_cast<Player>(cameraEntity), a);
|
levelRenderer->renderDestroyAnimation(
|
||||||
|
Tesselator::getInstance(),
|
||||||
|
std::dynamic_pointer_cast<Player>(cameraEntity), a);
|
||||||
|
}
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
|
|
||||||
if (cameraEntity->y >= Level::genDepth) {
|
if (cameraEntity->y >= Level::genDepth) {
|
||||||
|
FRAME_PROFILE_SCOPE(WeatherSky);
|
||||||
prepareAndRenderClouds(levelRenderer, a);
|
prepareAndRenderClouds(levelRenderer, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1517,7 +1553,10 @@ void GameRenderer::renderLevel(float a, int64_t until) {
|
||||||
setupFog(0, a);
|
setupFog(0, a);
|
||||||
glEnable(GL_FOG);
|
glEnable(GL_FOG);
|
||||||
PIXBeginNamedEvent(0, "Rendering snow and rain");
|
PIXBeginNamedEvent(0, "Rendering snow and rain");
|
||||||
renderSnowAndRain(a);
|
{
|
||||||
|
FRAME_PROFILE_SCOPE(WeatherSky);
|
||||||
|
renderSnowAndRain(a);
|
||||||
|
}
|
||||||
PIXEndNamedEvent();
|
PIXEndNamedEvent();
|
||||||
glDisable(GL_FOG);
|
glDisable(GL_FOG);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
#include "../Level/MultiPlayerLevel.h"
|
#include "../Level/MultiPlayerLevel.h"
|
||||||
#include "../../Minecraft.World/Util/SoundTypes.h"
|
#include "../../Minecraft.World/Util/SoundTypes.h"
|
||||||
#include "FrustumCuller.h"
|
#include "FrustumCuller.h"
|
||||||
|
#include "../Utils/FrameProfiler.h"
|
||||||
|
|
||||||
// #define DISABLE_SPU_CODE
|
// #define DISABLE_SPU_CODE
|
||||||
|
|
||||||
|
|
@ -736,6 +737,8 @@ void LevelRenderer::resortChunks(int xc, int yc, int zc) {
|
||||||
|
|
||||||
int LevelRenderer::render(std::shared_ptr<LivingEntity> player, int layer,
|
int LevelRenderer::render(std::shared_ptr<LivingEntity> player, int layer,
|
||||||
double alpha, bool updateChunks) {
|
double alpha, bool updateChunks) {
|
||||||
|
FRAME_PROFILE_SCOPE(Terrain);
|
||||||
|
|
||||||
int playerIndex = mc->player->GetXboxPad();
|
int playerIndex = mc->player->GetXboxPad();
|
||||||
|
|
||||||
// 4J - added - if the number of players has changed, we need to rebuild
|
// 4J - added - if the number of players has changed, we need to rebuild
|
||||||
|
|
@ -851,49 +854,55 @@ int LevelRenderer::renderChunks(int from, int to, int layer, double alpha) {
|
||||||
if (sortList.capacity() < (size_t)chunks[playerIndex].length) {
|
if (sortList.capacity() < (size_t)chunks[playerIndex].length) {
|
||||||
sortList.reserve(chunks[playerIndex].length);
|
sortList.reserve(chunks[playerIndex].length);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < chunks[playerIndex].length; i++, pClipChunk++) {
|
{
|
||||||
if (!pClipChunk->visible)
|
FRAME_PROFILE_SCOPE(ChunkCollect);
|
||||||
continue; // This will be set if the chunk isn't visible, or isn't
|
for (int i = 0; i < chunks[playerIndex].length; i++, pClipChunk++) {
|
||||||
// compiled, or has both empty flags set
|
if (!pClipChunk->visible)
|
||||||
if (pClipChunk->globalIdx == -1)
|
continue; // This will be set if the chunk isn't visible, or
|
||||||
continue; // Not sure if we should ever encounter this... TODO
|
// isn't compiled, or has both empty flags set
|
||||||
// check
|
if (pClipChunk->globalIdx == -1)
|
||||||
if ((globalChunkFlags[pClipChunk->globalIdx] & emptyFlag) == emptyFlag)
|
continue; // Not sure if we should ever encounter this...
|
||||||
continue;
|
// TODO check
|
||||||
|
if ((globalChunkFlags[pClipChunk->globalIdx] & emptyFlag) ==
|
||||||
|
emptyFlag)
|
||||||
|
continue;
|
||||||
|
|
||||||
sortList.push_back(pClipChunk);
|
sortList.push_back(pClipChunk);
|
||||||
}
|
|
||||||
// he sorts me till i
|
|
||||||
std::sort(sortList.begin(), sortList.end(),
|
|
||||||
[xOff, yOff, zOff, layer](ClipChunk* a, ClipChunk* b) {
|
|
||||||
float dxA = (float)((a->chunk->x + 8.0f) - xOff);
|
|
||||||
float dyA = (float)((a->chunk->y + 8.0f) - yOff);
|
|
||||||
float dzA = (float)((a->chunk->z + 8.0f) - zOff);
|
|
||||||
float distSqA = dxA * dxA + dyA * dyA + dzA * dzA;
|
|
||||||
|
|
||||||
float dxB = (float)((b->chunk->x + 8.0f) - xOff);
|
|
||||||
float dyB = (float)((b->chunk->y + 8.0f) - yOff);
|
|
||||||
float dzB = (float)((b->chunk->z + 8.0f) - zOff);
|
|
||||||
float distSqB = dxB * dxB + dyB * dyB + dzB * dzB;
|
|
||||||
|
|
||||||
if (layer == 0)
|
|
||||||
return distSqA < distSqB; // Opaque: Closest first
|
|
||||||
return distSqA > distSqB; // Transparent: Furthest first
|
|
||||||
});
|
|
||||||
|
|
||||||
for (ClipChunk* chunk : sortList) {
|
|
||||||
// ugly occluder
|
|
||||||
float dx = (chunk->chunk->x + 8.0f) - (float)xOff;
|
|
||||||
float dy = (chunk->chunk->y + 8.0f) - (float)yOff;
|
|
||||||
float dz = (chunk->chunk->z + 8.0f) - (float)zOff;
|
|
||||||
bool isVeryNear = (dx * dx + dy * dy + dz * dz) < (16.0f * 16.0f);
|
|
||||||
|
|
||||||
if (!isVeryNear && layer == 0) {
|
|
||||||
// todo: occlusion flag
|
|
||||||
}
|
}
|
||||||
|
// he sorts me till i
|
||||||
|
std::sort(sortList.begin(), sortList.end(),
|
||||||
|
[xOff, yOff, zOff, layer](ClipChunk* a, ClipChunk* b) {
|
||||||
|
float dxA = (float)((a->chunk->x + 8.0f) - xOff);
|
||||||
|
float dyA = (float)((a->chunk->y + 8.0f) - yOff);
|
||||||
|
float dzA = (float)((a->chunk->z + 8.0f) - zOff);
|
||||||
|
float distSqA = dxA * dxA + dyA * dyA + dzA * dzA;
|
||||||
|
|
||||||
int list = chunk->globalIdx * 2 + layer;
|
float dxB = (float)((b->chunk->x + 8.0f) - xOff);
|
||||||
list += chunkLists;
|
float dyB = (float)((b->chunk->y + 8.0f) - yOff);
|
||||||
|
float dzB = (float)((b->chunk->z + 8.0f) - zOff);
|
||||||
|
float distSqB = dxB * dxB + dyB * dyB + dzB * dzB;
|
||||||
|
|
||||||
|
if (layer == 0)
|
||||||
|
return distSqA < distSqB; // Opaque: Closest first
|
||||||
|
return distSqA > distSqB; // Transparent: Furthest
|
||||||
|
// first
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
FRAME_PROFILE_SCOPE(ChunkPlayback);
|
||||||
|
for (ClipChunk* chunk : sortList) {
|
||||||
|
// ugly occluder
|
||||||
|
float dx = (chunk->chunk->x + 8.0f) - (float)xOff;
|
||||||
|
float dy = (chunk->chunk->y + 8.0f) - (float)yOff;
|
||||||
|
float dz = (chunk->chunk->z + 8.0f) - (float)zOff;
|
||||||
|
bool isVeryNear = (dx * dx + dy * dy + dz * dz) < (16.0f * 16.0f);
|
||||||
|
|
||||||
|
if (!isVeryNear && layer == 0) {
|
||||||
|
// todo: occlusion flag
|
||||||
|
}
|
||||||
|
|
||||||
|
int list = chunk->globalIdx * 2 + layer;
|
||||||
|
list += chunkLists;
|
||||||
|
|
||||||
// 4jcraft: replaced glPushMatrix/glTranslatef/glPopMatrix per chunk
|
// 4jcraft: replaced glPushMatrix/glTranslatef/glPopMatrix per chunk
|
||||||
// no more full MVP upload per chunk, can also be bkwards compat
|
// no more full MVP upload per chunk, can also be bkwards compat
|
||||||
|
|
@ -901,12 +910,13 @@ int LevelRenderer::renderChunks(int from, int to, int layer, double alpha) {
|
||||||
(float)chunk->chunk->y,
|
(float)chunk->chunk->y,
|
||||||
(float)chunk->chunk->z);
|
(float)chunk->chunk->z);
|
||||||
|
|
||||||
if (RenderManager.CBuffCall(list, first)) {
|
if (RenderManager.CBuffCall(list, first)) {
|
||||||
first = false;
|
first = false;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
count++;
|
RenderManager.SetChunkOffset(0.f, 0.f, 0.f);
|
||||||
}
|
}
|
||||||
RenderManager.SetChunkOffset(0.f, 0.f, 0.f);
|
|
||||||
|
|
||||||
#ifdef __PSVITA__
|
#ifdef __PSVITA__
|
||||||
// AP - alpha cut out is expensive on vita. Now we render all the alpha cut
|
// AP - alpha cut out is expensive on vita. Now we render all the alpha cut
|
||||||
|
|
@ -917,25 +927,29 @@ int LevelRenderer::renderChunks(int from, int to, int layer, double alpha) {
|
||||||
first = true;
|
first = true;
|
||||||
pClipChunk = chunks[playerIndex].data;
|
pClipChunk = chunks[playerIndex].data;
|
||||||
emptyFlag = LevelRenderer::CHUNK_FLAG_EMPTY0 << layer;
|
emptyFlag = LevelRenderer::CHUNK_FLAG_EMPTY0 << layer;
|
||||||
for (int i = 0; i < chunks[playerIndex].length; i++, pClipChunk++) {
|
{
|
||||||
if (!pClipChunk->visible)
|
FRAME_PROFILE_SCOPE(ChunkPlayback);
|
||||||
continue; // This will be set if the chunk isn't visible, or isn't
|
for (int i = 0; i < chunks[playerIndex].length; i++, pClipChunk++) {
|
||||||
// compiled, or has both empty flags set
|
if (!pClipChunk->visible)
|
||||||
if (pClipChunk->globalIdx == -1)
|
continue; // This will be set if the chunk isn't visible, or
|
||||||
continue; // Not sure if we should ever encounter this... TODO
|
// isn't compiled, or has both empty flags set
|
||||||
// check
|
if (pClipChunk->globalIdx == -1)
|
||||||
if ((globalChunkFlags[pClipChunk->globalIdx] & emptyFlag) == emptyFlag)
|
continue; // Not sure if we should ever encounter this... TODO
|
||||||
continue; // Check that this particular layer isn't empty
|
// check
|
||||||
if (!(globalChunkFlags[pClipChunk->globalIdx] &
|
if ((globalChunkFlags[pClipChunk->globalIdx] & emptyFlag) ==
|
||||||
LevelRenderer::CHUNK_FLAG_CUT_OUT))
|
emptyFlag)
|
||||||
continue; // Does this chunk contain any cut out geometry
|
continue; // Check that this particular layer isn't empty
|
||||||
|
if (!(globalChunkFlags[pClipChunk->globalIdx] &
|
||||||
|
LevelRenderer::CHUNK_FLAG_CUT_OUT))
|
||||||
|
continue; // Does this chunk contain any cut out geometry
|
||||||
|
|
||||||
// List can be calculated directly from the chunk's global idex
|
// List can be calculated directly from the chunk's global idex
|
||||||
int list = pClipChunk->globalIdx * 2 + layer;
|
int list = pClipChunk->globalIdx * 2 + layer;
|
||||||
list += chunkLists;
|
list += chunkLists;
|
||||||
|
|
||||||
if (RenderManager.CBuffCallCutOut(list, first)) {
|
if (RenderManager.CBuffCallCutOut(list, first)) {
|
||||||
first = false;
|
first = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RenderManager.StateSetForceLOD(-1); // AP - back to normal mipmapping
|
RenderManager.StateSetForceLOD(-1); // AP - back to normal mipmapping
|
||||||
|
|
@ -1959,312 +1973,319 @@ bool LevelRenderer::updateDirtyChunks() {
|
||||||
|
|
||||||
// Set a flag if we should only rebuild existing chunks, not create anything
|
// Set a flag if we should only rebuild existing chunks, not create anything
|
||||||
// new
|
// new
|
||||||
unsigned int memAlloc = RenderManager.CBuffSize(-1);
|
|
||||||
/*
|
|
||||||
static int throttle = 0;
|
|
||||||
if( ( throttle % 100 ) == 0 )
|
|
||||||
{
|
{
|
||||||
app.DebugPrintf("CBuffSize: %d\n",memAlloc/(1024*1024));
|
FRAME_PROFILE_SCOPE(ChunkDirtyScan);
|
||||||
}
|
|
||||||
throttle++;
|
|
||||||
*/
|
|
||||||
PIXAddNamedCounter(((float)memAlloc) / (1024.0f * 1024.0f),
|
|
||||||
"Command buffer allocations");
|
|
||||||
bool onlyRebuild = (memAlloc >= MAX_COMMANDBUFFER_ALLOCATIONS);
|
|
||||||
EnterCriticalSection(&m_csDirtyChunks);
|
|
||||||
|
|
||||||
// Move any dirty chunks stored in the lock free stack into global flags
|
unsigned int memAlloc = RenderManager.CBuffSize(-1);
|
||||||
int index = 0;
|
/*
|
||||||
|
static int throttle = 0;
|
||||||
do {
|
if( ( throttle % 100 ) == 0 )
|
||||||
// See comment on dirtyChunksLockFreeStack.Push() regarding details of
|
{
|
||||||
// this casting/subtracting -2.
|
app.DebugPrintf("CBuffSize: %d\n",memAlloc/(1024*1024));
|
||||||
index = (size_t)dirtyChunksLockFreeStack.Pop();
|
|
||||||
#ifdef _CRITICAL_CHUNKS
|
|
||||||
int oldIndex = index;
|
|
||||||
index &= 0x0fffffff; // remove the top bit that marked the chunk as
|
|
||||||
// non-critical
|
|
||||||
#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
|
|
||||||
else if (index > 1) {
|
|
||||||
int i2 = index - 2;
|
|
||||||
if (i2 >= DIMENSION_OFFSETS[2]) {
|
|
||||||
i2 -= DIMENSION_OFFSETS[2];
|
|
||||||
int y2 = i2 & (CHUNK_Y_COUNT - 1);
|
|
||||||
i2 /= CHUNK_Y_COUNT;
|
|
||||||
int z2 = i2 / MAX_LEVEL_RENDER_SIZE[2];
|
|
||||||
int x2 = i2 - z2 * MAX_LEVEL_RENDER_SIZE[2];
|
|
||||||
x2 -= MAX_LEVEL_RENDER_SIZE[2] / 2;
|
|
||||||
z2 -= MAX_LEVEL_RENDER_SIZE[2] / 2;
|
|
||||||
}
|
|
||||||
setGlobalChunkFlag(index - 2, CHUNK_FLAG_DIRTY);
|
|
||||||
|
|
||||||
#ifdef _CRITICAL_CHUNKS
|
|
||||||
if (!(oldIndex & 0x10000000)) // was this chunk not marked as
|
|
||||||
// non-critical. Ugh double negatives
|
|
||||||
{
|
|
||||||
setGlobalChunkFlag(index - 2, CHUNK_FLAG_CRITICAL);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
dirtyChunkPresent = true;
|
|
||||||
}
|
}
|
||||||
} while (index);
|
throttle++;
|
||||||
|
*/
|
||||||
|
PIXAddNamedCounter(((float)memAlloc) / (1024.0f * 1024.0f),
|
||||||
|
"Command buffer allocations");
|
||||||
|
bool onlyRebuild = (memAlloc >= MAX_COMMANDBUFFER_ALLOCATIONS);
|
||||||
|
EnterCriticalSection(&m_csDirtyChunks);
|
||||||
|
|
||||||
// Only bother searching round all the chunks if we have some dirty chunk(s)
|
// Move any dirty chunks stored in the lock free stack into global flags
|
||||||
if (dirtyChunkPresent) {
|
int index = 0;
|
||||||
lastDirtyChunkFound = System::currentTimeMillis();
|
|
||||||
PIXBeginNamedEvent(0, "Finding nearest chunk\n");
|
do {
|
||||||
|
// See comment on dirtyChunksLockFreeStack.Push() regarding details of
|
||||||
|
// this casting/subtracting -2.
|
||||||
|
index = (size_t)dirtyChunksLockFreeStack.Pop();
|
||||||
|
#ifdef _CRITICAL_CHUNKS
|
||||||
|
int oldIndex = index;
|
||||||
|
index &= 0x0fffffff; // remove the top bit that marked the chunk as
|
||||||
|
// non-critical
|
||||||
|
#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
|
||||||
|
else if (index > 1) {
|
||||||
|
int i2 = index - 2;
|
||||||
|
if (i2 >= DIMENSION_OFFSETS[2]) {
|
||||||
|
i2 -= DIMENSION_OFFSETS[2];
|
||||||
|
int y2 = i2 & (CHUNK_Y_COUNT - 1);
|
||||||
|
i2 /= CHUNK_Y_COUNT;
|
||||||
|
int z2 = i2 / MAX_LEVEL_RENDER_SIZE[2];
|
||||||
|
int x2 = i2 - z2 * MAX_LEVEL_RENDER_SIZE[2];
|
||||||
|
x2 -= MAX_LEVEL_RENDER_SIZE[2] / 2;
|
||||||
|
z2 -= MAX_LEVEL_RENDER_SIZE[2] / 2;
|
||||||
|
}
|
||||||
|
setGlobalChunkFlag(index - 2, CHUNK_FLAG_DIRTY);
|
||||||
|
|
||||||
|
#ifdef _CRITICAL_CHUNKS
|
||||||
|
if (!(oldIndex & 0x10000000)) // was this chunk not marked as
|
||||||
|
// non-critical. Ugh double negatives
|
||||||
|
{
|
||||||
|
setGlobalChunkFlag(index - 2, CHUNK_FLAG_CRITICAL);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
dirtyChunkPresent = true;
|
||||||
|
}
|
||||||
|
} while (index);
|
||||||
|
|
||||||
|
// 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
|
#if defined __PS3__ && !defined DISABLE_SPU_CODE
|
||||||
// find the nearest chunk with a spu task, copy all the data over here
|
// find the nearest chunk with a spu task, copy all the data over here
|
||||||
// for uploading to SPU
|
// for uploading to SPU
|
||||||
g_findNearestChunkDataIn.numGlobalChunks = getGlobalChunkCount();
|
g_findNearestChunkDataIn.numGlobalChunks = getGlobalChunkCount();
|
||||||
g_findNearestChunkDataIn.pGlobalChunkFlags = globalChunkFlags;
|
g_findNearestChunkDataIn.pGlobalChunkFlags = globalChunkFlags;
|
||||||
g_findNearestChunkDataIn.onlyRebuild = onlyRebuild;
|
g_findNearestChunkDataIn.onlyRebuild = onlyRebuild;
|
||||||
g_findNearestChunkDataIn.lowerOffset =
|
g_findNearestChunkDataIn.lowerOffset =
|
||||||
(int)&((LevelChunk*)0)
|
(int)&((LevelChunk*)0)
|
||||||
->lowerBlocks; // dodgy bit of class structure poking, as we
|
->lowerBlocks; // dodgy bit of class structure poking, as we
|
||||||
// don't want to try and get the whole of
|
// don't want to try and get the whole of
|
||||||
// LevelChunk copmpiling on SPU
|
// LevelChunk copmpiling on SPU
|
||||||
g_findNearestChunkDataIn.upperOffset =
|
g_findNearestChunkDataIn.upperOffset =
|
||||||
(int)&((LevelChunk*)0)->upperBlocks;
|
(int)&((LevelChunk*)0)->upperBlocks;
|
||||||
g_findNearestChunkDataIn.xChunks = xChunks;
|
g_findNearestChunkDataIn.xChunks = xChunks;
|
||||||
g_findNearestChunkDataIn.yChunks = yChunks;
|
g_findNearestChunkDataIn.yChunks = yChunks;
|
||||||
g_findNearestChunkDataIn.zChunks = zChunks;
|
g_findNearestChunkDataIn.zChunks = zChunks;
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
g_findNearestChunkDataIn.chunks[i] =
|
g_findNearestChunkDataIn.chunks[i] =
|
||||||
(LevelRenderer_FindNearestChunk_DataIn::ClipChunk*)chunks[i]
|
(LevelRenderer_FindNearestChunk_DataIn::ClipChunk*)chunks[i]
|
||||||
.data;
|
.data;
|
||||||
g_findNearestChunkDataIn.chunkLengths[i] = chunks[i].length;
|
g_findNearestChunkDataIn.chunkLengths[i] = chunks[i].length;
|
||||||
g_findNearestChunkDataIn.level[i] = level[i];
|
g_findNearestChunkDataIn.level[i] = level[i];
|
||||||
g_findNearestChunkDataIn.playerData[i].bValid =
|
g_findNearestChunkDataIn.playerData[i].bValid =
|
||||||
mc->localplayers[i] != NULL;
|
mc->localplayers[i] != NULL;
|
||||||
if (mc->localplayers[i] != NULL) {
|
if (mc->localplayers[i] != NULL) {
|
||||||
g_findNearestChunkDataIn.playerData[i].x =
|
g_findNearestChunkDataIn.playerData[i].x =
|
||||||
mc->localplayers[i]->x;
|
mc->localplayers[i]->x;
|
||||||
g_findNearestChunkDataIn.playerData[i].y =
|
g_findNearestChunkDataIn.playerData[i].y =
|
||||||
mc->localplayers[i]->y;
|
mc->localplayers[i]->y;
|
||||||
g_findNearestChunkDataIn.playerData[i].z =
|
g_findNearestChunkDataIn.playerData[i].z =
|
||||||
mc->localplayers[i]->z;
|
mc->localplayers[i]->z;
|
||||||
|
}
|
||||||
|
if (level[i] != NULL) {
|
||||||
|
g_findNearestChunkDataIn.multiplayerChunkCache[i].XZOFFSET =
|
||||||
|
((MultiPlayerChunkCache*)(level[i]->chunkSource))->XZOFFSET;
|
||||||
|
g_findNearestChunkDataIn.multiplayerChunkCache[i].XZSIZE =
|
||||||
|
((MultiPlayerChunkCache*)(level[i]->chunkSource))->XZSIZE;
|
||||||
|
g_findNearestChunkDataIn.multiplayerChunkCache[i].cache =
|
||||||
|
(void**)((MultiPlayerChunkCache*)(level[i]->chunkSource))
|
||||||
|
->cache;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (level[i] != NULL) {
|
|
||||||
g_findNearestChunkDataIn.multiplayerChunkCache[i].XZOFFSET =
|
|
||||||
((MultiPlayerChunkCache*)(level[i]->chunkSource))->XZOFFSET;
|
|
||||||
g_findNearestChunkDataIn.multiplayerChunkCache[i].XZSIZE =
|
|
||||||
((MultiPlayerChunkCache*)(level[i]->chunkSource))->XZSIZE;
|
|
||||||
g_findNearestChunkDataIn.multiplayerChunkCache[i].cache =
|
|
||||||
(void**)((MultiPlayerChunkCache*)(level[i]->chunkSource))
|
|
||||||
->cache;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert(sizeof(LevelRenderer_FindNearestChunk_DataIn::Chunk)
|
// assert(sizeof(LevelRenderer_FindNearestChunk_DataIn::Chunk)
|
||||||
// == sizeof(Chunk));
|
// == sizeof(Chunk));
|
||||||
C4JSpursJob_LevelRenderer_FindNearestChunk findJob(
|
C4JSpursJob_LevelRenderer_FindNearestChunk findJob(
|
||||||
&g_findNearestChunkDataIn);
|
&g_findNearestChunkDataIn);
|
||||||
m_jobPort_FindNearestChunk->submitJob(&findJob);
|
m_jobPort_FindNearestChunk->submitJob(&findJob);
|
||||||
m_jobPort_FindNearestChunk->waitForCompletion();
|
m_jobPort_FindNearestChunk->waitForCompletion();
|
||||||
nearChunk = (ClipChunk*)g_findNearestChunkDataIn.nearChunk;
|
nearChunk = (ClipChunk*)g_findNearestChunkDataIn.nearChunk;
|
||||||
veryNearCount = g_findNearestChunkDataIn.veryNearCount;
|
veryNearCount = g_findNearestChunkDataIn.veryNearCount;
|
||||||
#else // __PS3__
|
#else // __PS3__
|
||||||
|
|
||||||
#ifdef _LARGE_WORLDS
|
#ifdef _LARGE_WORLDS
|
||||||
int maxNearestChunks = MAX_CONCURRENT_CHUNK_REBUILDS;
|
int maxNearestChunks = MAX_CONCURRENT_CHUNK_REBUILDS;
|
||||||
// 4J Stu - On XboxOne we should cut this down if in a constrained state
|
// 4J Stu - On XboxOne we should cut this down if in a constrained state
|
||||||
// so the saving threads get more time
|
// so the saving threads get more time
|
||||||
#endif
|
#endif
|
||||||
// Find nearest chunk that is dirty
|
// Find nearest chunk that is dirty
|
||||||
for (int p = 0; p < XUSER_MAX_COUNT; p++) {
|
for (int p = 0; p < XUSER_MAX_COUNT; p++) {
|
||||||
// It's possible that the localplayers member can be set to NULL on
|
// 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
|
// 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
|
// reference to the player object now. As it is a shared_ptr it
|
||||||
// should live as long as we need it
|
// should live as long as we need it
|
||||||
std::shared_ptr<LocalPlayer> player = mc->localplayers[p];
|
std::shared_ptr<LocalPlayer> player = mc->localplayers[p];
|
||||||
if (player == NULL) continue;
|
if (player == NULL) continue;
|
||||||
if (chunks[p].data == NULL) continue;
|
if (chunks[p].data == NULL) continue;
|
||||||
if (level[p] == 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 px = (int)player->x;
|
||||||
int py = (int)player->y;
|
int py = (int)player->y;
|
||||||
int pz = (int)player->z;
|
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,
|
//",px,py,pz,stackChunkDirty,nonStackChunkDirty,onlyRebuild,
|
||||||
// xChunks, zChunks);
|
// xChunks, zChunks);
|
||||||
|
|
||||||
int considered = 0;
|
int considered = 0;
|
||||||
int wouldBeNearButEmpty = 0;
|
int wouldBeNearButEmpty = 0;
|
||||||
for (int x = 0; x < xChunks; x++) {
|
for (int x = 0; x < xChunks; x++) {
|
||||||
for (int z = 0; z < zChunks; z++) {
|
for (int z = 0; z < zChunks; z++) {
|
||||||
for (int y = 0; y < CHUNK_Y_COUNT; y++) {
|
for (int y = 0; y < CHUNK_Y_COUNT; y++) {
|
||||||
ClipChunk* pClipChunk =
|
ClipChunk* pClipChunk =
|
||||||
&chunks[p][(z * yChunks + y) * xChunks + x];
|
&chunks[p][(z * yChunks + y) * xChunks + x];
|
||||||
// Get distance to this chunk - deliberately not calling
|
// Get distance to this chunk - deliberately not calling
|
||||||
// the chunk's method of doing this to avoid overheads
|
// the chunk's method of doing this to avoid overheads
|
||||||
// (passing entitie, type conversion etc.) that this
|
// (passing entitie, type conversion etc.) that this
|
||||||
// involves
|
// involves
|
||||||
int xd = pClipChunk->xm - px;
|
int xd = pClipChunk->xm - px;
|
||||||
int yd = pClipChunk->ym - py;
|
int yd = pClipChunk->ym - py;
|
||||||
int zd = pClipChunk->zm - pz;
|
int zd = pClipChunk->zm - pz;
|
||||||
int distSq = xd * xd + yd * yd + zd * zd;
|
int distSq = xd * xd + yd * yd + zd * zd;
|
||||||
int distSqWeighted =
|
int distSqWeighted =
|
||||||
xd * xd + yd * yd * 4 +
|
xd * xd + yd * yd * 4 +
|
||||||
zd *
|
zd *
|
||||||
zd; // Weighting against y to prioritise things
|
zd; // Weighting against y to prioritise things
|
||||||
// in same x/z plane as player first
|
// in same x/z plane as player first
|
||||||
|
|
||||||
if (globalChunkFlags[pClipChunk->globalIdx] &
|
if (globalChunkFlags[pClipChunk->globalIdx] &
|
||||||
CHUNK_FLAG_DIRTY) {
|
CHUNK_FLAG_DIRTY) {
|
||||||
if ((!onlyRebuild) ||
|
if ((!onlyRebuild) ||
|
||||||
globalChunkFlags[pClipChunk->globalIdx] &
|
globalChunkFlags[pClipChunk->globalIdx] &
|
||||||
CHUNK_FLAG_COMPILED ||
|
CHUNK_FLAG_COMPILED ||
|
||||||
(distSq <
|
(distSq <
|
||||||
20 * 20)) // Always rebuild really near things
|
20 * 20)) // Always rebuild really near things
|
||||||
// or else building (say) at tower
|
// or else building (say) at tower
|
||||||
// up into empty blocks when we are
|
// up into empty blocks when we are
|
||||||
// low on memory will not create
|
// low on memory will not create
|
||||||
// render data
|
// render data
|
||||||
{
|
|
||||||
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);
|
|
||||||
#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
|
considered++;
|
||||||
// would like to consider for rendering, at
|
// Is this chunk nearer than our nearest?
|
||||||
// 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
|
#ifdef _LARGE_WORLDS
|
||||||
nearestClipChunks.insert(
|
bool isNearer = nearestClipChunks.empty();
|
||||||
itNearest,
|
AUTO_VAR(itNearest, nearestClipChunks.begin());
|
||||||
std::pair<ClipChunk*, int>(
|
for (; itNearest != nearestClipChunks.end();
|
||||||
nearChunk, minDistSq));
|
++itNearest) {
|
||||||
if (nearestClipChunks.size() >
|
isNearer =
|
||||||
maxNearestChunks) {
|
distSqWeighted < itNearest->second;
|
||||||
nearestClipChunks.pop_back();
|
if (isNearer) break;
|
||||||
}
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
chunk->clearDirty();
|
|
||||||
globalChunkFlags[pClipChunk
|
|
||||||
->globalIdx] |=
|
|
||||||
CHUNK_FLAG_EMPTYBOTH;
|
|
||||||
wouldBeNearButEmpty++;
|
|
||||||
}
|
}
|
||||||
}
|
isNearer =
|
||||||
|
isNearer || (nearestClipChunks.size() <
|
||||||
|
maxNearestChunks);
|
||||||
|
#else
|
||||||
|
bool isNearer = distSqWeighted < minDistSq;
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef _CRITICAL_CHUNKS
|
#ifdef _CRITICAL_CHUNKS
|
||||||
// AP - is the chunk near and also critical
|
// AP - this will make sure that if a deferred
|
||||||
if (distSq < 20 * 20 &&
|
// grouping has started, only critical chunks go
|
||||||
((globalChunkFlags[pClipChunk->globalIdx] &
|
// into that grouping, even if a non-critical
|
||||||
CHUNK_FLAG_CRITICAL)))
|
// chunk is closer.
|
||||||
|
if ((!veryNearCount && isNearer) ||
|
||||||
|
(distSq < 20 * 20 &&
|
||||||
|
(globalChunkFlags[pClipChunk->globalIdx] &
|
||||||
|
CHUNK_FLAG_CRITICAL)))
|
||||||
#else
|
#else
|
||||||
if (distSq < 20 * 20)
|
if (isNearer)
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
veryNearCount++;
|
// 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::pair<ClipChunk*, int>(
|
||||||
|
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++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// app.DebugPrintf("[%d,%d,%d]\n",nearestClipChunks.empty(),considered,wouldBeNearButEmpty);
|
||||||
}
|
}
|
||||||
// app.DebugPrintf("[%d,%d,%d]\n",nearestClipChunks.empty(),considered,wouldBeNearButEmpty);
|
|
||||||
}
|
|
||||||
#endif // __PS3__
|
#endif // __PS3__
|
||||||
PIXEndNamedEvent();
|
PIXEndNamedEvent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Chunk* chunk = NULL;
|
Chunk* chunk = NULL;
|
||||||
#ifdef _LARGE_WORLDS
|
#ifdef _LARGE_WORLDS
|
||||||
if (!nearestClipChunks.empty()) {
|
if (!nearestClipChunks.empty()) {
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (AUTO_VAR(it, nearestClipChunks.begin());
|
{
|
||||||
it != nearestClipChunks.end(); ++it) {
|
FRAME_PROFILE_SCOPE(ChunkRebuildSchedule);
|
||||||
chunk = it->first->chunk;
|
for (AUTO_VAR(it, nearestClipChunks.begin());
|
||||||
// If this chunk is very near, then move the renderer into a
|
it != nearestClipChunks.end(); ++it) {
|
||||||
// deferred mode. This won't commit any command buffers for
|
chunk = it->first->chunk;
|
||||||
// rendering until we call CBuffDeferredModeEnd(), allowing us to
|
// If this chunk is very near, then move the renderer into a
|
||||||
// group any near changes into an atomic unit. This is essential so
|
// deferred mode. This won't commit any command buffers for
|
||||||
// we don't temporarily create any holes in the environment whilst
|
// rendering until we call CBuffDeferredModeEnd(), allowing us to
|
||||||
// updating one chunk and not the neighbours. The "ver near" aspect
|
// group any near changes into an atomic unit. This is essential so
|
||||||
// of this is just a cosmetic nicety - exactly the same thing would
|
// we don't temporarily create any holes in the environment whilst
|
||||||
// happen further away, but we just don't care about it so much from
|
// updating one chunk and not the neighbours. The "ver near" aspect
|
||||||
// terms of visual impact.
|
// of this is just a cosmetic nicety - exactly the same thing would
|
||||||
if (veryNearCount > 0) {
|
// happen further away, but we just don't care about it so much from
|
||||||
RenderManager.CBuffDeferredModeStart();
|
// 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.
|
||||||
|
permaChunk[index].makeCopyForRebuild(chunk);
|
||||||
|
++index;
|
||||||
}
|
}
|
||||||
// Build this chunk & return false to continue processing
|
LeaveCriticalSection(&m_csDirtyChunks);
|
||||||
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.
|
|
||||||
permaChunk[index].makeCopyForRebuild(chunk);
|
|
||||||
++index;
|
|
||||||
}
|
|
||||||
LeaveCriticalSection(&m_csDirtyChunks);
|
|
||||||
|
|
||||||
--index; // Bring it back into 0 counted range
|
--index; // Bring it back into 0 counted range
|
||||||
|
|
||||||
for (int i = MAX_CHUNK_REBUILD_THREADS - 1; i >= 0; --i) {
|
for (int i = MAX_CHUNK_REBUILD_THREADS - 1; i >= 0; --i) {
|
||||||
// Set the events that won't run
|
// Set the events that won't run
|
||||||
if ((i + 1) > index)
|
if ((i + 1) > index)
|
||||||
s_rebuildCompleteEvents->Set(i);
|
s_rebuildCompleteEvents->Set(i);
|
||||||
else
|
else
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (; index >= 0; --index) {
|
for (; index >= 0; --index) {
|
||||||
|
|
@ -2283,13 +2304,18 @@ bool LevelRenderer::updateDirtyChunks() {
|
||||||
|
|
||||||
// app.DebugPrintf("Rebuilding permaChunk %d\n", index);
|
// app.DebugPrintf("Rebuilding permaChunk %d\n", index);
|
||||||
|
|
||||||
permaChunk[index].rebuild();
|
{
|
||||||
|
FRAME_PROFILE_SCOPE(ChunkRebuildBody);
|
||||||
|
permaChunk[index].rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
if (index != 0)
|
if (index != 0) {
|
||||||
|
FRAME_PROFILE_SCOPE(ChunkRebuildSchedule);
|
||||||
s_rebuildCompleteEvents->Set(
|
s_rebuildCompleteEvents->Set(
|
||||||
index - 1); // MGH - this rebuild happening on the main
|
index - 1); // MGH - this rebuild happening on the main
|
||||||
// thread instead, mark the thread it
|
// thread instead, mark the thread it
|
||||||
// should have been running on as complete
|
// should have been running on as complete
|
||||||
|
}
|
||||||
|
|
||||||
// int64_t endTime = System::currentTimeMillis();
|
// int64_t endTime = System::currentTimeMillis();
|
||||||
// totalTime += (endTime - startTime);
|
// totalTime += (endTime - startTime);
|
||||||
|
|
@ -2300,44 +2326,54 @@ bool LevelRenderer::updateDirtyChunks() {
|
||||||
// 4J Stu - Ignore this path when in constrained mode on Xbox One
|
// 4J Stu - Ignore this path when in constrained mode on Xbox One
|
||||||
else {
|
else {
|
||||||
// Activate thread to rebuild this chunk
|
// Activate thread to rebuild this chunk
|
||||||
|
FRAME_PROFILE_SCOPE(ChunkRebuildSchedule);
|
||||||
s_activationEventA[index - 1]->Set();
|
s_activationEventA[index - 1]->Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the other threads to be done as well
|
// Wait for the other threads to be done as well
|
||||||
s_rebuildCompleteEvents->WaitForAll(INFINITE);
|
{
|
||||||
|
FRAME_PROFILE_SCOPE(ChunkRebuildSchedule);
|
||||||
|
s_rebuildCompleteEvents->WaitForAll(INFINITE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (nearChunk) {
|
if (nearChunk) {
|
||||||
chunk = nearChunk->chunk;
|
chunk = nearChunk->chunk;
|
||||||
PIXBeginNamedEvent(0, "Rebuilding near chunk %d %d %d", chunk->x,
|
PIXBeginNamedEvent(0, "Rebuilding near chunk %d %d %d", chunk->x,
|
||||||
chunk->y, chunk->z);
|
chunk->y, chunk->z);
|
||||||
// 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.
|
|
||||||
static Chunk permaChunk;
|
static Chunk permaChunk;
|
||||||
permaChunk.makeCopyForRebuild(chunk);
|
{
|
||||||
LeaveCriticalSection(&m_csDirtyChunks);
|
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 (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.
|
||||||
|
permaChunk.makeCopyForRebuild(chunk);
|
||||||
|
LeaveCriticalSection(&m_csDirtyChunks);
|
||||||
|
}
|
||||||
// static int64_t totalTime = 0;
|
// static int64_t totalTime = 0;
|
||||||
// static int64_t countTime = 0;
|
// static int64_t countTime = 0;
|
||||||
// int64_t startTime = System::currentTimeMillis();
|
// int64_t startTime = System::currentTimeMillis();
|
||||||
permaChunk.rebuild();
|
{
|
||||||
|
FRAME_PROFILE_SCOPE(ChunkRebuildBody);
|
||||||
|
permaChunk.rebuild();
|
||||||
|
}
|
||||||
// int64_t endTime = System::currentTimeMillis();
|
// int64_t endTime = System::currentTimeMillis();
|
||||||
// totalTime += (endTime - startTime);
|
// totalTime += (endTime - startTime);
|
||||||
// countTime++;
|
// countTime++;
|
||||||
|
|
@ -4102,7 +4138,10 @@ int LevelRenderer::rebuildChunkThreadProc(void* lpParam) {
|
||||||
s_activationEventA[index]->WaitForSignal(INFINITE);
|
s_activationEventA[index]->WaitForSignal(INFINITE);
|
||||||
|
|
||||||
// app.DebugPrintf("Rebuilding permaChunk %d\n", index + 1);
|
// app.DebugPrintf("Rebuilding permaChunk %d\n", index + 1);
|
||||||
permaChunk[index + 1].rebuild();
|
{
|
||||||
|
FRAME_PROFILE_SCOPE(ChunkRebuildBody);
|
||||||
|
permaChunk[index + 1].rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
// Inform the producer thread that we are done with this chunk
|
// Inform the producer thread that we are done with this chunk
|
||||||
s_rebuildCompleteEvents->Set(index);
|
s_rebuildCompleteEvents->Set(index);
|
||||||
|
|
|
||||||
260
Minecraft.Client/Utils/FrameProfiler.cpp
Normal file
260
Minecraft.Client/Utils/FrameProfiler.cpp
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
#include "../Platform/stdafx.h"
|
||||||
|
#include "FrameProfiler.h"
|
||||||
|
|
||||||
|
#ifdef ENABLE_FRAME_PROFILER
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define FRAME_PROFILER_NOINLINE __declspec(noinline)
|
||||||
|
#elif defined(__GNUC__) || defined(__clang__)
|
||||||
|
#define FRAME_PROFILER_NOINLINE __attribute__((noinline))
|
||||||
|
#else
|
||||||
|
#define FRAME_PROFILER_NOINLINE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using FrameProfilerClock = std::chrono::steady_clock;
|
||||||
|
using Bucket = FrameProfiler::Bucket;
|
||||||
|
constexpr std::uint64_t kNsPerMs = 1000ULL * 1000ULL;
|
||||||
|
constexpr std::uint64_t kReportIntervalNs = 1000ULL * 1000ULL * 1000ULL;
|
||||||
|
constexpr std::size_t kBucketCount = FrameProfiler::BucketCount();
|
||||||
|
constexpr auto kFalseTokens = std::to_array<std::string_view>({
|
||||||
|
"0", "false", "False", "FALSE", "no", "No", "NO", "off", "Off", "OFF",
|
||||||
|
});
|
||||||
|
constexpr std::array<FrameProfiler::BucketDescriptor, kBucketCount>
|
||||||
|
kBucketDescriptors = {{
|
||||||
|
{Bucket::Frame, "frame"},
|
||||||
|
{Bucket::World, "world"},
|
||||||
|
{Bucket::Terrain, "terrain"},
|
||||||
|
{Bucket::ChunkCull, "chunkCull"},
|
||||||
|
{Bucket::ChunkCollect, "chunkCollect"},
|
||||||
|
{Bucket::ChunkPlayback, "chunkPlayback"},
|
||||||
|
{Bucket::ChunkDirtyScan, "chunkDirtyScan"},
|
||||||
|
{Bucket::ChunkRebuildSchedule, "chunkRebuildSchedule"},
|
||||||
|
{Bucket::ChunkRebuildBody, "chunkRebuildBody"},
|
||||||
|
{Bucket::Entity, "entities"},
|
||||||
|
{Bucket::Particle, "particles"},
|
||||||
|
{Bucket::WeatherSky, "weather"},
|
||||||
|
{Bucket::UIHud, "ui"},
|
||||||
|
{Bucket::Lightmap, "lightmap"},
|
||||||
|
}};
|
||||||
|
|
||||||
|
struct BucketTotals {
|
||||||
|
std::uint64_t totalNs{};
|
||||||
|
std::uint64_t maxNs{};
|
||||||
|
std::uint64_t calls{};
|
||||||
|
|
||||||
|
void Record(std::uint64_t elapsedNs) noexcept {
|
||||||
|
totalNs += elapsedNs;
|
||||||
|
++calls;
|
||||||
|
if (elapsedNs > maxNs) maxNs = elapsedNs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Merge(const BucketTotals& other) noexcept {
|
||||||
|
totalNs += other.totalNs;
|
||||||
|
calls += other.calls;
|
||||||
|
if (other.maxNs > maxNs) maxNs = other.maxNs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AtomicBucketTotals {
|
||||||
|
std::atomic<std::uint64_t> totalNs{0};
|
||||||
|
std::atomic<std::uint64_t> maxNs{0};
|
||||||
|
std::atomic<std::uint64_t> calls{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProfilerState {
|
||||||
|
std::array<AtomicBucketTotals, kBucketCount> workerBuckets{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ThreadState {
|
||||||
|
std::uint32_t frameScopeDepth{};
|
||||||
|
std::uint64_t windowStartNs{};
|
||||||
|
std::array<BucketTotals, kBucketCount> localBuckets{};
|
||||||
|
};
|
||||||
|
|
||||||
|
constinit ProfilerState g_profilerState{};
|
||||||
|
constinit thread_local ThreadState t_threadState{};
|
||||||
|
|
||||||
|
static_assert(kBucketDescriptors.size() == kBucketCount);
|
||||||
|
|
||||||
|
[[nodiscard]] inline std::uint64_t nowNs() noexcept {
|
||||||
|
return static_cast<std::uint64_t>(
|
||||||
|
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
|
FrameProfilerClock::now().time_since_epoch())
|
||||||
|
.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr double nsToMs(std::uint64_t ns) noexcept {
|
||||||
|
return static_cast<double>(ns) / static_cast<double>(kNsPerMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr bool envSaysDisabled(
|
||||||
|
std::string_view value) noexcept {
|
||||||
|
if (value.empty()) return false;
|
||||||
|
|
||||||
|
for (std::string_view falseToken : kFalseTokens) {
|
||||||
|
if (value == falseToken) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void updateAtomicMax(std::atomic<std::uint64_t>& value,
|
||||||
|
std::uint64_t candidate) noexcept {
|
||||||
|
std::uint64_t current = value.load(std::memory_order_relaxed);
|
||||||
|
while (current < candidate &&
|
||||||
|
!value.compare_exchange_weak(current, candidate,
|
||||||
|
std::memory_order_relaxed,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void recordWorkerBucket(Bucket bucket, std::uint64_t elapsedNs) noexcept {
|
||||||
|
AtomicBucketTotals& state =
|
||||||
|
g_profilerState.workerBuckets[FrameProfiler::BucketIndex(bucket)];
|
||||||
|
state.totalNs.fetch_add(elapsedNs, std::memory_order_relaxed);
|
||||||
|
state.calls.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
updateAtomicMax(state.maxNs, elapsedNs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] inline bool isFrameThread() noexcept {
|
||||||
|
return t_threadState.frameScopeDepth != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FRAME_PROFILER_NOINLINE
|
||||||
|
[[nodiscard]] bool computeEnabled() noexcept {
|
||||||
|
const char* const envValue = std::getenv("C4J_FRAME_PROFILER");
|
||||||
|
if (envValue == nullptr) return true;
|
||||||
|
return !envSaysDisabled(envValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
FRAME_PROFILER_NOINLINE void emitWindowReport(
|
||||||
|
const std::array<BucketTotals, kBucketCount>& buckets) noexcept {
|
||||||
|
const std::uint64_t frames =
|
||||||
|
buckets[FrameProfiler::BucketIndex(Bucket::Frame)].calls;
|
||||||
|
if (frames == 0) return;
|
||||||
|
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[frame-prof] avg/frame(ms) frames=%llu",
|
||||||
|
static_cast<unsigned long long>(frames));
|
||||||
|
for (const auto& descriptor : kBucketDescriptors) {
|
||||||
|
const BucketTotals& bucket =
|
||||||
|
buckets[FrameProfiler::BucketIndex(descriptor.bucket)];
|
||||||
|
const std::string_view label = descriptor.label;
|
||||||
|
std::fprintf(stderr, " %.*s=%.2f", static_cast<int>(label.size()),
|
||||||
|
label.data(), nsToMs(bucket.totalNs) / frames);
|
||||||
|
}
|
||||||
|
std::fputc('\n', stderr);
|
||||||
|
|
||||||
|
std::fputs("[frame-prof] max(ms)/calls", stderr);
|
||||||
|
for (const auto& descriptor : kBucketDescriptors) {
|
||||||
|
const BucketTotals& bucket =
|
||||||
|
buckets[FrameProfiler::BucketIndex(descriptor.bucket)];
|
||||||
|
const std::string_view label = descriptor.label;
|
||||||
|
std::fprintf(stderr, " %.*s=%.2f/%llu",
|
||||||
|
static_cast<int>(label.size()), label.data(),
|
||||||
|
nsToMs(bucket.maxNs),
|
||||||
|
static_cast<unsigned long long>(bucket.calls));
|
||||||
|
}
|
||||||
|
std::fputc('\n', stderr);
|
||||||
|
std::fflush(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::array<BucketTotals, kBucketCount> snapshotAndResetWorkerBuckets()
|
||||||
|
noexcept {
|
||||||
|
std::array<BucketTotals, kBucketCount> snapshot = {};
|
||||||
|
for (std::size_t i = 0; i < kBucketCount; ++i) {
|
||||||
|
AtomicBucketTotals& workerBucket = g_profilerState.workerBuckets[i];
|
||||||
|
snapshot[i].totalNs =
|
||||||
|
workerBucket.totalNs.exchange(0, std::memory_order_relaxed);
|
||||||
|
snapshot[i].maxNs =
|
||||||
|
workerBucket.maxNs.exchange(0, std::memory_order_relaxed);
|
||||||
|
snapshot[i].calls =
|
||||||
|
workerBucket.calls.exchange(0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool FrameProfiler::IsEnabled() noexcept {
|
||||||
|
static const bool enabled = computeEnabled();
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameProfiler::Record(Bucket bucket, std::uint64_t elapsedNs) noexcept {
|
||||||
|
if (isFrameThread()) {
|
||||||
|
t_threadState.localBuckets[BucketIndex(bucket)].Record(elapsedNs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordWorkerBucket(bucket, elapsedNs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameProfiler::EndFrame(std::uint64_t elapsedNs) noexcept {
|
||||||
|
Record(Bucket::Frame, elapsedNs);
|
||||||
|
|
||||||
|
ThreadState& threadState = t_threadState;
|
||||||
|
const std::uint64_t now = nowNs();
|
||||||
|
|
||||||
|
if (threadState.windowStartNs == 0) {
|
||||||
|
threadState.windowStartNs = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((now - threadState.windowStartNs) < kReportIntervalNs) return;
|
||||||
|
|
||||||
|
std::array<BucketTotals, kBucketCount> combined = threadState.localBuckets;
|
||||||
|
const auto workerSnapshot = snapshotAndResetWorkerBuckets();
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < kBucketCount; ++i) {
|
||||||
|
combined[i].Merge(workerSnapshot[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitWindowReport(combined);
|
||||||
|
|
||||||
|
threadState.windowStartNs = now;
|
||||||
|
threadState.localBuckets = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameProfiler::Scope::Scope(Bucket bucket) noexcept
|
||||||
|
: m_startNs(0), m_bucket(bucket), m_enabled(FrameProfiler::IsEnabled()) {
|
||||||
|
if (m_enabled) m_startNs = nowNs();
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameProfiler::Scope::~Scope() noexcept {
|
||||||
|
if (!m_enabled) return;
|
||||||
|
FrameProfiler::Record(m_bucket, nowNs() - m_startNs);
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameProfiler::FrameScope::FrameScope() noexcept
|
||||||
|
: m_startNs(0), m_enabled(false) {
|
||||||
|
if (!FrameProfiler::IsEnabled()) return;
|
||||||
|
|
||||||
|
m_enabled = (t_threadState.frameScopeDepth++ == 0);
|
||||||
|
if (m_enabled) m_startNs = nowNs();
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameProfiler::FrameScope::~FrameScope() noexcept {
|
||||||
|
if (!m_enabled) {
|
||||||
|
if (t_threadState.frameScopeDepth > 0) {
|
||||||
|
--t_threadState.frameScopeDepth;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameProfiler::EndFrame(nowNs() - m_startNs);
|
||||||
|
|
||||||
|
if (t_threadState.frameScopeDepth > 0) {
|
||||||
|
--t_threadState.frameScopeDepth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
93
Minecraft.Client/Utils/FrameProfiler.h
Normal file
93
Minecraft.Client/Utils/FrameProfiler.h
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef ENABLE_FRAME_PROFILER
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
class FrameProfiler {
|
||||||
|
public:
|
||||||
|
enum class Bucket : std::uint8_t {
|
||||||
|
Frame,
|
||||||
|
World,
|
||||||
|
Terrain,
|
||||||
|
ChunkCull,
|
||||||
|
ChunkCollect,
|
||||||
|
ChunkPlayback,
|
||||||
|
ChunkDirtyScan,
|
||||||
|
ChunkRebuildSchedule,
|
||||||
|
ChunkRebuildBody,
|
||||||
|
Entity,
|
||||||
|
Particle,
|
||||||
|
WeatherSky,
|
||||||
|
UIHud,
|
||||||
|
Lightmap,
|
||||||
|
Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BucketDescriptor {
|
||||||
|
Bucket bucket;
|
||||||
|
const char* label;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] static constexpr std::size_t BucketIndex(
|
||||||
|
Bucket bucket) noexcept {
|
||||||
|
return static_cast<std::size_t>(std::to_underlying(bucket));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static constexpr std::size_t BucketCount() noexcept {
|
||||||
|
return BucketIndex(Bucket::Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static bool IsEnabled() noexcept;
|
||||||
|
|
||||||
|
class Scope {
|
||||||
|
public:
|
||||||
|
explicit Scope(Bucket bucket) noexcept;
|
||||||
|
Scope(const Scope&) = delete;
|
||||||
|
Scope& operator=(const Scope&) = delete;
|
||||||
|
Scope(Scope&&) = delete;
|
||||||
|
Scope& operator=(Scope&&) = delete;
|
||||||
|
~Scope() noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::uint64_t m_startNs;
|
||||||
|
Bucket m_bucket;
|
||||||
|
bool m_enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FrameScope {
|
||||||
|
public:
|
||||||
|
FrameScope() noexcept;
|
||||||
|
FrameScope(const FrameScope&) = delete;
|
||||||
|
FrameScope& operator=(const FrameScope&) = delete;
|
||||||
|
FrameScope(FrameScope&&) = delete;
|
||||||
|
FrameScope& operator=(FrameScope&&) = delete;
|
||||||
|
~FrameScope() noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::uint64_t m_startNs;
|
||||||
|
bool m_enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void Record(Bucket bucket, std::uint64_t elapsedNs) noexcept;
|
||||||
|
static void EndFrame(std::uint64_t elapsedNs) noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define FRAME_PROFILE_CONCAT_INNER(a, b) a##b
|
||||||
|
#define FRAME_PROFILE_CONCAT(a, b) FRAME_PROFILE_CONCAT_INNER(a, b)
|
||||||
|
#define FRAME_PROFILE_SCOPE(bucket_name) \
|
||||||
|
FrameProfiler::Scope FRAME_PROFILE_CONCAT(frameProfileScope_, __LINE__)( \
|
||||||
|
FrameProfiler::Bucket::bucket_name)
|
||||||
|
#define FRAME_PROFILE_FRAME_SCOPE() \
|
||||||
|
FrameProfiler::FrameScope FRAME_PROFILE_CONCAT(frameProfileFrameScope_, \
|
||||||
|
__LINE__)
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#define FRAME_PROFILE_SCOPE(bucket_name) ((void)0)
|
||||||
|
#define FRAME_PROFILE_FRAME_SCOPE() ((void)0)
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -80,6 +80,10 @@ if get_option('classic_panorama')
|
||||||
global_cpp_defs += '-DCLASSIC_PANORAMA'
|
global_cpp_defs += '-DCLASSIC_PANORAMA'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if get_option('enable_frame_profiler')
|
||||||
|
global_cpp_defs += ['-DENABLE_FRAME_PROFILER']
|
||||||
|
endif
|
||||||
|
|
||||||
if get_option('ui_backend') == 'shiggy'
|
if get_option('ui_backend') == 'shiggy'
|
||||||
shiggy_dep = dependency(
|
shiggy_dep = dependency(
|
||||||
'shiggy',
|
'shiggy',
|
||||||
|
|
@ -127,4 +131,4 @@ custom_target(
|
||||||
'@OUTPUT@',
|
'@OUTPUT@',
|
||||||
],
|
],
|
||||||
build_by_default: true,
|
build_by_default: true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,11 @@ option(
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
value: true,
|
value: true,
|
||||||
description: 'Toggles V-Sync and adds options to unlock maximum in-game framerate.',
|
description: 'Toggles V-Sync and adds options to unlock maximum in-game framerate.',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
option(
|
||||||
|
'enable_frame_profiler',
|
||||||
|
type: 'boolean',
|
||||||
|
value: false,
|
||||||
|
description: 'Enable the in-engine frame profiler for render hotspot discovery.',
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue