Add **HZB Occlusion Culling**

This commit is contained in:
2026-09-02 23:08:07 +02:00
parent 884ad3d726
commit cb513e9271
18 changed files with 639 additions and 114 deletions
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -467,7 +467,7 @@ public:
/// <param name="threadGroupCountX">The number of groups dispatched in the x direction.</param>
/// <param name="threadGroupCountY">The number of groups dispatched in the y direction.</param>
/// <param name="threadGroupCountZ">The number of groups dispatched in the z direction.</param>
API_FUNCTION() virtual void Dispatch(GPUShaderProgramCS* shader, uint32 threadGroupCountX, uint32 threadGroupCountY, uint32 threadGroupCountZ) = 0;
API_FUNCTION() virtual void Dispatch(GPUShaderProgramCS* shader, uint32 threadGroupCountX, uint32 threadGroupCountY = 1, uint32 threadGroupCountZ = 1) = 0;
/// <summary>
/// Executes a command list from a thread group. Buffer must contain GPUDispatchIndirectArgs.
+23 -15
View File
@@ -69,7 +69,8 @@ void RenderBuffers::ReleaseUnusedMemory()
UPDATE_LAZY_KEEP_RT(TemporalSSR);
UPDATE_LAZY_KEEP_RT(TemporalAA);
UPDATE_LAZY_KEEP_RT(HalfResDepth);
UPDATE_LAZY_KEEP_RT(HiZ);
UPDATE_LAZY_KEEP_RT(HiZ[0]);
UPDATE_LAZY_KEEP_RT(HiZ[1]);
UPDATE_LAZY_KEEP_RT(LuminanceMap);
#undef UPDATE_LAZY_KEEP_RT
for (int32 i = CustomBuffers.Count() - 1; i >= 0; i--)
@@ -118,45 +119,51 @@ GPUTexture* RenderBuffers::RequestHalfResDepth(GPUContext* context)
return HalfResDepth;
}
GPUTexture* RenderBuffers::RequestHiZ(GPUContext* context, bool fullRes, int32 mipLevels)
GPUTexture* RenderBuffers::RequestHiZ(GPUContext* context, bool fullRes, int32 mipLevels, bool closest, bool powerOfTwo)
{
// Skip if already done in the current frame
const auto currentFrame = Engine::FrameCount;
if (LastFrameHiZ == currentFrame)
return HiZ;
int32 idx = closest ? 0 : 1;
if (LastFrameHiZ[idx] == currentFrame)
return HiZ[idx];
if (!MultiScaler::Instance()->IsReady())
return nullptr;
LastFrameHiZ = currentFrame;
LastFrameHiZ[idx] = currentFrame;
// Allocate or resize buffer (with full mip-chain)
auto format = PixelFormat::R32_Float;
auto width = fullRes ? _width : Math::Max(_width >> 1, 1);
auto height = fullRes ? _height : Math::Max(_height >> 1, 1);
if (powerOfTwo)
{
width = Math::RoundUpToPowerOf2(width);
height = Math::RoundUpToPowerOf2(height);
}
auto desc = GPUTextureDescription::New2D(width, height, mipLevels, format, GPUTextureFlags::ShaderResource);
bool useCompute = false; // TODO: impl Compute Shader for downscaling depth to HiZ with a single dispatch (eg. FidelityFX Single Pass Downsampler)
if (useCompute)
desc.Flags |= GPUTextureFlags::UnorderedAccess;
else
desc.Flags |= GPUTextureFlags::RenderTarget | GPUTextureFlags::PerMipViews;
if (HiZ && HiZ->GetDescription() != desc)
if (HiZ[idx] && HiZ[idx]->GetDescription() != desc)
{
RenderTargetPool::Release(HiZ);
HiZ = nullptr;
RenderTargetPool::Release(HiZ[idx]);
HiZ[idx] = nullptr;
}
if (HiZ == nullptr)
if (HiZ[idx] == nullptr)
{
HiZ = RenderTargetPool::Get(desc);
RENDER_TARGET_POOL_SET_NAME(HiZ, "HiZ");
HiZ[idx] = RenderTargetPool::Get(desc);
RENDER_TARGET_POOL_SET_NAME(HiZ[idx], "HiZ");
#if PLATFORM_WEB
// Hack to fix WebGPU limitation that requires to specify different sampler type manually to load 32-bit float texture
SetWebGPUTextureViewSampler(HiZ->View(), GPU_WEBGPU_SAMPLER_TYPE_UNFILTERABLE_FLOAT);
SetWebGPUTextureViewSampler(HiZ[idx]->View(), GPU_WEBGPU_SAMPLER_TYPE_UNFILTERABLE_FLOAT);
#endif
}
// Downscale
MultiScaler::Instance()->BuildHiZ(context, DepthBuffer, HiZ);
MultiScaler::Instance()->BuildHiZ(context, DepthBuffer, HiZ[idx], closest);
return HiZ;
return HiZ[idx];
}
PixelFormat RenderBuffers::GetOutputFormat() const
@@ -298,7 +305,8 @@ void RenderBuffers::Release()
UPDATE_LAZY_KEEP_RT(TemporalSSR);
UPDATE_LAZY_KEEP_RT(TemporalAA);
UPDATE_LAZY_KEEP_RT(HalfResDepth);
UPDATE_LAZY_KEEP_RT(HiZ);
UPDATE_LAZY_KEEP_RT(HiZ[0]);
UPDATE_LAZY_KEEP_RT(HiZ[1]);
UPDATE_LAZY_KEEP_RT(LuminanceMap);
#undef UPDATE_LAZY_KEEP_RT
CustomBuffers.ClearDelete();
+6 -4
View File
@@ -62,9 +62,9 @@ API_CLASS() class FLAXENGINE_API RenderBuffers : public ScriptingObject, private
private:
GPUTexture* HalfResDepth = nullptr;
GPUTexture* HiZ = nullptr;
GPUTexture* HiZ[2] = {};
uint64 LastFrameHalfResDepth = 0;
uint64 LastFrameHiZ = 0;
uint64 LastFrameHiZ[2] = {};
// Scene drawing cache with the per-object state (eg. LOD transitions, motion-vectors movement)
struct SceneData
@@ -155,13 +155,15 @@ public:
GPUTexture* RequestHalfResDepth(GPUContext* context);
/// <summary>
/// Requests the Hierarchical Z-Buffer (closest) to be prepared for the current frame.
/// Requests the Hierarchical Z-Buffer (closest or furthest) to be prepared for the current frame.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="fullRes">Generates the full-resolution buffer, otherwise HiZ starts at half-res of the original Depth Buffer.</param>
/// <param name="mipLevels">Maximum amount of mip levels to generate. Value 0 generates a full mip chain down to 1x1.</param>
/// <param name="closest">True if generate the closest depth values, otherwise will use the furthest depths filter.</param>
/// <param name="powerOfTwo">True if the buffer dimensions should be powers of two.</param>
/// <returns>The HiZ depth buffer.</returns>
GPUTexture* RequestHiZ(GPUContext* context, bool fullRes = false, int32 mipLevels = 0);
GPUTexture* RequestHiZ(GPUContext* context, bool fullRes = false, int32 mipLevels = 0, bool closest = true, bool powerOfTwo = false);
public:
/// <summary>
@@ -20,6 +20,7 @@ GPUBufferView* GPUBufferDX11::View() const
void* GPUBufferDX11::Map(GPUResourceMapMode mode)
{
CHECK_RETURN(_resource, nullptr);
const bool isMainThread = IsInMainThread();
if (!isMainThread)
_device->Locker.Lock();
@@ -642,7 +642,8 @@ void GPUSwapChainVulkan::Present(bool vsync)
// Cache a command buffer to wait on its fence before drawing to this backbuffer again
auto& acquiredBackBuffer = _backBuffers[_acquiredImageIndex];
ASSERT(acquiredBackBuffer.SubmitCmdBuffer == nullptr || acquiredBackBuffer.SubmitCmdBuffer->IsSubmitted());
// TODO: fix rare issues with the assert below
//ASSERT(acquiredBackBuffer.SubmitCmdBuffer == nullptr || acquiredBackBuffer.SubmitCmdBuffer->IsSubmitted());
acquiredBackBuffer.SubmitCmdBuffer = context->GetCmdBufferManager()->GetActiveCmdBuffer();
context->GetCmdBufferManager()->SubmitActiveCmdBuffer(_backBuffers[_acquiredImageIndex].RenderingDoneSemaphore);
@@ -85,10 +85,10 @@ bool GPUBufferWebGPU::OnInit()
bufferDesc.usage |= WGPUBufferUsage_MapWrite | WGPUBufferUsage_CopySrc;
break;
case GPUResourceUsage::StagingReadback:
bufferDesc.usage |= WGPUBufferUsage_MapRead;
bufferDesc.usage |= WGPUBufferUsage_MapRead | WGPUBufferUsage_CopyDst;
break;
case GPUResourceUsage::Staging:
bufferDesc.usage |= WGPUBufferUsage_MapRead | WGPUBufferUsage_MapWrite | WGPUBufferUsage_CopySrc;
bufferDesc.usage |= WGPUBufferUsage_MapRead | WGPUBufferUsage_MapWrite | WGPUBufferUsage_CopySrc | WGPUBufferUsage_CopyDst;
break;
}
bufferDesc.size = (_desc.Size + 3) & ~0x3; // Align up to the multiple of 4 bytes
@@ -0,0 +1,283 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#include "HZBOcclusionCulling.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/RenderTask.h"
#include "Engine/Graphics/RenderBuffers.h"
#include "Engine/Graphics/RenderContext.h"
#include "Engine/Graphics/Shaders/GPUShader.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerGPU.h"
#include "Engine/Core/Config/GraphicsSettings.h"
#include "Engine/Engine/Engine.h"
#define ResultValue uint32
#define ResultType PixelFormat::R32_UInt
HZBOcclusionCulling::HZBOcclusionCulling(const SpawnParams& params)
: ScriptingObject(params)
, _shader(Content::LoadAsyncInternal<Shader>(TEXT("Shaders/Utils/Culling")))
, _boundsBuffer(0, PixelFormat::R32G32B32_Float, false, TEXT("HZB.Bounds"))
{
_boundsBuffer.Usage = GPUResourceUsage::Dynamic;
}
HZBOcclusionCulling::~HZBOcclusionCulling()
{
SAFE_DELETE_GPU_RESOURCE(_resultsBuffer);
SAFE_DELETE_GPU_RESOURCES(_readbackBuffers);
}
bool HZBOcclusionCulling::IsSupported()
{
const GPULimits& limits = GPUDevice::Instance->Limits;
return limits.HasCompute;
}
void HZBOcclusionCulling::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
for (int32 i = framesCount; i < _framesCount; i++)
SAFE_DELETE_GPU_RESOURCE(_readbackBuffers[i]);
for (int32 i = _framesCount; i < framesCount; i++)
_readbackBuffers[i] = GPUDevice::Instance->CreateBuffer(TEXT("HZB.Readback"));
if (!_resultsBuffer)
_resultsBuffer = GPUDevice::Instance->CreateBuffer(TEXT("HZB.Results"));
else if (_resultsBuffer->IsAllocated())
{
auto desc = _resultsBuffer->GetDescription().ToStagingReadback();
for (int32 i = _framesCount; i < framesCount; i++)
_readbackBuffers[i]->Init(desc);
}
_framesCount = framesCount;
for (auto& item : _items)
{
item.Occluded = false;
Platform::MemoryClear(item.Frames, sizeof(item.Frames));
}
Platform::MemoryClear(_readbackFrames, sizeof(_readbackFrames));
}
_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
bool forceVisible = renderContext.Task->IsCameraCut;
// Skip reading when view was not rendered for some time
uint64 engineFrame = Engine::FrameCount;
forceVisible |= (int32)(engineFrame - _lastEngineFrameUsed) >= framesCount;
_lastEngineFrameUsed = engineFrame;
int32 frame = _frameCounter, bufferedFrame = _frameCounter % framesCount;
if (forceVisible || _submitFailed)
{
// Reset
_submitFailed = false;
for (auto& item : _items)
item.Occluded = false;
}
else if (_readbackFrames[bufferedFrame])
{
// Read results from the last frame
int32 itemsCount = _items.Count(), itemsUsed = 0;
auto* readback = (const ResultValue*)_readbackBuffers[bufferedFrame]->Map(GPUResourceMapMode::Read);
if (readback)
{
auto* items = _items.Get();
int32 frameItemsCount = Math::Min(_readbackCounts[bufferedFrame], itemsCount);
ASSERT(_readbackBuffers[bufferedFrame]->GetSize() >= frameItemsCount * sizeof(ResultValue));
for (int32 i = 0; i < frameItemsCount; i++)
{
auto& item = items[i];
int32 itemFrame = item.Frames[bufferedFrame];
int32 lag = frame - itemFrame;
if (itemFrame && lag <= framesCount)
{
// Clear frame
item.Frames[bufferedFrame] = 0;
// Read result
ResultValue result = readback[i];
item.Occluded = result == 0;
itemsUsed++;
}
}
_readbackBuffers[bufferedFrame]->Unmap();
}
else
{
for (auto& item : _items)
item.Occluded = false;
}
ZoneValue(itemsUsed);
_readbackFrames[bufferedFrame] = 0;
}
_items.BeginFrame();
// Resize buffers to store items culling results
int32 itemsCapacity = Math::RoundUpToPowerOf2(_items.Count());
if (_resultsBuffer->GetSize() < (uint32)itemsCapacity * sizeof(ResultValue))
{
itemsCapacity = Math::Max(itemsCapacity, 512);
auto desc = GPUBufferDescription::Buffer(itemsCapacity * sizeof(ResultValue), GPUBufferFlags::UnorderedAccess, ResultType, nullptr, sizeof(ResultValue));
_resultsBuffer->Init(desc);
desc = desc.ToStagingReadback();
for (int32 i = 0; i < _framesCount; i++)
_readbackBuffers[i]->Init(desc);
}
// Prepare buffer to write geometry bounds in async during drawing
bool init = _boundsBuffer.Data.IsEmpty();
_boundsBuffer.Data.Resize(_items.Count() * 2 * sizeof(Float3), true);
if (init && _boundsBuffer.Data.HasItems())
Platform::MemoryClear(_boundsBuffer.Data.Get(), sizeof(Float3) * 2); // Clear first unused item
_dirtyBounds = 0;
_submitFailed = false;
}
void HZBOcclusionCulling::EndFrame(const RenderContext& renderContext)
{
// Move to the next frame
_frameCounter++;
}
void HZBOcclusionCulling::Submit(const RenderContext& renderContext)
{
if (_items.IsEmpty() || !_shader || !_shader->IsLoaded())
return;
PROFILE_CPU();
PROFILE_GPU("HZB Occlusion Culling");
GPUContext* context = GPUDevice::Instance->GetMainContext();
// Update objects to cull
auto frustum = renderContext.View.Frustum;
auto* items = _items.Get();
int32 frame = _frameCounter, bufferedFrame = _frameCounter % _framesCount, itemsCount = _items.Count(), itemsEnd = 0;
for (int32 i = 0; i < itemsCount; i++)
{
auto& item = items[i];
if (item.LastUsedFrame != frame || frustum.Contains(item.Bounds) == ContainmentType::Disjoint)
continue;
itemsEnd = i + 1;
// Mark as used in this HZB frame (to read results later)
item.Frames[bufferedFrame] = frame;
}
ZoneValue(itemsEnd);
_readbackCounts[bufferedFrame] = itemsEnd;
if (itemsEnd == 0)
return;
// Build HZB with furthest depths (full mip chain) for the current frame
context->ResetRenderTarget();
GPUTexture* hzb = renderContext.Buffers->RequestHiZ(context, false, 0, false, true);
if (!hzb)
{
_submitFailed = true;
return;
}
// Upload object bounds data
if (_dirtyBounds)
_boundsBuffer.Flush(context);
ASSERT(_boundsBuffer.GetBuffer()->GetSize() >= sizeof(Float3) * 2 * itemsCount);
ASSERT(_resultsBuffer->GetSize() >= itemsCount * sizeof(ResultValue));
ASSERT(_readbackBuffers[bufferedFrame]->GetSize() >= itemsCount * sizeof(ResultValue));
// Test all object bounds against current frame HZB
auto cs = _shader->GPU->GetCS("CS_HZBCull");
auto cb = _shader->GPU->GetCB(0);
{
OcclusionCullingData data;
Matrix::Transpose(renderContext.View.ViewProjection(), data.ViewProjectionMatrix);
data.RTSizeX = (float)hzb->Width();
data.RTSizeY = (float)hzb->Height();
data.MaxMipLevel = (float)hzb->MipLevels();
data.CullCount = itemsEnd;
context->UpdateCB(cb, &data);
}
context->BindCB(0, cb);
context->BindUA(0, _resultsBuffer->View());
context->BindSR(0, _boundsBuffer.GetBuffer()->View());
context->BindSR(1, hzb->View());
context->Dispatch(cs, Math::DivideAndRoundUp(itemsEnd, 64));
// Copy results to the readback buffer
context->CopyBuffer(_readbackBuffers[bufferedFrame], _resultsBuffer, itemsEnd * sizeof(ResultValue));
// Mark the readback buffer has a valid frame data
_readbackFrames[bufferedFrame] = frame;
// Restore state
context->ResetSR();
context->SetViewportAndScissors(renderContext.Buffers->GetViewport());
}
bool HZBOcclusionCulling::IsVisible(const BoundingBox& bounds, uint32& cullingId)
{
return IsVisible(bounds, cullingId, nullptr);
}
bool HZBOcclusionCulling::IsVisible(const BoundingBox& bounds, GeometryDrawState& drawState)
{
return IsVisible(bounds, drawState.CullingId, &drawState);
}
bool HZBOcclusionCulling::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 id
if (_items.GetCullingId(cullingId))
return true;
// Update item bounds
auto& item = _items.Get()[cullingId];
if (item.Bounds != bounds || _forceUpdateBounds)
{
// Update bounds
item.Bounds = bounds;
// Write to the bounds buffer
Float3 boxMin = bounds.Minimum - _origin;
Float3 boxMax = bounds.Maximum - _origin;
ASSERT_LOW_LAYER(_boundsBuffer.Data.Count() >= (2 * sizeof(Float3)) * (cullingId + 1));
Float3* boundsPtr = (Float3*)(_boundsBuffer.Data.Get() + (2 * sizeof(Float3)) * cullingId);
boundsPtr[0] = boxMin;
boundsPtr[1] = 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;
}
@@ -0,0 +1,61 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#include "IOcclusionCulling.h"
#include "OcclusionCullingTools.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Graphics/DynamicBuffer.h"
#include "Engine/Scripting/ScriptingObject.h"
#include "Engine/Content/AssetReference.h"
/// <summary>
/// Occlusion culling system based on Hierarchical Z-Buffer visibility test with a readback.
/// It builds a mipmap chain from the depth buffer and runs a compute shader to test object bounding boxes against it to skip rendering unseen geometry.
/// Results are readback with a few frames latency (objects can pop in fast motion).
/// </summary>
API_CLASS(Sealed) class FLAXENGINE_API HZBOcclusionCulling : public ScriptingObject, public IOcclusionCulling
{
DECLARE_SCRIPTING_TYPE(HZBOcclusionCulling);
~HZBOcclusionCulling();
// Maximum number of frames to delay the visibility results readback from GPU (to avoid stalls). The higher value the more latency but less GPU stalls.
constexpr static int32 MaxFrames = 4;
private:
struct alignas(sizeof(uint64)) Item
{
BoundingBox Bounds; // Object bounds (world-space)
int32 Frames[MaxFrames]; // Frame counter for each test (0 if not performed)
int32 LastUsedFrame; // Last frame object was drawn (incl. not frustum-culled or hidden)
bool Occluded; // Result from the last frame
};
int32 _framesCount = 0, _frameCounter = 0;
volatile int64 _dirtyBounds = 0;
uint64 _lastEngineFrameUsed = 0;
Vector3 _origin = Vector3::Zero;
Vector3 _viewPos = Vector3::Zero;
AssetReference<class Shader> _shader;
OcclusionCullingItems<Item> _items;
DynamicTypedBuffer _boundsBuffer;
GPUBuffer* _resultsBuffer = nullptr;
GPUBuffer* _readbackBuffers[MaxFrames] = {};
int32 _readbackFrames[MaxFrames] = {};
int32 _readbackCounts[MaxFrames] = {};
float _boundsScale = 1.0f;
bool _forceUpdateBounds = true;
bool _submitFailed = false;
public:
// [IOcclusionCulling]
bool IsSupported() override;
void BeginFrame(const RenderContext& renderContext) override;
void EndFrame(const RenderContext& renderContext) override;
void Submit(const RenderContext& renderContext) override;
bool IsVisible(const BoundingBox& bounds, uint32& cullingId) override;
bool IsVisible(const BoundingBox& bounds, GeometryDrawState& drawState) override;
private:
bool IsVisible(BoundingBox bounds, uint32& cullingId, GeometryDrawState* drawState);
};
@@ -28,6 +28,10 @@ HardwareOcclusionCulling::HardwareOcclusionCulling(const SpawnParams& params)
HardwareOcclusionCulling::~HardwareOcclusionCulling()
{
SAFE_DELETE_GPU_RESOURCE(_indexBuffer);
#if COMPILE_WITH_DEV_ENV
if (_shader)
_shader->Reloading.Unbind<HardwareOcclusionCulling, &HardwareOcclusionCulling::OnShaderReloading>(this);
#endif
}
void HardwareOcclusionCulling::BeginFrame(const RenderContext& renderContext)
@@ -100,42 +104,7 @@ void HardwareOcclusionCulling::BeginFrame(const RenderContext& renderContext)
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();
_items.BeginFrame();
// Prepare vertex buffer to build geometry bound meshes in async during drawing
_vertexBuffer.Data.Resize(_items.Count() * 8 * sizeof(Float3), true);
@@ -203,9 +172,9 @@ void HardwareOcclusionCulling::Submit(const RenderContext& renderContext)
}
auto cb = _shader->GPU->GetCB(0);
{
Matrix viewProjectionMatrix;
Matrix::Transpose(renderContext.View.ViewProjection(), viewProjectionMatrix);
context->UpdateCB(cb, &viewProjectionMatrix);
OcclusionCullingData data;
Matrix::Transpose(renderContext.View.ViewProjection(), data.ViewProjectionMatrix);
context->UpdateCB(cb, &data);
}
context->BindCB(0, cb);
context->BindIB(_indexBuffer);
@@ -266,23 +235,9 @@ bool HardwareOcclusionCulling::IsVisible(BoundingBox bounds, uint32& cullingId,
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;
}
}
// Check id
if (_items.GetCullingId(cullingId))
return true;
// Update item bounds
auto& item = _items.Get()[cullingId];
@@ -3,6 +3,7 @@
#pragma once
#include "IOcclusionCulling.h"
#include "OcclusionCullingTools.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Graphics/DynamicBuffer.h"
#include "Engine/Scripting/ScriptingObject.h"
@@ -34,20 +35,17 @@ private:
};
int32 _framesCount = 2, _frameCounter = 0;
volatile int64 _freeItemsCount = 0;
volatile int64 _newItemsCount = 0;
volatile int64 _dirtyBounds = 0;
uint64 _lastEngineFrameUsed = 0;
float _boundsScale = 1.0f;
Array<Item> _items;
Array<uint32> _freeItems;
GPUPipelineState* _pso = nullptr;
DynamicVertexBuffer _vertexBuffer;
GPUBuffer* _indexBuffer = nullptr;
Vector3 _origin = Vector3::Zero;
Vector3 _viewPos = Vector3::Zero;
bool _forceUpdateBounds = true;
AssetReference<class Shader> _shader;
OcclusionCullingItems<Item> _items;
float _boundsScale = 1.0f;
bool _forceUpdateBounds = true;
public:
// [IOcclusionCulling]
@@ -0,0 +1,91 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Graphics/Config.h"
GPU_CB_STRUCT(OcclusionCullingData {
Matrix ViewProjectionMatrix;
float RTSizeX;
float RTSizeY;
float MaxMipLevel;
uint32 CullCount;
});
/// <summary>
/// Utility for occlusion culling implementations to manage stable CullingId for objects with state tracking (over multiple frames).
/// </summary>
template<typename Item>
class OcclusionCullingItems : public Array<Item>
{
private:
volatile int64 _freeItemsCount = 0;
volatile int64 _newItemsCount = 0;
Array<uint32> _freeItems;
public:
void BeginFrame()
{
// 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 < this->Count(); i++)
{
auto& item = this->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 = this->Count(), count = (int32)_newItemsCount, freeStart = _freeItems.Count();
if (itemsStart == 0)
count++; // 0 is invalid for cullingId
this->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();
}
bool GetCullingId(uint32& cullingId)
{
// Check if object doesn't have ID assigned yet
if (cullingId == 0 || cullingId >= (uint32)this->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;
}
}
return false;
}
};
+24 -3
View File
@@ -102,6 +102,23 @@ void MultiScaler::Dispose()
_shader = nullptr;
}
#if COMPILE_WITH_DEV_ENV
void MultiScaler::OnShaderReloading(Asset* obj)
{
for (const auto& e : _psBilateralUpscale)
e.Value->ReleaseGPU();
_psBilateralUpscale.ClearDelete();
_psUpscale->ReleaseGPU();
_psBlur5.Release();
_psBlur9.Release();
_psBlur13.Release();
_psHalfDepth.Release();
invalidateResources();
}
#endif
void MultiScaler::Filter(FilterMode mode, GPUContext* context, int32 width, int32 height, GPUTextureView* src, GPUTextureView* dst, GPUTextureView* tmp)
{
PROFILE_GPU_CPU("MultiScaler Filter");
@@ -260,30 +277,34 @@ void MultiScaler::DownscaleDepth(GPUContext* context, int32 dstWidth, int32 dstH
context->UnBindCB(0);
}
void MultiScaler::BuildHiZ(GPUContext* context, GPUTexture* srcDepth, GPUTexture* dstHiZ)
void MultiScaler::BuildHiZ(GPUContext* context, GPUTexture* srcDepth, GPUTexture* dstHiZ, bool closest)
{
PROFILE_GPU_CPU("Build HiZ");
int32 dstWidth = dstHiZ->Width();
int32 dstHeight = dstHiZ->Height();
GPUPipelineState* state = _psHalfDepth[closest ? 2 : 0]; // See PS_HalfDepth permutations
// Copy mip0
if (srcDepth->Size() == dstHiZ->Size() && srcDepth->Format() == dstHiZ->Format())
{
// Size and format match
context->CopySubresource(dstHiZ, 0, srcDepth, 0);
}
else if (srcDepth->Size() == dstHiZ->Size())
{
// Size match
context->Draw(dstHiZ, srcDepth);
}
else
{
// Downscale
auto rt = dstHiZ->View();
auto rtAction = GPUDrawPassAction::Store;
GPUDrawPass drawPass(context, ToSpan(&rt, 1), ToSpan(&rtAction, 1));
context->SetViewportAndScissors((float)dstWidth, (float)dstHeight);
context->BindSR(0, srcDepth);
context->SetState(_psHalfDepth[2]);
context->SetState(state);
context->DrawFullscreenTriangle();
}
@@ -299,7 +320,7 @@ void MultiScaler::BuildHiZ(GPUContext* context, GPUTexture* srcDepth, GPUTexture
GPUDrawPass drawPass(context, ToSpan(&rt, 1), ToSpan(&rtAction, 1));
context->SetViewportAndScissors((float)mipWidth, (float)mipHeight);
context->BindSR(0, dstHiZ->View(0, mip - 1));
context->SetState(_psHalfDepth[2]);
context->SetState(state);
context->DrawFullscreenTriangle();
}
+5 -15
View File
@@ -67,7 +67,7 @@ public:
void Filter(FilterMode mode, GPUContext* context, int32 width, int32 height, GPUTextureView* srcDst, GPUTextureView* tmp);
/// <summary>
/// Downscales the depth buffer (to half resolution). Uses `min` operator (`max` for inverted depth) to output the furthest depths for conservative usage.
/// Downscales the depth buffer (to half resolution). Uses `max` operator (`min` for inverted depth) to output the furthest depths for conservative usage.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="dstWidth">The width of the destination texture (in pixels).</param>
@@ -77,12 +77,13 @@ public:
void DownscaleDepth(GPUContext* context, int32 dstWidth, int32 dstHeight, GPUTexture* src, GPUTextureView* dst);
/// <summary>
/// Generates the Hierarchical Z-Buffer (HiZ). Uses `min` operator (`max` for inverted depth) to output the furthest depths for conservative usage.
/// Generates the Hierarchical Z-Buffer (HiZ) for a given depth buffer with a mip chain.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="srcDepth">The source depth buffer texture (has to have ShaderResource flag).</param>
/// <param name="dstHiZ">The destination HiZ texture (has to have DepthStencil or RenderTarget flag).</param>
void BuildHiZ(GPUContext* context, GPUTexture* srcDepth, GPUTexture* dstHiZ);
/// <param name="closest">True if generate the closest depth values, otherwise will use the furthest depths filter.</param>
void BuildHiZ(GPUContext* context, GPUTexture* srcDepth, GPUTexture* dstHiZ, bool closest = true);
/// <summary>
/// Upscales the texture using Catmull-Rom filtering with 9-taps.
@@ -111,18 +112,7 @@ public:
bool Init() override;
void Dispose() override;
#if COMPILE_WITH_DEV_ENV
void OnShaderReloading(Asset* obj)
{
for (const auto& e : _psBilateralUpscale)
e.Value->ReleaseGPU();
_psBilateralUpscale.ClearDelete();
_psUpscale->ReleaseGPU();
_psBlur5.Release();
_psBlur9.Release();
_psBlur13.Release();
_psHalfDepth.Release();
invalidateResources();
}
void OnShaderReloading(Asset* obj);
#endif
protected:
+8
View File
@@ -173,9 +173,17 @@ float4 LoadTextureWGSL(Texture2D tex, float2 uv)
tex.GetDimensions(size.x, size.y);
return tex.Load(uint3(size * uv, 0));
}
float4 LoadTextureWGSL(Texture2D tex, float2 uv, float level)
{
uint2 size, levels;
tex.GetDimensions(level, size.x, size.y, levels);
return tex.Load(uint3(size * uv, level));
}
#define SAMPLE_RT_DEPTH(rt, texCoord) LoadTextureWGSL(rt, texCoord).r
#define SAMPLE_RT_DEPTH_LEVEL(rt, texCoord, level) LoadTextureWGSL(rt, texCoord, level).r
#else
#define SAMPLE_RT_DEPTH(rt, texCoord) SAMPLE_RT(rt, texCoord).r
#define SAMPLE_RT_DEPTH_LEVEL(rt, texCoord, level) rt.SampleLevel(SamplerPointClamp, texCoord, level).r
#endif
// General purpose constants
+106
View File
@@ -1,9 +1,13 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#include "./Flax/Common.hlsl"
#include "./Flax/Math/Math.hlsl"
META_CB_BEGIN(0, OcclusionCullingData)
float4x4 ViewProjectionMatrix;
float2 RTSize;
float MaxMipLevel;
uint CullCount;
META_CB_END
// Vertex Shader function for Hardware Occlusion Culling queries bounds projection
@@ -12,3 +16,105 @@ float4 VS_HardwareOcclusionCulling(float3 Position : POSITION0) : SV_Position
{
return mul(float4(Position, 1), ViewProjectionMatrix);
}
#ifdef _CS_HZBCull
RWBuffer<uint> HZBResults : register(u0);
Buffer<float3> BoundsBuffer : register(t0);
Texture2D<float> HiZ : register(t1);
// Compute Shader for HZB culling
// [Reference: https://interplayoflight.wordpress.com/2017/11/15/experiments-in-gpu-based-occlusion-culling/]
// [Reference: https://blog.selfshadow.com/publications/practical-visibility/]
META_CS(true, AUTO)
[numthreads(64, 1, 1)]
void CS_HZBCull(uint DispatchThreadId : SV_DispatchThreadID)
{
if (DispatchThreadId >= CullCount)
return;
// Load object bounds
float3 bondsMin = BoundsBuffer[DispatchThreadId * 2];
float3 bondsMax = BoundsBuffer[DispatchThreadId * 2 + 1];
float3 bondsSize = bondsMax - bondsMin;
// Project bounds onto the screen
float3 boundsCorners[] = {
bondsMin.xyz,
bondsMin.xyz + float3(bondsSize.x,0,0),
bondsMin.xyz + float3(0, bondsSize.y,0),
bondsMin.xyz + float3(0, 0, bondsSize.z),
bondsMin.xyz + float3(bondsSize.xy,0),
bondsMin.xyz + float3(0, bondsSize.yz),
bondsMin.xyz + float3(bondsSize.x, 0, bondsSize.z),
bondsMax.xyz
};
float closestZ = DEPTH_RANGE_MAX;
float2 minUV = 1, maxUV = 0;
UNROLL
for (uint i = 0; i < 8; i++)
{
// Transform world-space bounds to NDC
float4 clipPos = PROJECT_POINT(float4(boundsCorners[i], 1), ViewProjectionMatrix);
clipPos.xyz = clipPos.xyz / clipPos.w;
// Get min/max UVs
clipPos.xy = clipPos.xy * float2(0.5, -0.5) + float2(0.5, 0.5);
clipPos.xy = saturate(clipPos.xy);
minUV = min(clipPos.xy, minUV);
maxUV = max(clipPos.xy, maxUV);
// Get the closest depth
#if REVERSE_Z
if (clipPos.z < 0)
clipPos.z = 1; // Point is behind the camera
closestZ = saturate(max(closestZ, clipPos.z));
#else
closestZ = saturate(min(closestZ, clipPos.z));
#endif
}
// Calculate Hi-Z buffer mip (assumes HZB is power of two)
#if VULKAN || defined(WGSL) || 1
float2 pixelSize = RTSize * (maxUV - minUV) * 2.0f;
float mip = floor(log2(max(max(pixelSize.x, pixelSize.y), 1.0f)));
#else
int2 size = (maxUV - minUV) * RTSize;
float mip = ceil(log2(max(max(size.x, size.y), 1)));
#endif
mip = clamp(mip, 0, MaxMipLevel);
float4 boundsUVs = float4(minUV, maxUV);
// Texel footprint for the lower (finer-grained) level
float mipUp = max(mip - 1, 0);
float2 scale = exp2(-mipUp);
float2 a = floor(boundsUVs.xy * scale);
float2 b = ceil(boundsUVs.zw * scale);
float2 dims = b - a;
// Use the lower level if we only touch <= 2 texels in both dimensions
if (dims.x <= 2 && dims.y <= 2)
mip = mipUp;
// Load depths from Hi-Z buffer
float4 depths = {
SAMPLE_RT_DEPTH_LEVEL(HiZ, boundsUVs.xy, mip),
SAMPLE_RT_DEPTH_LEVEL(HiZ, boundsUVs.zy, mip),
SAMPLE_RT_DEPTH_LEVEL(HiZ, boundsUVs.xw, mip),
SAMPLE_RT_DEPTH_LEVEL(HiZ, boundsUVs.zw, mip)
};
// Find the furthest depth and test it
#if REVERSE_Z
float furthestDepth = Min4(depths);
bool visible = closestZ >= furthestDepth;
#else
float furthestDepth = Max4(depths);
bool visible = closestZ <= furthestDepth;
#endif
// Write culling result
HZBResults[DispatchThreadId] = visible ? 1u : 0u;
}
#endif
+7 -7
View File
@@ -2,6 +2,7 @@
#include "./Flax/Common.hlsl"
#include "./Flax/Gather.hlsl"
#include "./Flax/Math/Math.hlsl"
META_CB_BEGIN(0, Data)
float2 TexelSize;
@@ -29,17 +30,16 @@ float PS_HalfDepth(Quad_VS2PS input)
float4 depths = TextureGatherDepth(Input, input.TexCoord);
#if REVERSE_Z
#if HZB_CLOSEST
return max(depths.x, max(depths.y, max(depths.z, depths.w)));
float closest = Max4(depths);
float furthest = Min4(depths);
#else
return min(depths.x, min(depths.y, min(depths.z, depths.w)));
float closest = Min4(depths);
float furthest = Max4(depths);
#endif
#else
#if HZB_CLOSEST
return min(depths.x, min(depths.y, min(depths.z, depths.w)));
return closest;
#else
return max(depths.x, max(depths.y, max(depths.z, depths.w)));
#endif
return furthest;
#endif
}