/*
 * Copyright 2020 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "src/gpu/ganesh/d3d/GrD3DBuffer.h"

#include "src/gpu/ganesh/d3d/GrD3DGpu.h"
#include "src/gpu/ganesh/d3d/GrD3DUtil.h"

#ifdef SK_DEBUG
#define VALIDATE() this->validate()
#else
#define VALIDATE() do {} while(false)
#endif

static gr_cp<ID3D12Resource> make_d3d_buffer(GrD3DGpu* gpu,
                                             size_t size,
                                             GrGpuBufferType intendedType,
                                             GrAccessPattern accessPattern,
                                             D3D12_RESOURCE_STATES* resourceState,
                                             sk_sp<GrD3DAlloc>* alloc) {
    D3D12_HEAP_TYPE heapType;
    if (accessPattern == kStatic_GrAccessPattern) {
        SkASSERT(intendedType != GrGpuBufferType::kXferCpuToGpu &&
                 intendedType != GrGpuBufferType::kXferGpuToCpu);
        heapType = D3D12_HEAP_TYPE_DEFAULT;
        // Needs to be transitioned to appropriate state to be read in shader
        *resourceState = D3D12_RESOURCE_STATE_COPY_DEST;
    } else {
        if (intendedType == GrGpuBufferType::kXferGpuToCpu) {
            heapType = D3D12_HEAP_TYPE_READBACK;
            // Cannot be changed
            *resourceState = D3D12_RESOURCE_STATE_COPY_DEST;
        } else {
            heapType = D3D12_HEAP_TYPE_UPLOAD;
            // Cannot be changed
            // Includes VERTEX_AND_CONSTANT_BUFFER, INDEX_BUFFER, INDIRECT_ARGUMENT, and COPY_SOURCE
            *resourceState = D3D12_RESOURCE_STATE_GENERIC_READ;
        }
    }

    D3D12_RESOURCE_DESC bufferDesc = {};
    bufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
    bufferDesc.Alignment = 0;  // default alignment
    bufferDesc.Width = size;
    bufferDesc.Height = 1;
    bufferDesc.DepthOrArraySize = 1;
    bufferDesc.MipLevels = 1;
    bufferDesc.Format = DXGI_FORMAT_UNKNOWN;
    bufferDesc.SampleDesc.Count = 1;
    bufferDesc.SampleDesc.Quality = 0; // Doesn't apply to buffers
    bufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
    bufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE;

    gr_cp<ID3D12Resource> resource = gpu->memoryAllocator()->createResource(
            heapType, &bufferDesc, *resourceState, alloc, nullptr);

    return resource;
}

sk_sp<GrD3DBuffer> GrD3DBuffer::Make(GrD3DGpu* gpu, size_t size, GrGpuBufferType intendedType,
                                     GrAccessPattern accessPattern) {
    SkASSERT(!gpu->protectedContext() || (accessPattern != kStatic_GrAccessPattern));
    D3D12_RESOURCE_STATES resourceState;

    sk_sp<GrD3DAlloc> alloc;
    gr_cp<ID3D12Resource> resource = make_d3d_buffer(gpu, size, intendedType, accessPattern,
                                                     &resourceState, &alloc);
    if (!resource) {
        return nullptr;
    }

    return sk_sp<GrD3DBuffer>(new GrD3DBuffer(gpu, size, intendedType, accessPattern,
                                              std::move(resource), std::move(alloc),
                                              resourceState,
                                              /*label=*/"MakeD3DBuffer"));
}

GrD3DBuffer::GrD3DBuffer(GrD3DGpu* gpu, size_t size, GrGpuBufferType intendedType,
                         GrAccessPattern accessPattern, gr_cp<ID3D12Resource> bufferResource,
                         sk_sp<GrD3DAlloc> alloc,
                         D3D12_RESOURCE_STATES resourceState,
                         std::string_view label)
    : INHERITED(gpu, size, intendedType, accessPattern, label)
    , fResourceState(resourceState)
    , fD3DResource(std::move(bufferResource))
    , fAlloc(std::move(alloc)) {
    this->registerWithCache(skgpu::Budgeted::kYes);

    // TODO: persistently map UPLOAD resources?

    VALIDATE();
}

void GrD3DBuffer::setResourceState(const GrD3DGpu* gpu,
                                   D3D12_RESOURCE_STATES newResourceState) {
    if (newResourceState == fResourceState ||
        // GENERIC_READ encapsulates a lot of different read states
        (fResourceState == D3D12_RESOURCE_STATE_GENERIC_READ &&
         SkToBool(newResourceState | fResourceState))) {
        return;
    }

    D3D12_RESOURCE_TRANSITION_BARRIER barrier = {};
    barrier.pResource = this->d3dResource();
    barrier.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
    barrier.StateBefore = fResourceState;
    barrier.StateAfter = newResourceState;

    gpu->addBufferResourceBarriers(this, 1, &barrier);

    fResourceState = newResourceState;
}

void GrD3DBuffer::releaseResource() {
    if (this->wasDestroyed()) {
        return;
    }

    if (fMapPtr) {
        this->unmap();
    }

    SkASSERT(fD3DResource);
    SkASSERT(fAlloc);
    fD3DResource.reset();
    fAlloc.reset();
}

void GrD3DBuffer::onRelease() {
    this->releaseResource();
    this->INHERITED::onRelease();
}

void GrD3DBuffer::onAbandon() {
    this->releaseResource();
    this->INHERITED::onAbandon();
}

void GrD3DBuffer::onMap(MapType type) {
    fMapPtr = this->internalMap(type, 0, this->size());
}

void GrD3DBuffer::onUnmap(MapType type) {
    this->internalUnmap(type, 0, this->size());
}

bool GrD3DBuffer::onClearToZero() {
    if (!fD3DResource) {
        return false;
    }

    if (this->accessPattern() == kStatic_GrAccessPattern) {
        GrStagingBufferManager::Slice slice =
                this->getD3DGpu()->stagingBufferManager()->allocateStagingBufferSlice(this->size());
        if (!slice.fBuffer) {
            return false;
        }
        std::memset(slice.fOffsetMapPtr, 0, this->size());
        this->setResourceState(this->getD3DGpu(), D3D12_RESOURCE_STATE_COPY_DEST);
        this->getD3DGpu()->currentCommandList()->copyBufferToBuffer(
                sk_ref_sp<GrD3DBuffer>(this),
                0,
                static_cast<const GrD3DBuffer*>(slice.fBuffer)->d3dResource(),
                slice.fOffset,
                this->size());
        return true;
    }

    void* ptr = this->internalMap(MapType::kWriteDiscard, 0, this->size());
    if (!ptr) {
        return false;
    }
    std::memset(ptr, 0, this->size());
    this->internalUnmap(MapType::kWriteDiscard, 0, this->size());

    return true;
}

bool GrD3DBuffer::onUpdateData(const void* src, size_t offset, size_t size, bool /*preserve*/) {
    if (!fD3DResource) {
        return false;
    }

    void* ptr = this->internalMap(MapType::kWriteDiscard, offset, size);
    if (!ptr) {
        return false;
    }
    if (this->accessPattern() == kStatic_GrAccessPattern) {
        // We should never call this method on static buffers in protected contexts.
        SkASSERT(!this->getD3DGpu()->protectedContext());
        //*** any alignment restrictions?
    }
    memcpy(ptr, src, size);
    this->internalUnmap(MapType::kWriteDiscard, offset, size);

    return true;
}

void* GrD3DBuffer::internalMap(MapType type, size_t offset, size_t size) {
    // TODO: if UPLOAD heap type, could be persistently mapped (i.e., this would be a no-op)
    SkASSERT(fD3DResource);
    SkASSERT(!this->isMapped());
    SkASSERT(offset + size <= this->size());

    VALIDATE();

    if (this->accessPattern() == kStatic_GrAccessPattern) {
        if (type == MapType::kRead) {
            return nullptr;
        }
        SkASSERT(!fStagingBuffer);
        GrStagingBufferManager::Slice slice =
                this->getD3DGpu()->stagingBufferManager()->allocateStagingBufferSlice(size);
        if (!slice.fBuffer) {
            return nullptr;
        }
        fStagingBuffer = static_cast<const GrD3DBuffer*>(slice.fBuffer)->d3dResource();
        fStagingOffset = slice.fOffset;
        VALIDATE();
        return slice.fOffsetMapPtr;
    }

    D3D12_RANGE range;
    range.Begin = offset;
    // The range passed here indicates the portion of the resource that may be
    // read. If we're only writing then pass an empty range.
    range.End = type == MapType::kRead ? offset + size : offset;
    void* result;
    if (fD3DResource->Map(0, &range, &result) != S_OK) {
        return nullptr;
    }
    if (result) {
        result = SkTAddOffset<void>(result, offset);
    }
    VALIDATE();
    return result;
}

void GrD3DBuffer::internalUnmap(MapType type, size_t offset, size_t size) {
    // TODO: if UPLOAD heap type, could be persistently mapped (i.e., this would be a no-op)
    SkASSERT(fD3DResource);
    SkASSERT(offset + size <= this->size());
    VALIDATE();

    if (this->accessPattern() == kStatic_GrAccessPattern) {
        SkASSERT(type != GrGpuBuffer::MapType::kRead);
        SkASSERT(fStagingBuffer);
        this->setResourceState(this->getD3DGpu(), D3D12_RESOURCE_STATE_COPY_DEST);
        this->getD3DGpu()->currentCommandList()->copyBufferToBuffer(
                sk_ref_sp<GrD3DBuffer>(this),
                offset,
                fStagingBuffer,
                fStagingOffset,
                size);
        fStagingBuffer = nullptr;
    } else {
        D3D12_RANGE range;
        range.Begin = offset;
        range.End = type == MapType::kWriteDiscard ? offset + size : offset;
        // For READBACK heaps, unmap requires an empty range
        SkASSERT(fResourceState != D3D12_RESOURCE_STATE_COPY_DEST || range.Begin == range.End);
        fD3DResource->Unmap(0, &range);
    }

    VALIDATE();
}

void GrD3DBuffer::onSetLabel() {
    SkASSERT(fD3DResource);
    if (!this->getLabel().empty()) {
        const std::wstring label = L"_Skia_" + GrD3DMultiByteToWide(this->getLabel());
        this->d3dResource()->SetName(label.c_str());
    }
}

#ifdef SK_DEBUG
void GrD3DBuffer::validate() const {
    SkASSERT(this->intendedType() == GrGpuBufferType::kVertex ||
             this->intendedType() == GrGpuBufferType::kIndex ||
             this->intendedType() == GrGpuBufferType::kDrawIndirect ||
             this->intendedType() == GrGpuBufferType::kXferCpuToGpu ||
             this->intendedType() == GrGpuBufferType::kXferGpuToCpu);
}
#endif
