// Copyright 2020 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "src/dawn/native/d3d12/ResidencyManagerD3D12.h"

#include <algorithm>
#include <vector>

#include "src/dawn/native/d3d/D3DError.h"
#include "src/dawn/native/d3d12/DeviceD3D12.h"
#include "src/dawn/native/d3d12/Forward.h"
#include "src/dawn/native/d3d12/HeapD3D12.h"
#include "src/dawn/native/d3d12/PhysicalDeviceD3D12.h"
#include "src/dawn/native/d3d12/QueueD3D12.h"
#include "src/utils/compiler.h"

namespace dawn::native::d3d12 {

ResidencyManager::ResidencyManager(Device* device)
    : mDevice(device),
      mResidencyManagementEnabled(device->IsToggleEnabled(Toggle::UseD3D12ResidencyManagement)) {
    UpdateVideoMemoryInfo();
}

// Increments number of locks on a heap to ensure the heap remains resident.
MaybeError ResidencyManager::LockAllocation(Pageable* pageable) {
    if (!mResidencyManagementEnabled) {
        return {};
    }

    // If the heap isn't already resident, make it resident.
    if (!pageable->IsInResidencyLRUCache() && !pageable->IsResidencyLocked()) {
        ID3D12Pageable* d3d12Pageable = pageable->GetD3D12Pageable();
        uint64_t size = pageable->GetSize();

        DAWN_TRY(MakeAllocationsResident(GetMemorySegmentInfo(pageable->GetMemorySegment()), size,
                                         1, &d3d12Pageable));
    }

    // Since we can't evict the heap, it's unnecessary to track the heap in the LRU Cache.
    if (pageable->IsInResidencyLRUCache()) {
        pageable->RemoveFromList();
    }

    pageable->IncrementResidencyLock();

    return {};
}

// Decrements number of locks on a heap. When the number of locks becomes zero, the heap is
// inserted into the LRU cache and becomes eligible for eviction.
void ResidencyManager::UnlockAllocation(Pageable* pageable) {
    if (!mResidencyManagementEnabled) {
        return;
    }

    DAWN_ASSERT(pageable->IsResidencyLocked());
    DAWN_ASSERT(!pageable->IsInResidencyLRUCache());
    pageable->DecrementResidencyLock();

    // If another lock still exists on the heap, nothing further should be done.
    if (pageable->IsResidencyLocked()) {
        return;
    }

    // When all locks have been removed, the resource remains resident and becomes tracked in
    // the corresponding LRU.
    TrackResidentAllocation(pageable);
}

// Returns the appropriate MemorySegmentInfo for a given MemorySegment.
ResidencyManager::MemorySegmentInfo* ResidencyManager::GetMemorySegmentInfo(
    MemorySegment memorySegment) {
    switch (memorySegment) {
        case MemorySegment::Local:
            return &mVideoMemoryInfo.local;
        case MemorySegment::NonLocal:
            DAWN_ASSERT(!mDevice->GetDeviceInfo().isUMA);
            return &mVideoMemoryInfo.nonLocal;
        default:
            DAWN_UNREACHABLE();
    }
}

// Allows an application component external to Dawn to cap Dawn's residency budgets to prevent
// competition for device memory. Returns the amount of memory reserved, which may be less
// that the requested reservation when under pressure.
uint64_t ResidencyManager::SetExternalMemoryReservation(MemorySegment segment,
                                                        uint64_t requestedReservationSize) {
    MemorySegmentInfo* segmentInfo = GetMemorySegmentInfo(segment);

    segmentInfo->externalRequest = requestedReservationSize;

    UpdateMemorySegmentInfo(segmentInfo);

    return segmentInfo->externalReservation;
}

void ResidencyManager::UpdateVideoMemoryInfo() {
    UpdateMemorySegmentInfo(&mVideoMemoryInfo.local);
    if (!mDevice->GetDeviceInfo().isUMA) {
        UpdateMemorySegmentInfo(&mVideoMemoryInfo.nonLocal);
    }
}

void ResidencyManager::UpdateMemorySegmentInfo(MemorySegmentInfo* segmentInfo) {
    DXGI_QUERY_VIDEO_MEMORY_INFO queryVideoMemoryInfo;

    ToBackend(mDevice->GetPhysicalDevice())
        ->GetHardwareAdapter()
        ->QueryVideoMemoryInfo(0, segmentInfo->dxgiSegment, &queryVideoMemoryInfo);

    // The video memory budget provided by QueryVideoMemoryInfo is defined by the operating
    // system, and may be lower than expected in certain scenarios. Under memory pressure, we
    // cap the external reservation to half the available budget, which prevents the external
    // component from consuming a disproportionate share of memory and ensures that Dawn can
    // continue to make forward progress. Note the choice to halve memory is arbitrarily chosen
    // and subject to future experimentation.
    segmentInfo->externalReservation =
        std::min(queryVideoMemoryInfo.Budget / 2, segmentInfo->externalRequest);

    segmentInfo->usage = queryVideoMemoryInfo.CurrentUsage - segmentInfo->externalReservation;

    // If we're restricting the budget for testing, leave the budget as is.
    if (mRestrictBudgetForTesting) {
        return;
    }

    // We cap Dawn's budget to 95% of the provided budget. Leaving some budget unused
    // decreases fluctuations in the operating-system-defined budget, which improves stability
    // for both Dawn and other applications on the system. Note the value of 95% is arbitrarily
    // chosen and subject to future experimentation.
    static constexpr float kBudgetCap = 0.95f;
    segmentInfo->budget = static_cast<uint64_t>(
        (queryVideoMemoryInfo.Budget - segmentInfo->externalReservation) * kBudgetCap);
}

// Removes a heap from the LRU and returns the least recently used heap when possible. Returns
// nullptr when nothing further can be evicted.
ResultOrError<Pageable*> ResidencyManager::RemoveSingleEntryFromLRU(
    MemorySegmentInfo* memorySegment) {
    // If the LRU is empty, return nullptr to allow execution to continue. Note that fully
    // emptying the LRU is undesirable, because it can mean either 1) the LRU is not accurately
    // accounting for Dawn's GPU allocations, or 2) a component external to Dawn is using all of
    // the process budget and starving Dawn, which will cause thrash.
    if (memorySegment->lruCache.empty()) {
        return nullptr;
    }

    Pageable* pageable = memorySegment->lruCache.head()->value();

    ExecutionSerial lastSubmissionSerial = pageable->GetLastSubmission();

    // If the next candidate for eviction was inserted into the LRU during the current serial,
    // it is because more memory is being used in a single command list than is available.
    // In this scenario, we cannot make any more resources resident and thrashing must occur.
    if (lastSubmissionSerial == mDevice->GetQueue()->GetPendingCommandSerial()) {
        return nullptr;
    }

    // We must ensure that any previous use of a resource has completed before the resource can
    // be evicted.
    DAWN_TRY(ToBackend(mDevice->GetQueue())->WaitForSerial(lastSubmissionSerial));

    pageable->RemoveFromList();
    return pageable;
}

MaybeError ResidencyManager::EnsureCanAllocate(uint64_t allocationSize,
                                               MemorySegment memorySegment) {
    if (!mResidencyManagementEnabled) {
        return {};
    }

    [[maybe_unused]] uint64_t bytesEvicted;
    DAWN_TRY_ASSIGN(bytesEvicted,
                    EnsureCanMakeResident(allocationSize, GetMemorySegmentInfo(memorySegment)));
    return {};
}

// Any time we need to make something resident, we must check that we have enough free memory to
// make the new object resident while also staying within budget. If there isn't enough
// memory, we should evict until there is. Returns the number of bytes evicted.
ResultOrError<uint64_t> ResidencyManager::EnsureCanMakeResident(uint64_t sizeToMakeResident,
                                                                MemorySegmentInfo* memorySegment) {
    DAWN_ASSERT(mResidencyManagementEnabled);

    UpdateMemorySegmentInfo(memorySegment);

    uint64_t memoryUsageAfterMakeResident = sizeToMakeResident + memorySegment->usage;

    // Return when we can call MakeResident and remain under budget.
    if (memoryUsageAfterMakeResident < memorySegment->budget) {
        return 0;
    }

    std::vector<ID3D12Pageable*> resourcesToEvict;
    uint64_t sizeNeededToBeUnderBudget = memoryUsageAfterMakeResident - memorySegment->budget;
    uint64_t sizeEvicted = 0;
    while (sizeEvicted < sizeNeededToBeUnderBudget) {
        Pageable* pageable;
        DAWN_TRY_ASSIGN(pageable, RemoveSingleEntryFromLRU(memorySegment));

        // If no heap was returned, then nothing more can be evicted.
        if (pageable == nullptr) {
            break;
        }

        sizeEvicted += pageable->GetSize();
        resourcesToEvict.push_back(pageable->GetD3D12Pageable());
    }

    if (resourcesToEvict.size() > 0) {
        DAWN_TRY(CheckHRESULT(
            mDevice->GetD3D12Device()->Evict(static_cast<uint32_t>(resourcesToEvict.size()),
                                             resourcesToEvict.data()),
            "Evicting resident heaps to free memory"));
    }

    return sizeEvicted;
}

// Given a list of heaps that are pending usage, this function will estimate memory needed,
// evict resources until enough space is available, then make resident any heaps scheduled for
// usage.
MaybeError ResidencyManager::EnsureHeapsAreResident(Heap** heaps, size_t heapCount) {
    if (!mResidencyManagementEnabled) {
        return {};
    }

    std::vector<ID3D12Pageable*> localHeapsToMakeResident;
    std::vector<ID3D12Pageable*> nonLocalHeapsToMakeResident;
    uint64_t localSizeToMakeResident = 0;
    uint64_t nonLocalSizeToMakeResident = 0;

    ExecutionSerial pendingCommandSerial = mDevice->GetQueue()->GetPendingCommandSerial();
    for (size_t i = 0; i < heapCount; i++) {
        Heap* heap = DAWN_UNSAFE_TODO(heaps[i]);

        // Heaps that are locked resident are not tracked in the LRU cache.
        if (heap->IsResidencyLocked()) {
            continue;
        }

        if (heap->IsInResidencyLRUCache()) {
            // If the heap is already in the LRU, we must remove it and append again below to
            // update its position in the LRU.
            heap->RemoveFromList();
        } else {
            if (heap->GetMemorySegment() == MemorySegment::Local) {
                localSizeToMakeResident += heap->GetSize();
                localHeapsToMakeResident.push_back(heap->GetD3D12Pageable());
            } else {
                nonLocalSizeToMakeResident += heap->GetSize();
                nonLocalHeapsToMakeResident.push_back(heap->GetD3D12Pageable());
            }
        }

        // If we submit a command list to the GPU, we must ensure that heaps referenced by that
        // command list stay resident at least until that command list has finished execution.
        // Setting this serial unnecessarily can leave the LRU in a state where nothing is
        // eligible for eviction, even though some evictions may be possible.
        heap->SetLastSubmission(pendingCommandSerial);

        // Insert the heap into the appropriate LRU.
        TrackResidentAllocation(heap);
    }

    if (localSizeToMakeResident > 0) {
        return MakeAllocationsResident(&mVideoMemoryInfo.local, localSizeToMakeResident,
                                       localHeapsToMakeResident.size(),
                                       localHeapsToMakeResident.data());
    }

    if (nonLocalSizeToMakeResident > 0) {
        DAWN_ASSERT(!mDevice->GetDeviceInfo().isUMA);
        return MakeAllocationsResident(&mVideoMemoryInfo.nonLocal, nonLocalSizeToMakeResident,
                                       nonLocalHeapsToMakeResident.size(),
                                       nonLocalHeapsToMakeResident.data());
    }

    return {};
}

MaybeError ResidencyManager::MakeAllocationsResident(MemorySegmentInfo* segment,
                                                     uint64_t sizeToMakeResident,
                                                     uint64_t numberOfObjectsToMakeResident,
                                                     ID3D12Pageable** allocations) {
    [[maybe_unused]] uint64_t bytesEvicted;
    DAWN_TRY_ASSIGN(bytesEvicted, EnsureCanMakeResident(sizeToMakeResident, segment));

    // Note that MakeResident is a synchronous function and can add a significant
    // overhead to command recording. In the future, it may be possible to decrease this
    // overhead by using MakeResident on a secondary thread, or by instead making use of
    // the EnqueueMakeResident function (which is not available on all Windows 10
    // platforms).
    HRESULT hr = mDevice->GetD3D12Device()->MakeResident(
        static_cast<uint32_t>(numberOfObjectsToMakeResident), allocations);

    // A MakeResident call can fail if there's not enough available memory. This
    // could occur when there's significant fragmentation or if the allocation size
    // estimates are incorrect. We may be able to continue execution by evicting some
    // more memory and calling MakeResident again.
    while (FAILED(hr)) {
        constexpr uint32_t kAdditonalSizeToEvict = 50000000;  // 50MB

        uint64_t sizeEvicted = 0;

        DAWN_TRY_ASSIGN(sizeEvicted, EnsureCanMakeResident(kAdditonalSizeToEvict, segment));

        // If nothing can be evicted after MakeResident has failed, we cannot continue
        // execution and must throw a fatal error.
        if (sizeEvicted == 0) {
            return DAWN_OUT_OF_MEMORY_ERROR(
                "MakeResident has failed due to excessive video memory usage.");
        }

        hr = mDevice->GetD3D12Device()->MakeResident(
            static_cast<uint32_t>(numberOfObjectsToMakeResident), allocations);
    }

    return {};
}

// Inserts a heap at the bottom of the LRU. The passed heap must be resident or scheduled to
// become resident within the current serial. Failing to call this function when an allocation
// is implicitly made resident will cause the residency manager to view the allocation as
// non-resident and call MakeResident - which will make D3D12's internal residency refcount on
// the allocation out of sync with Dawn.
void ResidencyManager::TrackResidentAllocation(Pageable* pageable) {
    if (!mResidencyManagementEnabled) {
        return;
    }

    DAWN_ASSERT(pageable->IsInList() == false);
    GetMemorySegmentInfo(pageable->GetMemorySegment())->lruCache.Append(pageable);
}

// Places an artifical cap on Dawn's budget so we can test in a predictable manner. If used,
// this function must be called before any resources have been created.
void ResidencyManager::RestrictBudgetForTesting(uint64_t artificialBudgetCap) {
    DAWN_ASSERT(mVideoMemoryInfo.nonLocal.lruCache.empty());
    DAWN_ASSERT(!mRestrictBudgetForTesting);

    mRestrictBudgetForTesting = true;
    UpdateVideoMemoryInfo();

    // Dawn has a non-zero memory usage even before any resources have been created, and this
    // value can vary depending on the environment Dawn is running in. By adding this in
    // addition to the artificial budget cap, we can create a predictable and reproducible
    // budget for testing.
    mVideoMemoryInfo.local.budget = mVideoMemoryInfo.local.usage + artificialBudgetCap;
    if (!mDevice->GetDeviceInfo().isUMA) {
        mVideoMemoryInfo.nonLocal.budget = mVideoMemoryInfo.nonLocal.usage + artificialBudgetCap;
    }
}

}  // namespace dawn::native::d3d12
