// Copyright (c) Wojciech Figat. All rights reserved.

#include "VolumetricFogPass.h"
#include "ShadowsPass.h"
#include "GBufferPass.h"
#include "DrawCall.h"
#include "GI/DynamicDiffuseGlobalIllumination.h"
#include "Engine/Graphics/Graphics.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderBuffers.h"
#include "Engine/Graphics/RenderTargetPool.h"
#include "Engine/Graphics/GPULimits.h"
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Graphics/Shaders/GPUVertexLayout.h"
#include "Engine/Content/Assets/CubeTexture.h"
#include "Engine/Content/Content.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/Units.h"

// Must match shader source
int32 VolumetricFogGridInjectionGroupSize = 4;
int32 VolumetricFogIntegrationGroupSize = 8;
#define VOLUMETRIC_FOG_GRID_Z_LINEAR 1

GPU_CB_STRUCT(SkyLightData {
    Float3 MultiplyColor;
    float VolumetricScatteringIntensity;
    Float3 AdditiveColor;
    float Dummy0;
    });

GPU_CB_STRUCT(Data {
    ShaderGBufferData GBuffer;

    Float3 GlobalAlbedo;
    float GlobalExtinctionScale;

    Float3 GlobalEmissive;
    float HistoryWeight;

    Float3 GridSize;
    uint32 MissedHistorySamplesCount;

    uint32 GridSizeIntX;
    uint32 GridSizeIntY;
    uint32 GridSizeIntZ;
    float PhaseG;

    Float2 Dummy0;
    float VolumetricFogMaxDistance;
    float InverseSquaredLightDistanceBiasScale;

    Float4 FogParameters;
    Float4 GridSliceParameters;

    Matrix PrevWorldToClip;

    Float4 FrameJitterOffsets[8];

    ShaderLightData DirectionalLight;
    SkyLightData SkyLight;
    DynamicDiffuseGlobalIlluminationPass::ConstantsData DDGI;
    });

GPU_CB_STRUCT(PerLight {
    Float2 SliceToDepth;
    int32 MinZ;
    float LocalLightScatteringIntensity;

    Float4 ViewSpaceBoundingSphere;
    Matrix ViewToVolumeClip;

    ShaderLightData LocalLight;
    });

struct FrameCache
{
    Float3 GridSize;
    int32 GridPixelSize;
    int32 GridSizeZ;
    bool FogJitter;
    float HistoryWeight;
    int32 MissedHistorySamplesCount;
    float InverseSquaredLightDistanceBiasScale;
    float SphereRasterizeRadiusBias;
    Data Data;
};

String VolumetricFogPass::ToString() const
{
    return TEXT("VolumetricFogPass");
}

bool VolumetricFogPass::Init()
{
    const auto& limits = GPUDevice::Instance->Limits;
    _isSupported = limits.HasGeometryShaders && limits.HasVolumeTextureRendering && limits.HasCompute && limits.HasInstancing;

    // Create pipeline states
    _psInjectLight.CreatePipelineStates();

    // Load assets
    _shader = Content::LoadAsyncInternal<Shader>(TEXT("Shaders/VolumetricFog"));
    if (_shader == nullptr)
    {
        return true;
    }
#if COMPILE_WITH_DEV_ENV
    _shader.Get()->OnReloading.Bind<VolumetricFogPass, &VolumetricFogPass::OnShaderReloading>(this);
#endif

    return false;
}

bool VolumetricFogPass::setupResources()
{
    if (!_shader->IsLoaded())
        return true;
    auto shader = _shader->GetShader();
    CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data);
    // CB1 is used for per-draw info (ObjectIndex)
    CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 2, PerLight);

    // Cache compute shaders
    _csInitialize = shader->GetCS("CS_Initialize");
    _csLightScattering.Get(shader, "CS_LightScattering");
    _csFinalIntegration = shader->GetCS("CS_FinalIntegration");

    // Create pipeline stages
    GPUPipelineState::Description psDesc;
    if (!_psInjectLight.IsValid())
    {
        psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
        psDesc.BlendMode = BlendingMode::Add;
        psDesc.VS = shader->GetVS("VS_WriteToSlice");
        psDesc.GS = shader->GetGS("GS_WriteToSlice");
        if (_psInjectLight.Create(psDesc, shader, "PS_InjectLight"))
            return true;
    }

    return false;
}

void VolumetricFogPass::Dispose()
{
    // Base
    RendererPass::Dispose();

    // Cleanup
    _psInjectLight.Delete();
    _csInitialize = nullptr;
    _csLightScattering.Clear();
    _csFinalIntegration = nullptr;
    SAFE_DELETE_GPU_RESOURCE(_vbCircleRasterize);
    SAFE_DELETE_GPU_RESOURCE(_ibCircleRasterize);
    _shader = nullptr;
}

Float4 GetGridSliceParameters(float fogStart, float fogEnd, int32 gridSizeZ)
{
    float sliceToUV = 1.0f / (float)gridSizeZ;
#if VOLUMETRIC_FOG_GRID_Z_LINEAR
    float sliceToDepth = fogEnd / (float)gridSizeZ;
    return Float4(sliceToDepth, 1.0f / sliceToDepth, 0.0f, sliceToUV);
#else
    // Use logarithmic distribution for Z slices to have more resolution for close distances and less for far ones (less aliasing near camera)
    const float distribution = 220.0f; // Manually adjusted to give a good distribution across the range
    fogStart += UNITS_TO_METERS(10); // Bias start a bit for some more quality
    float y = (fogEnd - fogStart * Math::Exp2((float)(gridSizeZ - 1) / distribution)) / (fogEnd - fogStart);
    float x = (1.0f - y) / fogStart;
    return Float4(x, y, distribution, sliceToUV);
#endif
}

float GetDepthFromSlice(float slice, const Float4& gridSliceParameters)
{
#if VOLUMETRIC_FOG_GRID_Z_LINEAR
    return slice * gridSliceParameters.X;
#else
    return (Math::Exp2(slice / gridSliceParameters.Z) - gridSliceParameters.Y) / gridSliceParameters.X;
#endif
}

float GetSliceFromDepth(float sceneDepth, const Float4& gridSliceParameters)
{
#if VOLUMETRIC_FOG_GRID_Z_LINEAR
    return sceneDepth * gridSliceParameters.Y;
#else
    return Math::Log2(sceneDepth * gridSliceParameters.X + gridSliceParameters.Y) * gridSliceParameters.Z;
#endif
}

struct alignas(Float4) RasterizeSphere
{
    Float3 Center;
    float Radius;
    Float3 ViewSpaceCenter;
    uint16 VolumeZBoundsMin;
    uint16 VolumeZBoundsMax;
};

bool VolumetricFogPass::Init(FrameCache& cache, RenderContext& renderContext, GPUContext* context)
{
    const auto& fog = renderContext.List->Fog;
    if (renderContext.Buffers->LastFrameVolumetricFog == Engine::FrameCount)
        return false;
    if (fog.Renderer == nullptr || 
        !renderContext.List->Setup.UseVolumetricFog || 
        !_isSupported || 
        !fog.VolumetricFog.UseVolumetricFog() ||
        checkIfSkipPass())
    {
        RenderTargetPool::Release(renderContext.Buffers->VolumetricFog);
        renderContext.Buffers->VolumetricFog = nullptr;
        renderContext.Buffers->LastFrameVolumetricFog = 0;
        return true;
    }
    auto& options = fog.VolumetricFog;

    // Setup configuration
    cache.FogJitter = true;
    cache.HistoryWeight = 0.92f;
    cache.InverseSquaredLightDistanceBiasScale = 1.0f;
    switch (Graphics::VolumetricFogQuality)
    {
    case Quality::Low:
        cache.GridPixelSize = 24;
        cache.GridSizeZ = 50;
        cache.MissedHistorySamplesCount = 1;
        break;
    case Quality::Medium:
        cache.GridPixelSize = 20;
        cache.GridSizeZ = 54;
        cache.MissedHistorySamplesCount = 2;
        break;
    case Quality::High:
        cache.GridPixelSize = 16;
        cache.GridSizeZ = 64;
        cache.MissedHistorySamplesCount = 4;
        break;
    case Quality::Ultra:
        cache.GridPixelSize = 10;
        cache.GridSizeZ = 128;
        cache.MissedHistorySamplesCount = 8;
        break;
    }

    // Calculate volumetric fog size
    const int32 width = renderContext.Buffers->GetWidth();
    const int32 height = renderContext.Buffers->GetHeight();
    constexpr int32 resolutionLimit = 1920; // Limit resolution to 1080p to save on perf for players on 2k/4k displays (incl. consoles)
    if (resolutionLimit > 0)
    {
        const int32 limitX = resolutionLimit; // Controls screen width limit, height depends on the aspect ratio
        const int32 limitY = Math::CeilToInt((float)limitX * (float)height / (float)width);
        if (width > limitX || height > limitY)
        {
            const float scaleX = (float)Math::Max(width, limitX) / limitX;
            const float scaleY = (float)Math::Max(height, limitY) / limitY;
            cache.GridPixelSize = Math::CeilToInt(cache.GridPixelSize * Math::Max(scaleX, scaleY));
        }
    }
    cache.GridSize = Float3(
        (float)Math::DivideAndRoundUp(width, cache.GridPixelSize),
        (float)Math::DivideAndRoundUp(height, cache.GridPixelSize),
        (float)cache.GridSizeZ);
    auto& fogData = renderContext.Buffers->VolumetricFogData;
    fogData.MaxDistance = options.Distance;
    if (renderContext.Task->IsCameraCut ||
        renderContext.View.IsOriginTeleport() ||
        (renderContext.Buffers->VolumetricFog && renderContext.Buffers->VolumetricFog->Size3() != cache.GridSize))
    {
        // Don't blend with history on camera cuts or teleport or resizes
        cache.HistoryWeight = 0.0f;
    }

    // Init data (partial, without directional light or sky light data);
    GBufferPass::SetInputs(renderContext.View, cache.Data.GBuffer);
    cache.Data.GlobalAlbedo = options.Albedo.ToFloat3() * options.Albedo.A;
    cache.Data.GlobalExtinctionScale = options.ExtinctionScale;
    cache.Data.GlobalEmissive = options.Emissive.ToFloat3() * options.Emissive.A;
    cache.Data.GridSize = cache.GridSize;
    cache.Data.GridSizeIntX = (uint32)cache.GridSize.X;
    cache.Data.GridSizeIntY = (uint32)cache.GridSize.Y;
    cache.Data.GridSizeIntZ = (uint32)cache.GridSize.Z;
    cache.Data.HistoryWeight = cache.HistoryWeight;
    cache.Data.FogParameters = options.FogParameters;
    cache.Data.GridSliceParameters = GetGridSliceParameters(renderContext.View.Near, options.Distance, cache.GridSizeZ);
    /*static bool log = true;
    if (log)
    {
        log = false;
        for (int slice = 0; slice < cache.GridSizeZ; slice++)
            LOG(Error, "Slice {} -> {}", slice, GetDepthFromSlice((float)slice, cache.Data.GridSliceParameters));
    }*/
    cache.Data.InverseSquaredLightDistanceBiasScale = cache.InverseSquaredLightDistanceBiasScale;
    cache.Data.PhaseG = options.ScatteringDistribution;
    cache.Data.VolumetricFogMaxDistance = options.Distance;
    cache.Data.MissedHistorySamplesCount = Math::Clamp(cache.MissedHistorySamplesCount, 1, (int32)ARRAY_COUNT(cache.Data.FrameJitterOffsets));
    Matrix::Transpose(renderContext.View.PrevViewProjection, cache.Data.PrevWorldToClip);
    cache.Data.SkyLight.VolumetricScatteringIntensity = 0;

    // Fill frame jitter history
    const Float4 defaultOffset(0.5f, 0.5f, 0.5f, 0.0f);
    for (int32 i = 0; i < ARRAY_COUNT(cache.Data.FrameJitterOffsets); i++)
        cache.Data.FrameJitterOffsets[i] = defaultOffset;
    cache.SphereRasterizeRadiusBias = 0.0f;
    if (cache.FogJitter)
    {
        for (int32 i = 0; i < cache.MissedHistorySamplesCount; i++)
        {
            const uint64 frameNumber = renderContext.Task->LastUsedFrame - i;
            cache.Data.FrameJitterOffsets[i] = Float4(
                RenderTools::TemporalHalton(frameNumber & 1023, 2),
                RenderTools::TemporalHalton(frameNumber & 1023, 3),
                RenderTools::TemporalHalton(frameNumber & 1023, 5),
                0);
        }

        // Add bias to radius when using jittering to avoid pixelization on the circle borders (cell offset is randomized)
        float worldUnitsPerDepthCell = options.Distance / cache.GridSize.Z;
        // TODO: include XY size too?
        cache.SphereRasterizeRadiusBias = worldUnitsPerDepthCell * 0.25f;
    }

    // Set constant buffer data
    auto cb0 = _shader->GetShader()->GetCB(0);
    context->UpdateCB(cb0, &cache.Data);

    // Clear local lights scattering table if was used and will be probably reused later
    if (renderContext.Buffers->LocalShadowedLightScattering)
    {
        if (Float3::NearEqual(renderContext.Buffers->LocalShadowedLightScattering->Size3(), cache.GridSize))
        {
            context->Clear(renderContext.Buffers->LocalShadowedLightScattering->ViewVolume(), Color::Transparent);
        }
        else
        {
            RenderTargetPool::Release(renderContext.Buffers->LocalShadowedLightScattering);
            renderContext.Buffers->LocalShadowedLightScattering = nullptr;
        }
    }

    // Render fog this frame
    renderContext.Buffers->LastFrameVolumetricFog = Engine::FrameCount;
    return false;
}

bool VolumetricFogPass::InitSphereRasterize(FrameCache& cache, RasterizeSphere& sphere, RenderView& view, const Float3& center, float radius)
{
    ASSERT_LOW_LAYER(!center.IsNanOrInfinity() && !isnan(radius) && !isinf(radius));
    sphere.Center = center;
    sphere.Radius = radius + cache.SphereRasterizeRadiusBias;

    // Calculate sphere volume bounds in camera frustum depth range (min and max)
    sphere.ViewSpaceCenter = Float3::Transform(center, view.View);
    const float furthestSliceIndex = GetSliceFromDepth(sphere.ViewSpaceCenter.Z + sphere.Radius, cache.Data.GridSliceParameters);
    const float closestSliceIndex = GetSliceFromDepth(sphere.ViewSpaceCenter.Z - sphere.Radius, cache.Data.GridSliceParameters);
    sphere.VolumeZBoundsMin = (uint16)Math::Clamp(closestSliceIndex, 0.0f, cache.GridSize.Z - 1.0f);
    sphere.VolumeZBoundsMax = (uint16)Math::Clamp(furthestSliceIndex, 0.0f, cache.GridSize.Z - 1.0f);

    // Cull
    if ((view.Position - sphere.Center).LengthSquared() >= Math::Square(cache.Data.VolumetricFogMaxDistance + sphere.Radius) ||
        sphere.VolumeZBoundsMin > sphere.VolumeZBoundsMax)
    {
        return true;
    }

    return false;
}

GPUTextureView* VolumetricFogPass::GetLocalShadowedLightScattering(FrameCache& cache, RenderContext& renderContext, GPUContext* context) const
{
    if (renderContext.Buffers->LocalShadowedLightScattering == nullptr)
    {
        ASSERT(renderContext.Buffers->LastFrameVolumetricFog == Engine::FrameCount);
        const GPUTextureDescription volumeDescRGB = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R11G11B10_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess);
        const auto texture = RenderTargetPool::Get(volumeDescRGB);
        RENDER_TARGET_POOL_SET_NAME(texture, "VolumetricFog.LocalShadowedLightScattering");
        renderContext.Buffers->LocalShadowedLightScattering = texture;
        context->Clear(texture->ViewVolume(), Color::Transparent);
    }

    return renderContext.Buffers->LocalShadowedLightScattering->ViewVolume();
}

template<typename T>
void VolumetricFogPass::RenderRadialLight(FrameCache& cache, RenderContext& renderContext, GPUContext* context, T& light, PerLight& perLight, GPUConstantBuffer* cb2)
{
    RasterizeSphere sphere;
    if (InitSphereRasterize(cache, sphere, renderContext.View, light.Position, light.Radius))
        return;

    // Setup data
    perLight.SliceToDepth.X = cache.Data.GridSize.Z;
    perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance;
    perLight.MinZ = sphere.VolumeZBoundsMin;
    perLight.LocalLightScatteringIntensity = light.VolumetricScatteringIntensity;
    perLight.ViewSpaceBoundingSphere = Float4(sphere.ViewSpaceCenter, sphere.Radius);
    Matrix::Transpose(renderContext.View.Projection, perLight.ViewToVolumeClip);
    const bool withShadow = light.CastVolumetricShadow && light.HasShadow;
    light.SetShaderData(perLight.LocalLight, withShadow);

    // Upload data
    context->UpdateCB(cb2, &perLight);
    context->BindCB(2, cb2);

    // Ensure to have valid buffers created
    if (_vbCircleRasterize == nullptr || _ibCircleRasterize == nullptr)
        InitCircleBuffer();

    // Call rendering to the volume
    const int32 psIndex = withShadow ? 1 : 0;
    context->SetState(_psInjectLight.Get(psIndex));
    const int32 instanceCount = sphere.VolumeZBoundsMax - sphere.VolumeZBoundsMin + 1;
    const int32 indexCount = _ibCircleRasterize->GetElementsCount();
    context->BindVB(ToSpan(&_vbCircleRasterize, 1));
    context->BindIB(_ibCircleRasterize);
    context->DrawIndexedInstanced(indexCount, instanceCount, 0);
}

void VolumetricFogPass::Render(RenderContext& renderContext)
{
    auto context = GPUDevice::Instance->GetMainContext();
    FrameCache cache;
    if (Init(cache, renderContext, context))
        return;
    PROFILE_GPU_CPU("Volumetric Fog");

    // TODO: test exponential depth distribution (should give better quality near the camera)
    // TODO: use tiled light culling and render shadowed/unshadowed lights in single pass

    // Try to get shadows atlas
    GPUTexture* shadowMap;
    GPUBufferView* shadowsBuffer;
    ShadowsPass::GetShadowAtlas(renderContext.Buffers, shadowMap, shadowsBuffer);

    // Init directional light data
    Platform::MemoryClear(&cache.Data.DirectionalLight, sizeof(cache.Data.DirectionalLight));
    if (renderContext.List->DirectionalLights.HasItems())
    {
        const int32 dirLightIndex = (int32)renderContext.List->DirectionalLights.Count() - 1;
        const auto& dirLight = renderContext.List->DirectionalLights[dirLightIndex];
        const float brightness = dirLight.VolumetricScatteringIntensity;
        if (brightness > ZeroTolerance)
        {
            const bool useShadow = shadowMap && dirLight.CastVolumetricShadow && dirLight.HasShadow;
            dirLight.SetShaderData(cache.Data.DirectionalLight, useShadow);
            cache.Data.DirectionalLight.Color *= brightness;
        }
    }

    // Init GI data
    bool useDDGI = false;
    DynamicDiffuseGlobalIlluminationPass::BindingData bindingDataDDGI;
    if (EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::GI))
    {
        switch (renderContext.List->Settings.GlobalIllumination.Mode)
        {
        case GlobalIlluminationMode::DDGI:
            if (!DynamicDiffuseGlobalIlluminationPass::Instance()->Get(renderContext.Buffers, bindingDataDDGI))
            {
                cache.Data.DDGI = bindingDataDDGI.Constants;
                useDDGI = true;
            }
            break;
        }
    }

    // Init sky light data
    GPUTexture* skyLightImage = nullptr;
    Platform::MemoryClear(&cache.Data.SkyLight, sizeof(cache.Data.SkyLight));
    if (renderContext.List->SkyLights.HasItems() && !useDDGI)
    {
        const auto& skyLight = renderContext.List->SkyLights.Last();
        if (skyLight.VolumetricScatteringIntensity > ZeroTolerance)
        {
            cache.Data.SkyLight.MultiplyColor = skyLight.Color;
            cache.Data.SkyLight.AdditiveColor = skyLight.AdditiveColor;
            cache.Data.SkyLight.VolumetricScatteringIntensity = skyLight.VolumetricScatteringIntensity;
            const auto source = skyLight.Image;
            skyLightImage = source ? source->GetTexture() : nullptr;
        }
    }

    // Set constant buffer data
    auto cb0 = _shader->GetShader()->GetCB(0);
    context->UpdateCB(cb0, &cache.Data);
    context->BindCB(0, cb0);

    // Allocate buffers
    const GPUTextureDescription volumeDesc = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R16G16B16A16_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess);
    const GPUTextureDescription volumeDescRGB = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R11G11B10_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess);
    auto vBufferA = RenderTargetPool::Get(volumeDesc);
    RENDER_TARGET_POOL_SET_NAME(vBufferA, "VolumetricFog.VBufferA");
    auto vBufferB = RenderTargetPool::Get(volumeDescRGB);
    RENDER_TARGET_POOL_SET_NAME(vBufferB, "VolumetricFog.VBufferB");
    const auto lightScattering = RenderTargetPool::Get(volumeDesc);
    RENDER_TARGET_POOL_SET_NAME(lightScattering, "VolumetricFog.LightScattering");

    int32 groupCountX = Math::DivideAndRoundUp((int32)cache.GridSize.X, VolumetricFogGridInjectionGroupSize);
    int32 groupCountY = Math::DivideAndRoundUp((int32)cache.GridSize.Y, VolumetricFogGridInjectionGroupSize);
    int32 groupCountZ = Math::DivideAndRoundUp((int32)cache.GridSize.Z, VolumetricFogGridInjectionGroupSize);

    // Initialize fog volume properties
    {
        PROFILE_GPU("Initialize");
        context->ResetRenderTarget();
        context->BindUA(0, vBufferA->ViewVolume());
        context->BindUA(1, vBufferB->ViewVolume());
        context->Dispatch(_csInitialize, groupCountX, groupCountY, groupCountZ);
        context->ResetUA();
    }

    // Render local fog particles
    if (renderContext.List->VolumetricFogParticles.Count() != 0)
    {
        PROFILE_GPU_CPU_NAMED("Local Fog");

        // Bind the output
        GPUTextureView* rt[] = { vBufferA->ViewVolume(), vBufferB->ViewVolume() };
        context->SetRenderTarget(nullptr, Span<GPUTextureView*>(rt, 2));
        context->SetViewportAndScissors((float)volumeDesc.Width, (float)volumeDesc.Height);

        // Ensure to have valid buffers created
        if (_vbCircleRasterize == nullptr || _ibCircleRasterize == nullptr)
            InitCircleBuffer();

        MaterialBase::BindParameters bindParams(context, renderContext);
        CustomData customData;
        customData.Shader = _shader->GetShader();
        customData.GridSize = cache.GridSize;
        customData.VolumetricFogMaxDistance = cache.Data.VolumetricFogMaxDistance;
        customData.GridSliceParameters = cache.Data.GridSliceParameters;
        bindParams.CustomData = &customData;
        bindParams.BindViewData();
        bindParams.DrawCall = renderContext.List->VolumetricFogParticles.begin();
        bindParams.BindDrawData();

        for (auto& drawCall : renderContext.List->VolumetricFogParticles)
        {
            RasterizeSphere sphere;
            if (InitSphereRasterize(cache, sphere, renderContext.View, drawCall.Particle.VolumetricFog.Position, drawCall.Particle.VolumetricFog.Radius))
                continue;

            // Setup material shader data
            customData.ParticleIndex = drawCall.Particle.VolumetricFog.ParticleIndex;
            bindParams.DrawCall = &drawCall;
            drawCall.Material->Bind(bindParams);

            // Setup volumetric shader data
            PerLight perLight;
            auto cb2 = _shader->GetShader()->GetCB(2);
            perLight.SliceToDepth.X = cache.Data.GridSize.Z;
            perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance;
            perLight.MinZ = sphere.VolumeZBoundsMin;
            perLight.ViewSpaceBoundingSphere = Float4(sphere.ViewSpaceCenter, sphere.Radius);
            Matrix::Transpose(renderContext.View.Projection, perLight.ViewToVolumeClip);

            // Upload data
            context->UpdateCB(cb2, &perLight);
            context->BindCB(2, cb2);

            // Call rendering to the volume
            const int32 instanceCount = sphere.VolumeZBoundsMax - sphere.VolumeZBoundsMin + 1;
            const int32 indexCount = _ibCircleRasterize->GetElementsCount();
            context->BindVB(ToSpan(&_vbCircleRasterize, 1));
            context->BindIB(_ibCircleRasterize);
            context->DrawIndexedInstanced(indexCount, instanceCount, 0);
        }

        context->ResetRenderTarget();
        context->BindCB(0, cb0);
    }

    // Render Lights
    GPUTextureView* localShadowedLightScattering = nullptr;
    {
        // Get lights to render
        Array<uint16, InlinedAllocation<64, RendererAllocation>> pointLights;
        Array<uint16, InlinedAllocation<64, RendererAllocation>> spotLights;
        Float3 viewPosition = renderContext.View.Position;
        float distance = cache.Data.VolumetricFogMaxDistance;
        for (int32 i = 0; i < renderContext.List->PointLights.Count(); i++)
        {
            const auto& light = renderContext.List->PointLights.Get()[i];
            if (light.VolumetricScatteringIntensity > ZeroTolerance &&
                (viewPosition - light.Position).LengthSquared() < Math::Square(distance + light.Radius))
                pointLights.Add(i);
        }
        for (int32 i = 0; i < renderContext.List->SpotLights.Count(); i++)
        {
            const auto& light = renderContext.List->SpotLights.Get()[i];
            if (light.VolumetricScatteringIntensity > ZeroTolerance &&
                (viewPosition - light.Position).LengthSquared() < Math::Square(distance + light.Radius))
                spotLights.Add(i);
        }

        // Skip if no lights to render
        if (pointLights.Count() + spotLights.Count())
        {
            PROFILE_GPU_CPU_NAMED("Lights Injection");

            // Allocate temporary buffer for light scattering injection
            localShadowedLightScattering = GetLocalShadowedLightScattering(cache, renderContext, context);

            // Prepare
            PerLight perLight;
            perLight.SliceToDepth.X = cache.Data.GridSize.Z;
            perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance;
            auto cb2 = _shader->GetShader()->GetCB(2);

            // Bind the output
            context->SetRenderTarget(localShadowedLightScattering);
            context->SetViewportAndScissors((float)volumeDesc.Width, (float)volumeDesc.Height);

            // Render them to the volume
            context->BindSR(0, shadowMap);
            context->BindSR(1, shadowsBuffer);
            auto* pointLightsIdxPtr = pointLights.Get();
            auto* pointLightsPtr = renderContext.List->PointLights.Get();
            for (int32 i = 0; i < pointLights.Count(); i++)
                RenderRadialLight(cache, renderContext, context, pointLightsPtr[pointLightsIdxPtr[i]], perLight, cb2);
            auto* spotLightsIdxPtr = spotLights.Get();
            auto* spotLightsPtr = renderContext.List->SpotLights.Get();
            for (int32 i = 0; i < spotLights.Count(); i++)
                RenderRadialLight(cache, renderContext, context, spotLightsPtr[spotLightsIdxPtr[i]], perLight, cb2);

            // Cleanup
            context->UnBindCB(2);
            context->ResetRenderTarget();
            context->FlushState();
        }
        else if (renderContext.Buffers->LocalShadowedLightScattering)
        {
            localShadowedLightScattering = renderContext.Buffers->LocalShadowedLightScattering->ViewVolume();
        }
    }

    // Light Scattering
    {
        PROFILE_GPU("Light Scattering");

        const bool temporalHistoryIsValid = renderContext.Buffers->VolumetricFogHistory && Float3::NearEqual(renderContext.Buffers->VolumetricFogHistory->Size3(), cache.GridSize);
        const auto lightScatteringHistory = temporalHistoryIsValid ? renderContext.Buffers->VolumetricFogHistory : nullptr;

        context->BindUA(0, lightScattering->ViewVolume());
        context->BindSR(0, vBufferA->ViewVolume());
        context->BindSR(1, vBufferB->ViewVolume());
        context->BindSR(2, lightScatteringHistory ? lightScatteringHistory->ViewVolume() : nullptr);
        context->BindSR(3, localShadowedLightScattering);
        context->BindSR(4, shadowMap);
        context->BindSR(5, shadowsBuffer);
        int32 csIndex;
        if (useDDGI)
        {
            context->BindSR(6, bindingDataDDGI.ProbesData);
            context->BindSR(7, bindingDataDDGI.ProbesDistance);
            context->BindSR(8, bindingDataDDGI.ProbesIrradiance);
            csIndex = 1;
        }
        else
        {
            context->BindSR(6, skyLightImage);
            csIndex = 0;
        }
        context->Dispatch(_csLightScattering.Get(csIndex), groupCountX, groupCountY, groupCountZ);

        context->ResetSR();
        context->ResetUA();
    }

    // Release resources
    RenderTargetPool::Release(vBufferA);
    RenderTargetPool::Release(vBufferB);

    // Update the temporal history buffer
    RenderTargetPool::Release(renderContext.Buffers->VolumetricFogHistory);
    renderContext.Buffers->VolumetricFogHistory = lightScattering;

    // Get buffer for the integrated light scattering (try to reuse the previous frame if it's valid)
    GPUTexture* integratedLightScattering = renderContext.Buffers->VolumetricFog;
    if (integratedLightScattering == nullptr || !Float3::NearEqual(integratedLightScattering->Size3(), cache.GridSize))
    {
        RenderTargetPool::Release(integratedLightScattering);
        integratedLightScattering = RenderTargetPool::Get(volumeDesc);
        RENDER_TARGET_POOL_SET_NAME(integratedLightScattering, "VolumetricFog.Integrated");
        renderContext.Buffers->VolumetricFog = integratedLightScattering;
    }
    renderContext.Buffers->LastFrameVolumetricFog = Engine::FrameCount;

    // Update fog to be used by other passes
    const float ditherNoiseScale = 0.2f; // Scales noise in SampleVolumetricFog
    renderContext.List->Fog.VolumetricFogTexture = integratedLightScattering->ViewVolume();
    renderContext.List->Fog.VolumetricFogData.GridSliceParameters = cache.Data.GridSliceParameters;
    renderContext.List->Fog.VolumetricFogData.ScreenSize = renderContext.Buffers->GetSize();
    renderContext.List->Fog.VolumetricFogData.VolumeTexelSize = Float2(1.0f / cache.GridSize.X, 1.0f / cache.GridSize.Y) * ditherNoiseScale;

    groupCountX = Math::DivideAndRoundUp((int32)cache.GridSize.X, VolumetricFogIntegrationGroupSize);
    groupCountY = Math::DivideAndRoundUp((int32)cache.GridSize.Y, VolumetricFogIntegrationGroupSize);

    // Final Integration
    {
        PROFILE_GPU("Final Integration");
        context->BindUA(0, integratedLightScattering->ViewVolume());
        context->BindSR(0, lightScattering->ViewVolume());
        context->Dispatch(_csFinalIntegration, groupCountX, groupCountY, 1);
    }

    // Cleanup
    context->ResetUA();
    context->ResetSR();
    context->ResetRenderTarget();
    auto viewport = renderContext.Task->GetViewport();
    context->SetViewportAndScissors(viewport);
}

void VolumetricFogPass::InitCircleBuffer()
{
    const int32 vertices = 8;
    const int32 triangles = vertices - 2;
    const int32 rings = vertices;
    const float radiansPerRingSegment = PI / (float)rings;
    Float2 vbData[vertices];
    uint16 ibData[triangles * 3];

    const float radiusScale = 1.0f / Math::Cos(radiansPerRingSegment);
    for (int32 vertexIndex = 0; vertexIndex < vertices; vertexIndex++)
    {
        const float angle = vertexIndex / static_cast<float>(vertices - 1) * 2 * PI;
        vbData[vertexIndex] = Float2(radiusScale * Math::Cos(angle) * 0.5f + 0.5f, radiusScale * Math::Sin(angle) * 0.5f + 0.5f);
    }
    int32 ibIndex = 0;
    for (int32 triangleIndex = 0; triangleIndex < triangles; triangleIndex++)
    {
        const int32 firstVertexIndex = triangleIndex + 2;
        ibData[ibIndex++] = 0;
        ibData[ibIndex++] = firstVertexIndex - 1;
        ibData[ibIndex++] = firstVertexIndex;
    }

    // Create buffers
    ASSERT_LOW_LAYER(_vbCircleRasterize == nullptr && _ibCircleRasterize == nullptr);
    _vbCircleRasterize = GPUDevice::Instance->CreateBuffer(TEXT("VolumetricFog.CircleRasterize.VB"));
    _ibCircleRasterize = GPUDevice::Instance->CreateBuffer(TEXT("VolumetricFog.CircleRasterize.IB"));
    auto layout = GPUVertexLayout::Get({{ VertexElement::Types::TexCoord, 0, 0, 0, PixelFormat::R32G32_Float }});
    if (_vbCircleRasterize->Init(GPUBufferDescription::Vertex(layout, sizeof(Float2), vertices, vbData))
        || _ibCircleRasterize->Init(GPUBufferDescription::Index(sizeof(uint16), triangles * 3, ibData)))
    {
        LOG(Fatal, "Failed to setup volumetric fog buffers.");
    }
}
