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

#include "GPUDevice.h"
#include "RenderTargetPool.h"
#include "GPUPipelineState.h"
#include "GPUResourceProperty.h"
#include "GPUSwapChain.h"
#include "RenderTask.h"
#include "RenderTools.h"
#include "Graphics.h"
#include "Shaders/GPUShader.h"
#include "Shaders/GPUVertexLayout.h"
#include "Async/DefaultGPUTasksExecutor.h"
#include "Async/GPUTasksManager.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Utilities.h"
#include "Engine/Core/Types/StringBuilder.h"
#include "Engine/Content/Assets/Shader.h"
#include "Engine/Content/Assets/Material.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/SoftAssetReference.h"
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Render2D/Render2D.h"
#include "Engine/Engine/CommandLine.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Profiler/Profiler.h"
#include "Engine/Renderer/RenderList.h"
#include "Engine/Scripting/Enums.h"

GPUResourcePropertyBase::~GPUResourcePropertyBase()
{
    const auto e = _resource;
    if (e)
    {
        _resource = nullptr;
        e->Releasing.Unbind<GPUResourcePropertyBase, &GPUResourcePropertyBase::OnReleased>(this);
    }
}

void GPUResourcePropertyBase::OnSet(GPUResource* resource)
{
    auto e = _resource;
    if (e != resource)
    {
        if (e)
            e->Releasing.Unbind<GPUResourcePropertyBase, &GPUResourcePropertyBase::OnReleased>(this);
        _resource = e = resource;
        if (e)
            e->Releasing.Bind<GPUResourcePropertyBase, &GPUResourcePropertyBase::OnReleased>(this);
    }
}

void GPUResourcePropertyBase::OnReleased()
{
    auto e = _resource;
    if (e)
    {
        _resource = nullptr;
        e->Releasing.Unbind<GPUResourcePropertyBase, &GPUResourcePropertyBase::OnReleased>(this);
    }
}

GPUPipelineState* GPUPipelineState::Spawn(const SpawnParams& params)
{
    return GPUDevice::Instance->CreatePipelineState();
}

GPUPipelineState* GPUPipelineState::New()
{
    return GPUDevice::Instance->CreatePipelineState();
}

GPUPipelineState::GPUPipelineState()
    : GPUResource(SpawnParams(Guid::New(), TypeInitializer))
{
}

#if !BUILD_RELEASE

void GPUPipelineState::GetDebugName(DebugName& name) const
{
#define GET_NAME(e) \
    if (DebugDesc.e) \
    { \
        GPUShaderProgram::DebugName n; \
        DebugDesc.e->GetDebugName(n); \
        name.Add(n.Get(), n.Count() - 1); \
        name.Add('+'); \
    }
    GET_NAME(VS);
#if GPU_ALLOW_TESSELLATION_SHADERS
    GET_NAME(HS);
    GET_NAME(DS);
#endif
#if GPU_ALLOW_GEOMETRY_SHADERS
    GET_NAME(GS);
#endif
    GET_NAME(PS);
#undef GET_NAME
    if (name.Count() != 0 && name[name.Count() - 1] == '+')
        name.RemoveLast();
    name.Add('\0');
}

#endif

bool GPUPipelineState::Init(const Description& desc)
{
    // Cache description in development builds
#if !BUILD_RELEASE
    DebugDesc = desc;
#endif

    // Cache shader stages usage flags for pipeline state
    Platform::MemoryClear(&_meta, sizeof(_meta));
#define CHECK_STAGE(stage) \
	if (desc.stage) { \
		_meta.UsedCBsMask |= desc.stage->GetBindings().UsedCBsMask; \
		_meta.UsedSRsMask |= desc.stage->GetBindings().UsedSRsMask; \
		_meta.UsedUAsMask |= desc.stage->GetBindings().UsedUAsMask; \
		_meta.UsedSamplersMask |= desc.stage->GetBindings().UsedSamplersMask; \
	}
    CHECK_STAGE(VS);
    CHECK_STAGE(HS);
    CHECK_STAGE(DS);
    CHECK_STAGE(GS);
    CHECK_STAGE(PS);
#undef CHECK_STAGE

#if USE_EDITOR
    // Estimate somehow performance cost of this pipeline state for the content profiling
    const int32 textureLookupCost = 20;
    const int32 tessCost = 300;
    Complexity = Utilities::CountBits(_meta.UsedSRsMask) * textureLookupCost;
    if (desc.PS)
        Complexity += desc.PS->GetBindings().InstructionsCount;
    if (desc.HS || desc.DS)
        Complexity += tessCost;
    if (desc.DepthWriteEnable)
        Complexity += 5;
    if (desc.DepthEnable)
        Complexity += 5;
    if (desc.BlendMode.BlendEnable)
        Complexity += 20;
#endif

    return false;
}

GPUResourceType GPUPipelineState::GetResourceType() const
{
    return GPUResourceType::PipelineState;
}

// @formatter:off
GPUPipelineState::Description GPUPipelineState::Description::Default =
{
    true, // DepthEnable
    true, // DepthWriteEnable
    true, // DepthClipEnable
    false, // DepthBoundsEnable
    ComparisonFunc::Less, // DepthFunc
    false, // StencilEnable
    0xff, // StencilReadMask
    0xff, // StencilWriteMask
    ComparisonFunc::Always, // StencilFunc
    StencilOperation::Keep, // StencilFailOp
    StencilOperation::Keep, // StencilDepthFailOp
    StencilOperation::Keep, // StencilPassOp
    nullptr, // VS
    nullptr, // HS
    nullptr, // DS
    nullptr, // GS
    nullptr, // PS
    PrimitiveTopologyType::Triangle, // PrimitiveTopology
    false, // Wireframe
    CullMode::Normal, // CullMode
    BlendingMode::Opaque, // BlendMode
};

GPUPipelineState::Description GPUPipelineState::Description::DefaultNoDepth =
{
    false, // DepthEnable
    false, // DepthWriteEnable
    false, // DepthClipEnable
    false, // DepthBoundsEnable
    ComparisonFunc::Less, // DepthFunc
    false, // StencilEnable
    0xff, // StencilReadMask
    0xff, // StencilWriteMask
    ComparisonFunc::Always, // StencilFunc
    StencilOperation::Keep, // StencilFailOp
    StencilOperation::Keep, // StencilDepthFailOp
    StencilOperation::Keep, // StencilPassOp
    nullptr, // VS
    nullptr, // HS
    nullptr, // DS
    nullptr, // GS
    nullptr, // PS
    PrimitiveTopologyType::Triangle, // PrimitiveTopology
    false, // Wireframe
    CullMode::Normal, // CullMode
    BlendingMode::Opaque, // BlendMode
};

GPUPipelineState::Description GPUPipelineState::Description::DefaultFullscreenTriangle =
{
    false, // DepthEnable
    false, // DepthWriteEnable
    false, // DepthClipEnable
    false, // DepthBoundsEnable
    ComparisonFunc::Less, // DepthFunc
    false, // StencilEnable
    0xff, // StencilReadMask
    0xff, // StencilWriteMask
    ComparisonFunc::Always, // StencilFunc
    StencilOperation::Keep, // StencilFailOp
    StencilOperation::Keep, // StencilDepthFailOp
    StencilOperation::Keep, // StencilPassOp
    nullptr, // VS (Set to default quad VS via GPUDevice)
    nullptr, // HS
    nullptr, // DS
    nullptr, // GS
    nullptr, // PS
    PrimitiveTopologyType::Triangle, // PrimitiveTopology
    false, // Wireframe
    CullMode::TwoSided, // CullMode
    BlendingMode::Opaque, // BlendMode
};
// @formatter:on

GPUResource::GPUResource()
    : ScriptingObject(SpawnParams(Guid::New(), TypeInitializer))
{
}

GPUResource::GPUResource(const SpawnParams& params)
    : ScriptingObject(params)
{
}

GPUResource::~GPUResource()
{
#if !BUILD_RELEASE && GPU_ENABLE_RESOURCE_NAMING
    if (_memoryUsage != 0)
        LOG(Error, "{0} '{1}' has not been disposed before destruction", ScriptingObject::ToString(), GetName());
#endif
#if GPU_ENABLE_RESOURCE_NAMING
    if (_namePtr)
        Platform::Free(_namePtr);
#endif
}

uint64 GPUResource::GetMemoryUsage() const
{
    return _memoryUsage;
}

#if GPU_ENABLE_RESOURCE_NAMING

StringView GPUResource::GetName() const
{
    return StringView(_namePtr, _nameSize);
}

void GPUResource::SetName(const StringView& name)
{
    if (_nameCapacity < name.Length() + 1)
    {
        if (_namePtr)
            Platform::Free(_namePtr);
        _nameCapacity = name.Length() + 1;
        _namePtr = (Char*)Platform::Allocate(_nameCapacity * sizeof(Char), 16);
    }
    _nameSize = name.Length();
    if (name.HasChars())
    {
        Platform::MemoryCopy(_namePtr, name.Get(), _nameSize * sizeof(Char));
        _namePtr[_nameSize] = 0;
    }
    OnRenamed();
}

void GPUResource::OnRenamed()
{
}

#elif !BUILD_RELEASE

StringView GPUResource::GetName() const
{
    return StringView::Empty;
}

void GPUResource::SetName(const StringView& name)
{
}

#endif

void GPUResource::ReleaseGPU()
{
    if (_memoryUsage != 0)
    {
        Releasing();
        OnReleaseGPU();
        _memoryUsage = 0;
    }
}

void GPUResource::OnDeviceDispose()
{
    // By default we want to release resource data but keep it alive
    ReleaseGPU();
}

void GPUResource::OnReleaseGPU()
{
}

String GPUResource::ToString() const
{
#if GPU_ENABLE_RESOURCE_NAMING
    if (_namePtr)
        return String(_namePtr, _nameSize);
#endif
    return ScriptingObject::ToString();
}

void GPUResource::OnDeleteObject()
{
    ReleaseGPU();

    ScriptingObject::OnDeleteObject();
}

double GPUResourceView::DummyLastRenderTime = -1;

struct GPUDevice::PrivateData
{
    AssetReference<Shader> QuadShader;
    GPUPipelineState* PS_CopyLinear = nullptr;
    GPUPipelineState* PS_Clear = nullptr;
    GPUPipelineState* PS_DecodeYUY2 = nullptr;
    GPUPipelineState* PS_DecodeNV12 = nullptr;
    GPUBuffer* FullscreenTriangleVB = nullptr;
    AssetReference<Material> DefaultMaterial;
    SoftAssetReference<Material> DefaultDeformableMaterial;
    AssetReference<Texture> DefaultNormalMap;
    AssetReference<Texture> DefaultWhiteTexture;
    AssetReference<Texture> DefaultBlackTexture;
    GPUTasksManager TasksManager;
};

GPUDevice* GPUDevice::Instance = nullptr;

void GPUDevice::OnRequestingExit()
{
    if (Engine::FatalError != FatalErrorType::GPUCrash && 
        Engine::FatalError != FatalErrorType::GPUHang && 
        Engine::FatalError != FatalErrorType::GPUOutOfMemory)
        return;
    // TODO: get and log actual GPU memory used by the engine (API-specific)
    DumpResourcesToLog();
}

GPUDevice::GPUDevice(RendererType type, ShaderProfile profile)
    : ScriptingObject(SpawnParams(Guid::New(), TypeInitializer))
    , _state(DeviceState::Missing)
    , _isRendering(false)
    , _wasVSyncUsed(false)
    , _drawGpuEventIndex(0)
    , _rendererType(type)
    , _shaderProfile(profile)
    , _featureLevel(RenderTools::GetFeatureLevel(profile))
    , _res(New<PrivateData>())
    , _resources(1024)
    , TotalGraphicsMemory(0)
    , IsDebugToolAttached(false)
    , QuadShader(nullptr)
    , CurrentTask(nullptr)
{
    ASSERT_LOW_LAYER(_rendererType != RendererType::Unknown);
}

GPUDevice::~GPUDevice()
{
    Delete(_res);

    // Unlink
    ASSERT(GPUDevice::Instance == this);
    GPUDevice::Instance = nullptr;
}

bool GPUDevice::Init()
{
    // Clamp texture limits (eg. if driver reports higher value)
    Limits.MaximumTexture1DSize = Math::Min(Limits.MaximumTexture1DSize, GPU_MAX_TEXTURE_SIZE);
    Limits.MaximumTexture2DSize = Math::Min(Limits.MaximumTexture2DSize, GPU_MAX_TEXTURE_SIZE);
    Limits.MaximumTexture3DSize = Math::Min(Limits.MaximumTexture3DSize, GPU_MAX_TEXTURE_SIZE);
    Limits.MaximumTextureCubeSize = Math::Min(Limits.MaximumTextureCubeSize, GPU_MAX_TEXTURE_SIZE);
    Limits.MaximumTexture1DArraySize = Math::Min(Limits.MaximumTexture1DArraySize, GPU_MAX_TEXTURE_ARRAY_SIZE);
    Limits.MaximumTexture2DArraySize = Math::Min(Limits.MaximumTexture2DArraySize, GPU_MAX_TEXTURE_ARRAY_SIZE);
    Limits.MaximumMipLevelsCount = Math::Min(Limits.MaximumMipLevelsCount, GPU_MAX_TEXTURE_MIP_LEVELS);

    _res->TasksManager.SetExecutor(CreateTasksExecutor());
    LOG(Info, "Total graphics memory: {0}", Utilities::BytesToText(TotalGraphicsMemory));
    for (const auto& videoOutput : VideoOutputs)
        LOG(Info, "Video output '{0}' {1}x{2} {3} Hz", videoOutput.Name, videoOutput.Width, videoOutput.Height, videoOutput.RefreshRate);
    Engine::RequestingExit.Bind<GPUDevice, &GPUDevice::OnRequestingExit>(this);
    return false;
}

bool GPUDevice::LoadContent()
{
    // Load internal rendering shader for GPU device low-level impl
    _res->QuadShader = Content::LoadAsyncInternal<Shader>(TEXT("Shaders/Quad"));
    if (_res->QuadShader == nullptr || _res->QuadShader->WaitForLoaded())
        return true;
    QuadShader = _res->QuadShader->GetShader();
    GPUPipelineState::Description::DefaultFullscreenTriangle.VS = QuadShader->GetVS("VS");
    _res->PS_CopyLinear = CreatePipelineState();
    GPUPipelineState::Description desc = GPUPipelineState::Description::DefaultFullscreenTriangle;
    desc.PS = QuadShader->GetPS("PS_CopyLinear");
    if (_res->PS_CopyLinear->Init(desc))
        return true;
    _res->PS_Clear = CreatePipelineState();
    desc.PS = QuadShader->GetPS("PS_Clear");
    if (_res->PS_Clear->Init(desc))
        return true;

    // Create fullscreen triangle vertex buffer
    {
        // Create vertex buffer
        // @formatter:off
        static float vb[] =
        {
            //   XY            UV
            -1.0f, -1.0f,  0.0f,  1.0f,
            -1.0f,  3.0f,  0.0f, -1.0f,
             3.0f, -1.0f,  2.0f,  1.0f,
        };
        // @formatter:on
        _res->FullscreenTriangleVB = CreateBuffer(TEXT("QuadVB"));
        auto layout = GPUVertexLayout::Get({
            { VertexElement::Types::Position, 0, 0, 0, PixelFormat::R32G32_Float },
            { VertexElement::Types::TexCoord, 0, 8, 0, PixelFormat::R32G32_Float },
        });
        if (_res->FullscreenTriangleVB->Init(GPUBufferDescription::Vertex(layout, 16, 3, vb)))
            return true;
    }

    // Load default material
    _res->DefaultMaterial = Content::LoadAsyncInternal<Material>(TEXT("Engine/DefaultMaterial"));
    if (_res->DefaultMaterial == nullptr)
        return true;
    _res->DefaultDeformableMaterial = Guid(0x639e12c0, 0x42d34bae, 0x89dd8b81, 0x7e1efc2d);

    // Load default normal map
    _res->DefaultNormalMap = Content::LoadAsyncInternal<Texture>(TEXT("Engine/Textures/NormalTexture"));
    if (_res->DefaultNormalMap == nullptr)
        return true;

    // Load default solid white
    _res->DefaultWhiteTexture = Content::LoadAsyncInternal<Texture>(TEXT("Engine/Textures/WhiteTexture"));
    if (_res->DefaultWhiteTexture == nullptr)
        return true;

    // Load default solid black
    _res->DefaultBlackTexture = Content::LoadAsyncInternal<Texture>(TEXT("Engine/Textures/BlackTexture"));
    if (_res->DefaultBlackTexture == nullptr)
        return true;

    return false;
}

bool GPUDevice::CanDraw()
{
    return true;
}

void GPUDevice::AddResource(GPUResource* resource)
{
    _resourcesLock.Lock();
    ASSERT(resource && !_resources.Contains(resource));
    _resources.Add(resource);
    _resourcesLock.Unlock();
}

void GPUDevice::RemoveResource(GPUResource* resource)
{
    _resourcesLock.Lock();
    ASSERT(resource && _resources.Contains(resource));
    _resources.Remove(resource);
    _resourcesLock.Unlock();
}

void GPUDevice::DumpResourcesToLog() const
{
    StringBuilder output;
    _resourcesLock.Lock();

    output.AppendFormat(TEXT("GPU Resources dump. Count: {0}, total GPU memory used: {1}"), _resources.Count(), Utilities::BytesToText(GetMemoryUsage()));
    output.AppendLine();
    output.AppendLine();

    const bool printTypes[(int32)GPUResourceType::MAX] =
    {
        true, // RenderTarget
        true, // Texture
        true, // CubeTexture
        true, // VolumeTexture
        true, // Buffer
        false, // Shader
        false, // PipelineState
        false, // Descriptor
        false, // Query
        false, // Sampler
    };
    for (int32 typeIndex = 0; typeIndex < (int32)GPUResourceType::MAX; typeIndex++)
    {
        const auto type = static_cast<GPUResourceType>(typeIndex);
        const auto printType = printTypes[typeIndex];

        // Get resource sof a given type
        uint64 memUsage = 0;
        struct Resource
        {
            const GPUResource* Object;
            uint64 MemoryUsage;

            bool operator<(const Resource& other) const
            {
                if (MemoryUsage != other.MemoryUsage)
                    return MemoryUsage > other.MemoryUsage;
#if GPU_ENABLE_RESOURCE_NAMING
                return Object->GetName().Compare(other.Object->GetName()) > 0;
#else
                return (uintptr)Object < (uintptr)other.Object;
#endif
            }
        };
        Array<Resource> resources;
        for (int32 i = 0; i < _resources.Count(); i++)
        {
            const GPUResource* resource = _resources[i];
            if (resource->GetResourceType() == type && resource->GetMemoryUsage() != 0)
            {
                resources.Add({ resource, resource->GetMemoryUsage() });
            }
        }
        if (resources.IsEmpty())
            continue;
        output.AppendFormat(TEXT("> {0}:"), ScriptingEnum::ToString(type));
        output.AppendLine();

        // Sort them by size
        Sorting::QuickSort(resources);

        // Print resources
        for (auto e : resources)
        {
            memUsage += e.MemoryUsage;
            if (!printType)
                continue;
            output.Append(TEXT("  "));
            output.Append(Utilities::BytesToText(e.MemoryUsage));
            output.Append(TEXT(", "));
            if (e.Object->Is<GPUTexture>())
            {
                auto texture = (GPUTexture*)e.Object;
                auto& desc = texture->GetDescription();
                output.AppendFormat(TEXT("Size: {}x{}x{}[{}], "), desc.Width, desc.Height, desc.Depth, desc.ArraySize);
                if (texture->ResidentMipLevels() == desc.MipLevels)
                    output.AppendFormat(TEXT("Mips: {}, "), desc.MipLevels);
                else
                    output.AppendFormat(TEXT("Mips: {}/{}, "), texture->ResidentMipLevels(), desc.MipLevels);
#if GPU_ENABLE_RESOURCE_NAMING
                auto name = texture->GetName();
#else
                StringView name;
#endif
                output.AppendFormat(TEXT("Format: {}, Flags: {}, {}"), ScriptingEnum::ToString(desc.Format), ScriptingEnum::ToStringFlags(desc.Flags), name);
            }
            else if (e.Object->Is<GPUBuffer>())
            {
                auto buffer = (GPUBuffer*)e.Object;
                auto& desc = buffer->GetDescription();
                output.AppendFormat(TEXT("Stride: {} bytes, "), desc.Stride);
                if (desc.Format != PixelFormat::Unknown)
                    output.AppendFormat(TEXT("Format: {}, "), ScriptingEnum::ToString(desc.Format));
                if (desc.Usage != GPUResourceUsage::Default)
                    output.AppendFormat(TEXT("Usage: {}, "), ScriptingEnum::ToString(desc.Usage));
#if GPU_ENABLE_RESOURCE_NAMING
                auto name = buffer->GetName();
#else
                StringView name;
#endif
                output.AppendFormat(TEXT("Flags: {}, {}"), ScriptingEnum::ToStringFlags(desc.Flags), name);
            }
            else
            {
                output.Append(e.Object->ToString());
            }
            output.AppendLine();
        }

        output.AppendFormat(TEXT("Total count: {0}, memory usage: {1}"), resources.Count(), Utilities::BytesToText(memUsage));
        output.AppendLine();
        output.AppendLine();
    }

    _resourcesLock.Unlock();
    LOG_STR(Info, output.ToStringView());
}

void GPUDevice::DumpResources()
{
    if (GPUDevice::Instance)
        GPUDevice::Instance->DumpResourcesToLog();
}

extern void ClearVertexLayoutCache();

void GPUDevice::preDispose()
{
    Locker.Lock();
    RenderTargetPool::Flush();

    // Release resources
    _res->DefaultMaterial = nullptr;
    _res->DefaultDeformableMaterial = nullptr;
    _res->DefaultNormalMap = nullptr;
    _res->DefaultWhiteTexture = nullptr;
    _res->DefaultBlackTexture = nullptr;
    SAFE_DELETE_GPU_RESOURCE(_res->PS_CopyLinear);
    SAFE_DELETE_GPU_RESOURCE(_res->PS_Clear);
    SAFE_DELETE_GPU_RESOURCE(_res->PS_DecodeYUY2);
    SAFE_DELETE_GPU_RESOURCE(_res->FullscreenTriangleVB);
    ClearVertexLayoutCache();

    Locker.Unlock();

    // Release GPU resources memory and unlink from device
    // Note: after that no GPU resources should be used/created, only deleted
    _resourcesLock.Lock();
    for (int32 i = _resources.Count() - 1; i >= 0 && i < _resources.Count(); i--)
    {
        _resources[i]->OnDeviceDispose();
    }
    _resources.Clear();
    _resourcesLock.Unlock();
}

void GPUDevice::DrawBegin()
{
    // Set flag
    _isRendering = true;

    // Clear stats
    RenderTask::TasksDoneLastFrame = 0;
}

void GPUDevice::DrawEnd()
{
    PROFILE_CPU_NAMED("Present");

    // Check if use VSync
    bool useVSync = Graphics::UseVSync;
    if (CommandLine::Options.NoVSync.HasValue())
        useVSync = !CommandLine::Options.NoVSync.GetValue();
    else if (CommandLine::Options.VSync.HasValue())
        useVSync = CommandLine::Options.VSync.GetValue();

    // Find index of the last rendered window task (use vsync only on the last window)
    int32 lastWindowIndex = -1;
    for (int32 i = RenderTask::Tasks.Count() - 1; i >= 0; i--)
    {
        const auto task = RenderTask::Tasks[i];
        if (task && task->LastUsedFrame == Engine::FrameCount && task->SwapChain && task->SwapChain->IsReady())
        {
            lastWindowIndex = i;
            break;
        }
    }

    // Call present on all used tasks
    int32 presentCount = 0;
    bool anyVSync = false;
#if COMPILE_WITH_PROFILER
    const double presentStart = Platform::GetTimeSeconds();
#endif
    for (int32 i = 0; i < RenderTask::Tasks.Count(); i++)
    {
        const auto task = RenderTask::Tasks[i];
        if (task && task->LastUsedFrame == Engine::FrameCount && task->SwapChain && task->SwapChain->IsReady())
        {
            bool vsync = useVSync;
            if (lastWindowIndex != i)
            {
                // Perform VSync only on the last window
                vsync = false;
            }
            else
            {
                // End profiler timer queries
#if COMPILE_WITH_PROFILER
                ProfilerGPU::OnPresent();
#endif
            }

            anyVSync |= vsync;
            task->OnPresent(vsync);
            presentCount++;
        }
    }

    // If no `Present` calls has been performed just execute GPU commands
    if (presentCount == 0)
    {
        // End profiler timer queries
#if COMPILE_WITH_PROFILER
        ProfilerGPU::OnPresent();
#endif
        GetMainContext()->Flush();
    }
#if COMPILE_WITH_PROFILER
    const double presentEnd = Platform::GetTimeSeconds();
    ProfilerGPU::OnPresentTime((float)((presentEnd - presentStart) * 1000.0));
#endif
    GetMainContext()->OnPresent();

    _wasVSyncUsed = anyVSync;
    _isRendering = false;

    RenderTargetPool::Flush();
}

void GPUDevice::RenderBegin()
{
#if COMPILE_WITH_PROFILER
    _drawGpuEventIndex = ProfilerGPU::BeginEvent(TEXT("Draw"));
#endif
}

void GPUDevice::RenderEnd()
{
#if COMPILE_WITH_PROFILER
    ProfilerGPU::EndEvent(_drawGpuEventIndex);
#endif
}

GPUTasksContext* GPUDevice::CreateTasksContext()
{
    return New<GPUTasksContext>(this);
}

GPUTasksExecutor* GPUDevice::CreateTasksExecutor()
{
    return New<DefaultGPUTasksExecutor>();
}

#if !COMPILE_WITHOUT_CSHARP

#include "Engine/Scripting/ManagedCLR/MUtils.h"

void* GPUDevice::GetResourcesInternal()
{
    _resourcesLock.Lock();
    MArray* result = MCore::Array::New(GPUResource::TypeInitializer.GetClass(), _resources.Count());
    int32 i = 0;
    for (const auto& e : _resources)
        MCore::GC::WriteArrayRef(result, e->GetOrCreateManagedInstance(), i++);
    _resourcesLock.Unlock();
    return result;
}

#endif

void GPUDevice::Draw()
{
    PROFILE_MEM(Graphics);
    DrawBegin();

    auto context = GetMainContext();

    // Begin frame
    context->FrameBegin();
    RenderBegin();
    _res->TasksManager.FrameBegin();
    Render2D::BeginFrame();

    // Perform actual drawing
    Engine::Draw();
    EngineService::OnDraw();
    RenderTask::DrawAll();

    // End frame
    Render2D::EndFrame();
    _res->TasksManager.FrameEnd();
    RenderEnd();
    context->FrameEnd();

    DrawEnd();
}

void GPUDevice::Dispose()
{
    RenderList::CleanupCache();
    VideoOutputs.Resize(0);
    VideoOutputModes.Resize(0);
}

uint64 GPUDevice::GetMemoryUsage() const
{
    uint64 result = 0;
    _resourcesLock.Lock();
    for (int32 i = 0; i < _resources.Count(); i++)
        result += _resources[i]->GetMemoryUsage();
    _resourcesLock.Unlock();
    return result;
}

Array<GPUResource*> GPUDevice::GetResources() const
{
    _resourcesLock.Lock();
    Array<GPUResource*> result = _resources;
    _resourcesLock.Unlock();
    return result;
}

GPUTasksManager* GPUDevice::GetTasksManager() const
{
    return &_res->TasksManager;
}

MaterialBase* GPUDevice::GetDefaultMaterial() const
{
    return _res->DefaultMaterial;
}

MaterialBase* GPUDevice::GetDefaultDeformableMaterial() const
{
    return _res->DefaultDeformableMaterial.Get();
}

GPUTexture* GPUDevice::GetDefaultNormalMap() const
{
    return _res->DefaultNormalMap ? _res->DefaultNormalMap->GetTexture() : nullptr;
}

GPUTexture* GPUDevice::GetDefaultWhiteTexture() const
{
    return _res->DefaultWhiteTexture ? _res->DefaultWhiteTexture->GetTexture() : nullptr;
}

GPUTexture* GPUDevice::GetDefaultBlackTexture() const
{
    return _res->DefaultBlackTexture ? _res->DefaultBlackTexture->GetTexture() : nullptr;
}

GPUPipelineState* GPUDevice::GetCopyLinearPS() const
{
    return _res->PS_CopyLinear;
}

GPUPipelineState* GPUDevice::GetClearPS() const
{
    return _res->PS_Clear;
}

GPUPipelineState* GPUDevice::GetDecodeYUY2PS() const
{
    if (_res->PS_DecodeYUY2 == nullptr)
    {
        auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
        psDesc.PS = QuadShader->GetPS("PS_DecodeYUY2");
        _res->PS_DecodeYUY2 = const_cast<GPUDevice*>(this)->CreatePipelineState();
        _res->PS_DecodeYUY2->Init(psDesc);
    }
    return _res->PS_DecodeYUY2;
}

GPUPipelineState* GPUDevice::GetDecodeNV12PS() const
{
    if (_res->PS_DecodeNV12 == nullptr)
    {
        auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
        psDesc.PS = QuadShader->GetPS("PS_DecodeNV12");
        _res->PS_DecodeNV12 = const_cast<GPUDevice*>(this)->CreatePipelineState();
        _res->PS_DecodeNV12->Init(psDesc);
    }
    return _res->PS_DecodeNV12;
}

GPUBuffer* GPUDevice::GetFullscreenTriangleVB() const
{
    return _res->FullscreenTriangleVB;
}
