Files
FlaxEngine/Source/Engine/Renderer/Culling/HardwareOcclusionCulling.cpp
T

329 lines
12 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#include "HardwareOcclusionCulling.h"
#include "Engine/Renderer/DrawCall.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/Assets/Shader.h"
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/GPUPass.h"
#include "Engine/Graphics/GPUPipelineState.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderBuffers.h"
#include "Engine/Graphics/RenderContext.h"
#include "Engine/Graphics/Shaders/GPUShader.h"
#include "Engine/Graphics/Shaders/GPUVertexLayout.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerGPU.h"
#include "Engine/Core/Config/GraphicsSettings.h"
#include "Engine/Engine/Engine.h"
HardwareOcclusionCulling::HardwareOcclusionCulling(const SpawnParams& params)
: ScriptingObject(params)
, _vertexBuffer(0, sizeof(Float3), TEXT("HardwareOcclusionCulling.VB"), GPUVertexLayout::Get({ { VertexElement::Types::Position, 0, 0, 0, PixelFormat::R32G32B32_Float } }))
, _shader(Content::LoadAsyncInternal<Shader>(TEXT("Shaders/Utils/Culling")))
{
}
HardwareOcclusionCulling::~HardwareOcclusionCulling()
{
SAFE_DELETE_GPU_RESOURCE(_indexBuffer);
}
void HardwareOcclusionCulling::BeginFrame(const RenderContext& renderContext)
{
PROFILE_CPU();
// Read settings
auto settings = GraphicsSettings::Get();
auto framesCount = Math::Clamp(settings->OcclusionBufferedFrames, 1, MaxFrames);
if (_framesCount != framesCount)
{
// Reset state (graphics backend recycles stale queries)
_framesCount = framesCount;
for (auto& e : _items)
{
Platform::MemoryClear(e.Queries, sizeof(e.Queries) + sizeof(e.Frames));
}
}
_boundsScale = Math::Max(settings->OcclusionBoundsScale, 1.01f);
// Handle origin-relative rendering
_forceUpdateBounds = renderContext.View.Origin != _origin;
_origin = renderContext.View.Origin;
_viewPos = renderContext.View.WorldPosition;
// Skip reading occlusion results on camera cuts (but issue queries for the next frame)
bool forceVisible = renderContext.Task->IsCameraCut;
// Skip reading when view was not rendered for some time (queries might expire)
uint64 engineFrame = Engine::FrameCount;
forceVisible |= (int32)(engineFrame - _lastEngineFrameUsed) >= framesCount;
_lastEngineFrameUsed = engineFrame;
if (forceVisible)
{
// Reset visibility
for (auto& item : _items)
{
item.Occluded = false;
Platform::MemoryClear(item.Queries, sizeof(item.Queries) + sizeof(item.Frames));
}
}
else if (_frameCounter > 0)
{
// Resolve the last buffered frame results (with wait)
PROFILE_CPU_NAMED("Wait for Occlusion Queries");
ZoneColor(TracyWaitZoneColor);
int32 frame = _frameCounter, bufferedFrame = _frameCounter % framesCount, itemsUsed = 0;
auto device = GPUDevice::Instance;
for (auto& item : _items)
{
uint64 query = item.Queries[bufferedFrame];
int32 lag = frame - item.Frames[bufferedFrame];
if (query && lag <= framesCount)
{
// Clear query
item.Queries[bufferedFrame] = 0;
// Read result (occluded object didn't pass any depth test, assume visible if query failed)
uint64 result = 1;
device->GetQueryResult(query, result, true);
item.Occluded = result == 0;
itemsUsed++;
}
else
{
// Maintain state if no new query has been issued (eg. object goes outside frustum or gets hidden)
}
}
ZoneValue(itemsUsed);
}
// Remove used free items
_freeItems.Resize(Math::Max((int32)_freeItemsCount, 0));
#if 0 // TODO: find a different way as there might be some invisible object with CullingId assigned and drawing it later will overlap with reused IDs
// Trim history
constexpr int32 frameTTL = 20;
if (_frameCounter % 10 == 0 && _frameCounter > frameTTL)
{
const int32 lastFrame = _frameCounter - frameTTL;
for (int32 i = 0; i < _items.Count(); i++)
{
auto& item = _items.Get()[i];
if (item.LastUsedFrame && item.LastUsedFrame < lastFrame)
{
Platform::MemoryClear(&item, sizeof(item));
_freeItems.Add(i);
}
}
}
#endif
// Allocate new items (as requested during the previous frame)
if (_newItemsCount > 0)
{
int32 itemsStart = _items.Count(), count = (int32)_newItemsCount, freeStart = _freeItems.Count();
if (itemsStart == 0)
count++; // 0 is invalid for cullingId
_items.AddZeroed(count);
_freeItems.AddUninitialized(count);
for (int32 i = 0; i < count; i++)
_freeItems.Get()[freeStart + i] = itemsStart + i;
if (itemsStart == 0)
_freeItems.RemoveAt(0); // 0 is invalid for cullingId
_newItemsCount = 0;
}
_freeItemsCount = _freeItems.Count();
// Prepare vertex buffer to build geometry bound meshes in async during drawing
_vertexBuffer.Data.Resize(_items.Count() * 8 * sizeof(Float3), true);
_dirtyBounds = 0;
}
void HardwareOcclusionCulling::EndFrame(const RenderContext& renderContext)
{
// Move to the next frame
_frameCounter++;
}
void HardwareOcclusionCulling::Submit(const RenderContext& renderContext)
{
if (_items.IsEmpty() || !_shader || !_shader->IsLoaded())
return;
PROFILE_CPU();
PROFILE_GPU("Occlusion Culling");
GPUContext* context = GPUDevice::Instance->GetMainContext();
// Setup vertex and index buffers
if (!_indexBuffer)
{
const uint16 cubeIndices[12 * 3] =
{
0, 2, 3,
0, 3, 1,
4, 5, 7,
4, 7, 6,
0, 1, 5,
0, 5, 4,
2, 6, 7,
2, 7, 3,
0, 4, 6,
0, 6, 2,
1, 3, 7,
1, 7, 5,
};
auto desc = GPUBufferDescription::Index(sizeof(uint16), ARRAY_COUNT(cubeIndices), cubeIndices);
_indexBuffer = GPUDevice::Instance->CreateBuffer(TEXT("HardwareOcclusionCulling.IB"));
_indexBuffer->Init(desc);
}
if (_dirtyBounds)
_vertexBuffer.Flush(context);
auto vb = _vertexBuffer.GetBuffer();
// Use depth-only for testing visibility
GPUDrawPass pass(context, *renderContext.Buffers->DepthBuffer, GPUDrawPassAction::Load, Span<GPUTextureView*>(), Span<GPUDrawPassAction>());
if (!_pso)
{
_pso = GPUDevice::Instance->CreatePipelineState();
auto desc = GPUPipelineState::Description::Default;
desc.DepthWriteEnable = false;
desc.DepthClipEnable = false;
desc.DepthFunc = ComparisonFunc::DefaultEqual;
desc.StencilEnable = false;
desc.CullMode = CullMode::Inverted;
desc.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::None;
desc.VS = _shader->GPU->GetVS("VS_HardwareOcclusionCulling");
if (_pso->Init(desc))
return;
#if COMPILE_WITH_DEV_ENV
_shader->Reloading.Bind<HardwareOcclusionCulling, &HardwareOcclusionCulling::OnShaderReloading>(this);
#endif
}
auto cb = _shader->GPU->GetCB(0);
{
Matrix viewProjectionMatrix;
Matrix::Transpose(renderContext.View.ViewProjection(), viewProjectionMatrix);
context->UpdateCB(cb, &viewProjectionMatrix);
}
context->BindCB(0, cb);
context->BindIB(_indexBuffer);
context->BindVB(Span<GPUBuffer*>(&vb, 1));
context->SetState(_pso);
#if COMPILE_WITH_PROFILER
auto stats = RenderStatsData::Counter;
#endif
// Issue occlusion queries
int32 frame = _frameCounter, bufferedFrame = _frameCounter % _framesCount, itemsCount = _items.Count(), itemsUsed = 0;
auto frustum = renderContext.View.Frustum;
auto* items = _items.Get();
for (int32 i = 0; i < itemsCount; i++)
{
auto& item = items[i];
if (item.LastUsedFrame != frame || frustum.Contains(item.Bounds) == ContainmentType::Disjoint)
continue;
itemsUsed++;
// Begin occlusion query for this object index
uint64 query = context->BeginQuery(GPUQueryType::BinaryOcclusion);
item.Queries[bufferedFrame] = query;
item.Frames[bufferedFrame] = frame;
// Draw the low-poly bounds of that object
context->DrawIndexed(12 * 3, i * 8);
// End occlusion query
context->EndQuery(query);
}
ZoneValue(itemsUsed);
#if COMPILE_WITH_PROFILER
// Cancel-out any draw stats from profiler (hidden draws)
RenderStatsData::Counter = stats;
#endif
}
bool HardwareOcclusionCulling::IsVisible(const BoundingBox& bounds, uint32& cullingId)
{
return IsVisible(bounds, cullingId, nullptr);
}
bool HardwareOcclusionCulling::IsVisible(const BoundingBox& bounds, GeometryDrawState& drawState)
{
return IsVisible(bounds, drawState.CullingId, &drawState);
}
bool HardwareOcclusionCulling::IsVisible(BoundingBox bounds, uint32& cullingId, GeometryDrawState* drawState)
{
// Enlarge bounds to reduce popping
bounds = BoundingBox::MakeScaled(bounds, _boundsScale);
// TODO: use camera motion to enlarge bounds
// TODO: use object motion (from prev frame world matrix) to enlarge bounds in the direction of movement to reduce popping
// Assume visible when view is right inside the bounds
if (bounds.Contains(_viewPos) == ContainmentType::Contains)
return true;
// Check if object doesn't have ID assigned yet
if (cullingId == 0 || cullingId >= (uint32)_items.Count())
{
int64 freeIndex = Platform::InterlockedDecrement(&_freeItemsCount);
if (freeIndex >= 0)
{
// Use the ID from the free list
ASSERT_LOW_LAYER(freeIndex < _freeItems.Count());
cullingId = _freeItems.Get()[freeIndex];
}
else
{
// Count space needed to contain all objects (for the next frame)
Platform::InterlockedIncrement(&_newItemsCount);
return true;
}
}
// Update item bounds
auto& item = _items.Get()[cullingId];
if (item.Bounds != bounds || _forceUpdateBounds)
{
// Update bounds
item.Bounds = bounds;
// Write to the vertex buffer
Float3 boxMin = bounds.Minimum - _origin;
Float3 boxMax = bounds.Maximum - _origin;
ASSERT_LOW_LAYER(_vertexBuffer.Data.Count() >= (8 * sizeof(Float3)) * (cullingId + 1));
Float3* vertices = (Float3*)(_vertexBuffer.Data.Get() + (8 * sizeof(Float3)) * cullingId);
vertices[0] = boxMin;
vertices[1] = Float3(boxMin.X, boxMin.Y, boxMax.Z);
vertices[2] = Float3(boxMin.X, boxMax.Y, boxMin.Z);
vertices[3] = Float3(boxMin.X, boxMax.Y, boxMax.Z);
vertices[4] = Float3(boxMax.X, boxMin.Y, boxMin.Z);
vertices[5] = Float3(boxMax.X, boxMin.Y, boxMax.Z);
vertices[6] = Float3(boxMax.X, boxMax.Y, boxMin.Z);
vertices[7] = boxMax;
Platform::InterlockedIncrement(&_dirtyBounds);
}
// Force visible when object was not rendered last frame (eg. outside the frustum)
if (_frameCounter - item.LastUsedFrame > 1)
{
item.Occluded = false;
}
item.LastUsedFrame = _frameCounter;
// Read occlusion result
return !item.Occluded;
}
#if COMPILE_WITH_DEV_ENV
void HardwareOcclusionCulling::OnShaderReloading(Asset* obj)
{
SAFE_DELETE_GPU_RESOURCE(_pso);
}
#endif