Optimize Global Surface Atlas defragmentation with copying old atlas to avoid redrawing all objects

This commit is contained in:
2026-04-29 17:19:40 +02:00
parent d4da1d80d0
commit 61431a6400
4 changed files with 191 additions and 18 deletions
Binary file not shown.
@@ -44,6 +44,7 @@
#define GLOBAL_SURFACE_ATLAS_DEBUG_DRAW_CHUNKS 0 // Debug draws culled chunks bounds (non-empty)
#define GLOBAL_SURFACE_ATLAS_MAX_NEW_OBJECTS_PER_FRAME 500 // Limits the amount of newly added objects to atlas per-frame to reduce hitches on 1st frame or camera-cut
#define GLOBAL_SURFACE_ATLAS_DIRTY_FRAMES(flags) (EnumHasAnyFlags(flags, StaticFlags::Lightmap) ? 200 : 10) // Amount of frames after which update object (less frequent updates for static scenes)
#define GLOBAL_SURFACE_ATLAS_DEFRAG_COPY 1 // Copies existing atlas contents after defragmentation to avoid dirtying objects
#if GLOBAL_SURFACE_ATLAS_DEBUG_DRAW_OBJECTS || GLOBAL_SURFACE_ATLAS_DEBUG_DRAW_CHUNKS
#include "Engine/Debug/DebugDraw.h"
@@ -138,6 +139,18 @@ struct GlobalSurfaceAtlasObject
POD_COPYABLE(GlobalSurfaceAtlasObject);
};
struct GlobalSurfaceAtlasObjectDefragmentation
{
void* ActorObject;
struct Tile
{
uint16 X;
uint16 Y;
uint16 Width;
uint16 Height;
} Tiles[6];
};
struct GlobalSurfaceAtlasLight
{
uint64 LastFrameUsed = 0;
@@ -170,6 +183,10 @@ public:
Dictionary<Guid, GlobalSurfaceAtlasLight> Lights;
SamplesBuffer<uint32, 30> CulledObjectsUsageHistory;
Array<GlobalSurfaceAtlasFreeShaderSlot> FreeObjectsBufferSlots[6]; // Bin for each tile count for quick reusage
#if GLOBAL_SURFACE_ATLAS_DEFRAG_COPY
Array<GlobalSurfaceAtlasObjectDefragmentation> ObjectsDefragmentation;
int32 ObjectsDefragmentationTilesCount = 0;
#endif
// Cached data to be reused during RasterizeActor
Array<void*> DirtyObjectsBuffer;
@@ -292,10 +309,35 @@ public:
(float)AtlasPixelsUsed / AtlasPixelsTotal < maxUsageToDefrag)
{
PROFILE_CPU_NAMED("Defragment Atlas");
// Destroy atlas to recreate it without fragmentation but cache existing atlas structure to copy existing tiles back to the new atlas to reduce amount of dirty tiles for redraw
LastFrameAtlasDefragmentation = Engine::FrameCount;
#if GLOBAL_SURFACE_ATLAS_DEFRAG_COPY
ObjectsDefragmentation.Clear();
ObjectsDefragmentation.Resize(Objects.Count());
ObjectsDefragmentationTilesCount = 0;
#endif
int32 i = 0;
for (auto& e : Objects)
{
auto& object = e.Value;
#if GLOBAL_SURFACE_ATLAS_DEFRAG_COPY
// Cache object tiles positions in atlas to copy them back after defragmentation to avoid dirtying all objects
auto& defrag = ObjectsDefragmentation[i++];
defrag.ActorObject = e.Key;
Platform::MemoryClear(defrag.Tiles, sizeof(defrag.Tiles));
for (int32 tileIndex = 0; tileIndex < 6; tileIndex++)
{
if (object.Tiles[tileIndex])
{
Platform::MemoryCopy(&defrag.Tiles[tileIndex].X, &object.Tiles[tileIndex]->X, sizeof(GlobalSurfaceAtlasTile::Size) * 4);
ObjectsDefragmentationTilesCount++;
}
}
#endif
// Free atlas tiles and data slot
Platform::MemoryClear(object.Tiles, sizeof(object.Tiles));
if (object.ObjectDataAddress.TilesCount != 0)
{
@@ -459,6 +501,7 @@ public:
auto objectsListData = (uint32*)ObjectsListBuffer.Data.Get();
int32 dirtyTiles = 0, objectIndex = 0;
int32 dirtyObjectsLimitLeft = 50; // TODO: expose as scalability parameter
// TODO: sort dirt objects by size to collect biggest ones first
for (auto& e : Objects)
{
auto& object = e.Value;
@@ -699,13 +742,20 @@ bool GlobalSurfaceAtlasPass::setupResources()
if (_psDebug1->Init(psDesc))
return true;
}
psDesc.DepthEnable = true;
psDesc.DepthWriteEnable = true;
psDesc.DepthFunc = ComparisonFunc::Always;
psDesc.VS = shader->GetVS("VS_Atlas");
if (!_psCopy)
{
_psCopy = device->CreatePipelineState();
psDesc.PS = shader->GetPS("PS_Copy");
if (_psCopy->Init(psDesc))
return true;
}
if (!_psClear)
{
_psClear = device->CreatePipelineState();
psDesc.DepthEnable = true;
psDesc.DepthWriteEnable = true;
psDesc.DepthFunc = ComparisonFunc::Always;
psDesc.VS = shader->GetVS("VS_Atlas");
psDesc.PS = shader->GetPS("PS_Clear");
if (_psClear->Init(psDesc))
return true;
@@ -716,7 +766,6 @@ bool GlobalSurfaceAtlasPass::setupResources()
if (!_psClearLighting)
{
_psClearLighting = device->CreatePipelineState();
psDesc.VS = shader->GetVS("VS_Atlas");
psDesc.PS = shader->GetPS("PS_ClearLighting");
if (_psClearLighting->Init(psDesc))
return true;
@@ -746,6 +795,7 @@ bool GlobalSurfaceAtlasPass::setupResources()
void GlobalSurfaceAtlasPass::OnShaderReloading(Asset* obj)
{
SAFE_DELETE_GPU_RESOURCE(_psCopy);
SAFE_DELETE_GPU_RESOURCE(_psClear);
SAFE_DELETE_GPU_RESOURCE(_psClearLighting);
SAFE_DELETE_GPU_RESOURCE(_psDirectLighting0);
@@ -767,6 +817,7 @@ void GlobalSurfaceAtlasPass::Dispose()
SAFE_DELETE_GPU_RESOURCE(_culledObjectsSizeBuffer);
SAFE_DELETE_GPU_RESOURCE(_psClear);
SAFE_DELETE_GPU_RESOURCE(_psClearLighting);
SAFE_DELETE_GPU_RESOURCE(_psCopy);
SAFE_DELETE_GPU_RESOURCE(_psDirectLighting0);
SAFE_DELETE_GPU_RESOURCE(_psDirectLighting1);
SAFE_DELETE_GPU_RESOURCE(_psIndirectLighting);
@@ -837,8 +888,8 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co
const float resolutionInv = 1.0f / (float)resolution;
// Initialize buffers
bool noCache = surfaceAtlasData.Resolution != resolution;
if (noCache)
bool atlasResized = surfaceAtlasData.Resolution != resolution;
if (atlasResized)
{
surfaceAtlasData.Reset();
surfaceAtlasData.Atlas.Init(resolution, resolution);
@@ -869,6 +920,8 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co
memUsage += surfaceAtlasData.ChunksBuffer->GetMemoryUsage();
}
LOG(Info, "Global Surface Atlas resolution: {0}, memory usage: {1} MB", resolution, memUsage / (1024 * 1024));
context->Clear(surfaceAtlasData.AtlasLighting->View(), Color::Transparent);
}
for (SceneRendering* scene : renderContext.List->Scenes)
surfaceAtlasData.ListenSceneRendering(scene);
@@ -894,12 +947,12 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co
Float2 minPos((float)tile->X, (float)tile->Y), maxPos((float)(tile->X + tile->Width), (float)(tile->Y + tile->Height)); \
Half2 min(minPos * posToClipMul + posToClipAdd), max(maxPos * posToClipMul + posToClipAdd); \
auto* quad = _vertexBuffer->WriteReserve<AtlasTileVertex>(6); \
quad[0].Position = max; \
quad[1].Position = { min.X, max.Y }; \
quad[2].Position = min; \
quad[3].Position = quad[2].Position; \
quad[4].Position = { max.X, min.Y }; \
quad[5].Position = quad[0].Position
quad[0] = { { max }, Half2::Zero, 0 }; \
quad[1] = { { min.X, max.Y }, Half2::Zero, 0 }; \
quad[2] = { { min }, Half2::Zero, 0 }; \
quad[3] = quad[2]; \
quad[4] = { { max.X, min.Y }, Half2::Zero, 0 }; \
quad[5] = quad[0]
#define VB_WRITE_TILE(tile) \
Float2 minPos((float)tile->X, (float)tile->Y), maxPos((float)(tile->X + tile->Width), (float)(tile->Y + tile->Height)); \
Half2 min(minPos * posToClipMul + posToClipAdd), max(maxPos * posToClipMul + posToClipAdd); \
@@ -917,11 +970,102 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co
context->BindVB(ToSpan(&vb, 1)); \
context->DrawInstanced(_vertexBuffer->Data.Count() / sizeof(AtlasTileVertex), 1);
#if GLOBAL_SURFACE_ATLAS_DEFRAG_COPY
// When performing atlas defragmentation, preserve the previous atlas to copy tiles from it (to avoid redrawing them)
bool atlasDefrag = !atlasResized && surfaceAtlasData.LastFrameAtlasDefragmentation == currentFrame && surfaceAtlasData.ObjectsDefragmentation.HasItems();
if (atlasDefrag)
{
PROFILE_GPU_CPU_NAMED("Defragment");
// TODO: atlas copy maybe could be done in separate pass for each surface if they could alias the same memory chunk (eg. in Vulkan/D3D12)
// Allocate a new atlas textures to copy data from the old ones
#define INIT_ATLAS_TEXTURE(texture) GPUTexture* defrag##texture = surfaceAtlasData.texture; surfaceAtlasData.texture = RenderTargetPool::Get(surfaceAtlasData.texture->GetDescription()); RENDER_TARGET_POOL_SET_NAME(surfaceAtlasData.texture, "GlobalSurfaceAtlas." #texture);
INIT_ATLAS_TEXTURE(AtlasDepth);
INIT_ATLAS_TEXTURE(AtlasEmissive);
INIT_ATLAS_TEXTURE(AtlasGBuffer0);
INIT_ATLAS_TEXTURE(AtlasGBuffer1);
INIT_ATLAS_TEXTURE(AtlasLighting);
#undef INIT_ATLAS_TEXTURE
// Bind and clear outputs
// TODO: convert into GPUDrawPass maybe
GPUTextureView* depthBuffer = surfaceAtlasData.AtlasDepth->View();
GPUTextureView* targetBuffers[4] =
{
surfaceAtlasData.AtlasEmissive->View(),
surfaceAtlasData.AtlasGBuffer0->View(),
surfaceAtlasData.AtlasGBuffer1->View(),
surfaceAtlasData.AtlasLighting->View(),
};
context->SetRenderTarget(depthBuffer, ToSpan(targetBuffers, ARRAY_COUNT(targetBuffers)));
context->ClearDepth(depthBuffer);
context->Clear(targetBuffers[0], Color::Transparent);
context->Clear(targetBuffers[1], Color::Transparent);
context->Clear(targetBuffers[2], Color::Transparent);
context->Clear(targetBuffers[3], Color::Transparent);
// Copy old atlas tiles into the new atlas tiles
_vertexBuffer->Clear();
_vertexBuffer->Data.EnsureCapacity(surfaceAtlasData.ObjectsDefragmentationTilesCount * sizeof(AtlasTileVertex));
for (auto& e : surfaceAtlasData.ObjectsDefragmentation)
{
const GlobalSurfaceAtlasObject* objectPtr = surfaceAtlasData.Objects.TryGet(e.ActorObject);
if (!objectPtr)
continue;
const GlobalSurfaceAtlasObject& object = *objectPtr;
for (int32 tileIndex = 0; tileIndex < 6; tileIndex++)
{
auto* tile = object.Tiles[tileIndex];
if (!tile)
continue;
VB_WRITE_TILE_POS_ONLY(tile);
// Write old atlas UVs of this tile
auto defragTile = e.Tiles[tileIndex];
Half2 minUV(defragTile.X * resolutionInv, defragTile.Y * resolutionInv);
Half2 maxUV((defragTile.X + defragTile.Width) * resolutionInv, (defragTile.Y + defragTile.Height) * resolutionInv);
quad[0].TileUV = maxUV;
quad[1].TileUV = Half2(minUV.X, maxUV.Y);
quad[2].TileUV = minUV;
quad[3].TileUV = minUV;
quad[4].TileUV = Half2(maxUV.X, minUV.Y);
quad[5].TileUV = maxUV;
}
// Don't redraw this object if all tiles were restored from old atlas
object.Dirty = false;
surfaceAtlasData.DirtyObjectsBuffer.Remove(e.ActorObject);
}
surfaceAtlasData.ObjectsDefragmentation.Clear();
context->BindSR(0, defragAtlasDepth->View());
context->BindSR(1, defragAtlasEmissive->View());
context->BindSR(2, defragAtlasGBuffer0->View());
context->BindSR(3, defragAtlasGBuffer1->View());
context->BindSR(4, defragAtlasLighting->View());
context->SetState(_psCopy);
context->SetViewportAndScissors(Viewport(0, 0, (float)resolution, (float)resolution));
VB_DRAW();
// Free old atlas textures
context->ResetRenderTarget();
context->ResetSR();
context->FlushState();
RenderTargetPool::Release(defragAtlasLighting);
RenderTargetPool::Release(defragAtlasGBuffer1);
RenderTargetPool::Release(defragAtlasGBuffer0);
RenderTargetPool::Release(defragAtlasEmissive);
RenderTargetPool::Release(defragAtlasDepth);
}
#else
constexpr bool atlasDefrag = false;
#endif
// Rasterize world geometry material properties into Global Surface Atlas
if (surfaceAtlasData.DirtyObjectsBuffer.Count() != 0)
if (surfaceAtlasData.DirtyObjectsBuffer.HasItems())
{
PROFILE_GPU_CPU_NAMED("Rasterize Tiles");
// Init rendering context
RenderContext renderContextTiles = renderContext;
renderContextTiles.List = RenderList::GetFromPool();
renderContextTiles.View.Pass = DrawPass::GBuffer | DrawPass::GlobalSurfaceAtlas;
@@ -932,6 +1076,7 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co
renderContextTiles.View.Near = 0.0f;
renderContextTiles.View.Prepare(renderContextTiles);
// Bind render targets for materials and clear them (whole or just dirty tiles)
GPUTextureView* depthBuffer = surfaceAtlasData.AtlasDepth->View();
GPUTextureView* targetBuffers[3] =
{
@@ -940,9 +1085,10 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co
surfaceAtlasData.AtlasGBuffer1->View(),
};
context->SetRenderTarget(depthBuffer, ToSpan(targetBuffers, ARRAY_COUNT(targetBuffers)));
if (!atlasDefrag)
{
PROFILE_GPU_CPU_NAMED("Clear");
if (noCache || GLOBAL_SURFACE_ATLAS_DEBUG_FORCE_REDRAW_TILES || surfaceAtlasData.LastFrameAtlasDefragmentation == currentFrame)
if (atlasResized || GLOBAL_SURFACE_ATLAS_DEBUG_FORCE_REDRAW_TILES)
{
// Full-atlas hardware clear
context->ClearDepth(depthBuffer);
@@ -974,6 +1120,8 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co
VB_DRAW();
}
}
// Draw all dirty tiles
auto& drawCallsListGBuffer = renderContextTiles.List->DrawCallsLists[(int32)DrawCallsListType::GBuffer];
auto& drawCallsListGBufferNoDecals = renderContextTiles.List->DrawCallsLists[(int32)DrawCallsListType::GBufferNoDecals];
drawCallsListGBuffer.CanUseInstancing = false;
@@ -1036,6 +1184,8 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co
}
}
ZoneValue(tilesDrawn);
// Cleanup
context->ResetRenderTarget();
RenderList::ReturnToPool(renderContextTiles.List);
}
@@ -43,6 +43,7 @@ public:
private:
bool _supported = false;
AssetReference<Shader> _shader;
GPUPipelineState* _psCopy = nullptr;
GPUPipelineState* _psClear = nullptr;
GPUPipelineState* _psClearLighting = nullptr;
GPUPipelineState* _psDirectLighting0 = nullptr;
+23 -1
View File
@@ -49,6 +49,28 @@ AtlasVertexOutput VS_Atlas(AtlasVertexInput input)
return output;
}
#ifdef _PS_Copy
Texture2D DepthTexture : register(t0);
Texture2D EmissiveTexture : register(t1);
Texture2D GBuffer0Texture : register(t2);
Texture2D GBuffer1Texture : register(t3);
Texture2D LightingTexture : register(t4);
// Pixel shader for Global Surface Atlas copying (eg. after defragmentation)
META_PS(true, FEATURE_LEVEL_SM5)
void PS_Copy(AtlasVertexOutput input, out float Depth : SV_Depth, out float4 Emissive : SV_Target0, out float4 GBuffer0 : SV_Target1, out float4 GBuffer1 : SV_Target2, out float4 Lighting : SV_Target3)
{
float2 atlasUV = input.TileUV; // Old-atlas UVs computed on CPU
Depth = SAMPLE_RT_DEPTH(DepthTexture, atlasUV);
Emissive = SAMPLE_RT(EmissiveTexture, atlasUV);
GBuffer0 = SAMPLE_RT(GBuffer0Texture, atlasUV);
GBuffer1 = SAMPLE_RT(GBuffer1Texture, atlasUV);
Lighting = SAMPLE_RT(LightingTexture, atlasUV);
}
#endif
// Pixel shader for Global Surface Atlas software clearing
META_PS(true, FEATURE_LEVEL_SM5)
void PS_Clear(out float4 Light : SV_Target0, out float4 RT0 : SV_Target1, out float4 RT1 : SV_Target2, out float4 RT2 : SV_Target3)
@@ -64,7 +86,7 @@ void PS_Clear(out float4 Light : SV_Target0, out float4 RT0 : SV_Target1, out fl
Buffer<float4> GlobalSurfaceAtlasObjects : register(t4);
Texture2D Texture : register(t7);
// Pixel shader for Global Surface Atlas clearing
// Pixel shader for Global Surface Atlas clearing (copies emissive texture)
META_PS(true, FEATURE_LEVEL_SM5)
float4 PS_ClearLighting(AtlasVertexOutput input) : SV_Target
{