//--------------------------------------------------------------------------------------
// File: GraphicsMemory.cpp
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// http://go.microsoft.com/fwlink/?LinkId=248929
//--------------------------------------------------------------------------------------

#include "pch.h"

#include "GraphicsMemory.h"
#include "PlatformHelpers.h"

using namespace DirectX;
using Microsoft::WRL::ComPtr;


namespace
{
    template <typename T> __forceinline T AlignUp(T value, size_t alignment)
    {
        assert(((alignment - 1) & alignment) == 0);
        return static_cast<T>( (static_cast<size_t>(value) + alignment - 1) & ~(alignment - 1) );
    }
}


#if defined(_XBOX_ONE) && defined(_TITLE)

//======================================================================================
// Xbox One Direct3D 11.x
//======================================================================================

class GraphicsMemory::Impl
{
public:
    Impl(GraphicsMemory* owner) :
        mOwner(owner),
        mCurrentFrame(0)
    {
        if (s_graphicsMemory)
        {
            throw std::exception("GraphicsMemory is a singleton");
        }

        s_graphicsMemory = this;
    }

    ~Impl()
    {
        if (mDevice && mDeviceContext)
        {
            UINT64 finalFence = mDeviceContext->InsertFence(0);

            while (mDevice->IsFencePending(finalFence))
            {
                SwitchToThread();
            }

            mDeviceContext.Reset();
            mDevice.Reset();
        }

        s_graphicsMemory = nullptr;
    }

    void Initialize(_In_ ID3D11DeviceX* device, UINT backBufferCount)
    {
        assert( device != 0 );
        mDevice = device;

        device->GetImmediateContextX( mDeviceContext.GetAddressOf() );

        mFrames.resize( backBufferCount );
    }

    void* Allocate(_In_opt_ ID3D11DeviceContext* deviceContext, size_t size, int alignment)
    {
        // Currently use a single global allocator instead of a per-context allocator
        UNREFERENCED_PARAMETER(deviceContext);

        std::lock_guard<std::mutex> lock(mGuard);

        return mFrames[mCurrentFrame].Allocate(size, alignment);
    }

    void Commit()
    {
        std::lock_guard<std::mutex> lock(mGuard);

        mFrames[mCurrentFrame].mFence = mDeviceContext->InsertFence(D3D11_INSERT_FENCE_NO_KICKOFF);

        ++mCurrentFrame;
        if (mCurrentFrame >= mFrames.size())
        {
            mCurrentFrame = 0;
        }

        mFrames[mCurrentFrame].WaitOnFence(mDevice.Get());

        mFrames[mCurrentFrame].Clear();
    }

    GraphicsMemory*  mOwner;

    std::mutex mGuard;

    struct MemoryPage
    {
        MemoryPage() : mPageSize(0), mGrfxMemory(nullptr) {}

        void Initialize(size_t reqSize)
        {
            mPageSize = 0x100000; // 1 MB general pages for Xbox One
            if (mPageSize < reqSize)
            {
                mPageSize = AlignUp(reqSize, 65536);
            }

            mGrfxMemory = VirtualAlloc(nullptr, mPageSize,
                                       MEM_LARGE_PAGES | MEM_GRAPHICS | MEM_RESERVE | MEM_COMMIT,
                                       PAGE_WRITECOMBINE | PAGE_READWRITE | PAGE_GPU_READONLY);
            if (!mGrfxMemory)
                throw  std::bad_alloc();
        }

        size_t mPageSize;
        void* mGrfxMemory;
    };

    struct MemoryFrame
    {
        MemoryFrame() : mCurOffset(0), mFence(0) {}

        ~MemoryFrame() { Clear(); }

        UINT mCurOffset;

        UINT64 mFence;

        void* Allocate(size_t size, size_t alignment)
        {
            size_t alignedSize = AlignUp(size, alignment);

            if (mPages.empty())
            {
                MemoryPage newPage;
                newPage.Initialize(alignedSize);

                mCurOffset = 0;

                mPages.emplace_back(newPage);
            }
            else
            {
                mCurOffset = AlignUp(mCurOffset, alignment);

                if (mCurOffset + alignedSize > mPages.front().mPageSize)
                {
                    MemoryPage newPage;
                    newPage.Initialize(alignedSize);

                    mCurOffset = 0;

                    mPages.emplace_front(newPage);
                }
            }

            void* ptr = static_cast<uint8_t*>(mPages.front().mGrfxMemory) + mCurOffset;

            mCurOffset += static_cast<UINT>( alignedSize );

            return ptr;
        }

        void WaitOnFence(ID3D11DeviceX* device)
        {
            if (mFence)
            {
                while (device->IsFencePending(mFence))
                {
                    SwitchToThread();
                }

                mFence = 0;
            }
        }

        void Clear()
        {
            for (auto it = mPages.begin(); it != mPages.end(); ++it)
            {
                if (it->mGrfxMemory)
                {
                    VirtualFree(it->mGrfxMemory, 0, MEM_RELEASE);
                    it->mGrfxMemory = nullptr;
                }
            }

            mPages.clear();

            mCurOffset = 0;
        }

        std::list<MemoryPage> mPages;
    };

    UINT mCurrentFrame;
    std::vector<MemoryFrame> mFrames;

    ComPtr<ID3D11DeviceX> mDevice;
    ComPtr<ID3D11DeviceContextX> mDeviceContext;
    
    static GraphicsMemory::Impl* s_graphicsMemory;
};

GraphicsMemory::Impl* GraphicsMemory::Impl::s_graphicsMemory = nullptr;

#else

//======================================================================================
// Null allocator for standard Direct3D
//======================================================================================

class GraphicsMemory::Impl
{
public:
    Impl(GraphicsMemory* owner) :
        mOwner(owner)
    {
        if (s_graphicsMemory)
        {
            throw std::exception("GraphicsMemory is a singleton");
        }

        s_graphicsMemory = this;
    }

    ~Impl()
    {
        s_graphicsMemory = nullptr;
    }

    void Initialize(_In_ ID3D11Device* device, UINT backBufferCount)
    {
        UNREFERENCED_PARAMETER(device);
        UNREFERENCED_PARAMETER(backBufferCount);
    }

    void* Allocate(_In_opt_ ID3D11DeviceContext* context, size_t size, int alignment)
    {
        UNREFERENCED_PARAMETER(context);
        UNREFERENCED_PARAMETER(size);
        UNREFERENCED_PARAMETER(alignment);
        return nullptr;
    }

    void Commit()
    {
    }

    GraphicsMemory*  mOwner;

    static GraphicsMemory::Impl* s_graphicsMemory;
};

GraphicsMemory::Impl* GraphicsMemory::Impl::s_graphicsMemory = nullptr;

#endif


//--------------------------------------------------------------------------------------

#pragma warning( disable : 4355 )

// Public constructor.
#if defined(_XBOX_ONE) && defined(_TITLE)
GraphicsMemory::GraphicsMemory(_In_ ID3D11DeviceX* device, UINT backBufferCount)
#else
GraphicsMemory::GraphicsMemory(_In_ ID3D11Device* device, UINT backBufferCount)
#endif
    : pImpl(new Impl(this))
{
    pImpl->Initialize(device, backBufferCount);
}


// Move constructor.
GraphicsMemory::GraphicsMemory(GraphicsMemory&& moveFrom)
    : pImpl(std::move(moveFrom.pImpl))
{
    pImpl->mOwner = this;
}


// Move assignment.
GraphicsMemory& GraphicsMemory::operator= (GraphicsMemory&& moveFrom)
{
    pImpl = std::move(moveFrom.pImpl);
    pImpl->mOwner = this;
    return *this;
}


// Public destructor.
GraphicsMemory::~GraphicsMemory()
{
}


void* GraphicsMemory::Allocate(_In_opt_ ID3D11DeviceContext* context, size_t size, int alignment)
{
    return pImpl->Allocate(context, size, alignment);
}


void GraphicsMemory::Commit()
{
    pImpl->Commit();
}


GraphicsMemory& GraphicsMemory::Get()
{
    if (!Impl::s_graphicsMemory || !Impl::s_graphicsMemory->mOwner)
        throw std::exception("GraphicsMemory singleton not created");

    return *Impl::s_graphicsMemory->mOwner;
}
