// Copyright 2024 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 <queue>
#include <vector>

#include "src/dawn/common/MutexProtected.h"
#include "src/dawn/tests/perf_tests/DawnPerfTest.h"
#include "src/dawn/utils/ComboRenderPipelineDescriptor.h"
#include "src/dawn/utils/WGPUHelpers.h"
#include "src/utils/compiler.h"

// This is for developers only to ensure the triangle color drawn is as expected.
// #define PIXEL_CHECK 1

namespace dawn {
namespace {

constexpr unsigned int kNumIterations = 100;

constexpr uint32_t kTextureSize = 128;
constexpr size_t kUniformDataSize = 4 * sizeof(float);
constexpr size_t kUniformBufferSize = 256;

constexpr float kVertexData[12] = {
    0.0f, 0.5f, 0.0f, 1.0f, -0.5f, -0.5f, 0.0f, 1.0f, 0.5f, -0.5f, 0.0f, 1.0f,
};

constexpr char kVertexShader[] = R"(
        @vertex fn main(
            @location(0) pos : vec4f
        ) -> @builtin(position) vec4f {
            return pos;
        })";

constexpr char kFragmentShader[] = R"(
        @group(0) @binding(0) var<uniform> color : vec3f;
        @fragment fn main() -> @location(0) vec4f {
            return vec4f(color * (1.0 / %d), 1.0);
        })";

enum class UploadMethod {
    // Use Queue.WriteBuffer() to update the uniform buffers.
    WriteBuffer,
    // Map and copy to a common staging buffer first, then copy it to all uniform buffers.
    SingleStagingBuffer,
    // Map and copy to a specific staging buffer first for each uniform buffer to then copy from.
    MultipleStagingBuffer,
    // Map uniform buffer directly
    MapWithExtendedUsages,
};

enum class UploadSize {
    Partial,
    Full,
};

enum class UniformBuffer {
    Single,    // Use one same uniform buffer for all draws.
    Multiple,  // Switch uniform buffers between draws.
};

struct UniformBufferUpdateParams : AdapterTestParam {
    UniformBufferUpdateParams(const AdapterTestParam& param,
                              UploadMethod uploadMethod,
                              UploadSize uploadSize,
                              UniformBuffer uniformBuffer)
        : AdapterTestParam(param),
          uploadMethod(uploadMethod),
          uploadSize(uploadSize),
          uniformBuffer(uniformBuffer) {}

    UploadMethod uploadMethod;
    UploadSize uploadSize;
    UniformBuffer uniformBuffer;
};

std::ostream& operator<<(std::ostream& ostream, const UniformBufferUpdateParams& param) {
    ostream << static_cast<const AdapterTestParam&>(param);

    switch (param.uploadMethod) {
        case UploadMethod::WriteBuffer:
            ostream << "_WriteBuffer";
            break;
        case UploadMethod::SingleStagingBuffer:
            ostream << "_SingleStagingBuffer";
            break;
        case UploadMethod::MultipleStagingBuffer:
            ostream << "_MultipleStagingBuffer";
            break;
        case UploadMethod::MapWithExtendedUsages:
            ostream << "_MapWithExtendedUsages";
            break;
    }

    switch (param.uploadSize) {
        case UploadSize::Partial:
            ostream << "_PartialSize";
            break;
        case UploadSize::Full:
            ostream << "_FullSize";
            break;
    }

    switch (param.uniformBuffer) {
        case UniformBuffer::Single:
            ostream << "_SingleUniformBuffer";
            break;
        case UniformBuffer::Multiple:
            ostream << "_MultipleUniformBuffer";
            break;
    }

    return ostream;
}

// Test updating a uniform buffer |kNumIterations| times.
class UniformBufferUpdatePerf : public DawnPerfTestWithParams<UniformBufferUpdateParams> {
  public:
    UniformBufferUpdatePerf() : DawnPerfTestWithParams(kNumIterations, 1) {}
    ~UniformBufferUpdatePerf() override = default;

  protected:
    void SetUpPerfTest() override;

  private:
    void Step() override;
    std::vector<wgpu::FeatureName> GetRequiredFeatures() override;

    size_t GetBufferSize();
    wgpu::Buffer FindOrCreateUniformBuffer();
    void ReturnUniformBuffer(wgpu::Buffer buffer);
    wgpu::Buffer FindOrCreateStagingBuffer();
    void ReturnStagingBuffer(wgpu::Buffer buffer);

    wgpu::Texture mColorAttachmentTexture;
    wgpu::TextureView mColorAttachmentTextureView;
    wgpu::TextureView mDepthStencilAttachment;
    wgpu::Buffer mVertexBuffer;
    wgpu::BindGroupLayout mUniformBindGroupLayout;
    wgpu::RenderPipeline mPipeline;

    // Free uniform buffers to be re-used.
    MutexProtected<std::queue<wgpu::Buffer>> mUniformBuffers;

    wgpu::Buffer mSingleStagingBuffer;
    // Free staging buffers to be re-used. All buffers are mapped already.
    MutexProtected<std::queue<wgpu::Buffer>> mMultipleStagingBuffers;
};

std::vector<wgpu::FeatureName> UniformBufferUpdatePerf::GetRequiredFeatures() {
    std::vector<wgpu::FeatureName> requiredFeatures = DawnPerfTestWithParams::GetRequiredFeatures();
    if (!UsesWire() && GetParam().uploadMethod == UploadMethod::MapWithExtendedUsages &&
        SupportsFeatures({wgpu::FeatureName::BufferMapExtendedUsages})) {
        requiredFeatures.push_back(wgpu::FeatureName::BufferMapExtendedUsages);
    }
    return requiredFeatures;
}

size_t UniformBufferUpdatePerf::GetBufferSize() {
    // The actual data size, and buffer create size should be same for full upload size.
    return GetParam().uploadSize == UploadSize::Full ? kUniformDataSize : kUniformBufferSize;
}

// Try to grab a free uniform buffer. If unavailable, create a new one on-the-fly.
wgpu::Buffer UniformBufferUpdatePerf::FindOrCreateUniformBuffer() {
    if (!mUniformBuffers->empty()) {
        wgpu::Buffer buffer = mUniformBuffers->front();
        mUniformBuffers->pop();
        return buffer;
    }
    wgpu::BufferDescriptor descriptor;
    descriptor.usage = wgpu::BufferUsage::Uniform;

    if (GetParam().uploadMethod == UploadMethod::MapWithExtendedUsages) {
        descriptor.usage |= wgpu::BufferUsage::MapWrite;
        descriptor.mappedAtCreation = true;
    } else {
        descriptor.usage |= wgpu::BufferUsage::CopyDst;
    }

    descriptor.size = GetBufferSize();
    return device.CreateBuffer(&descriptor);
}

// Return a uniform buffer, so that it's free to be re-used.
void UniformBufferUpdatePerf::ReturnUniformBuffer(wgpu::Buffer buffer) {
    mUniformBuffers->push(buffer);
}

// Try to grab a free staging buffer. If unavailable, create a new one on-the-fly.
wgpu::Buffer UniformBufferUpdatePerf::FindOrCreateStagingBuffer() {
    if (!mMultipleStagingBuffers->empty()) {
        wgpu::Buffer buffer = mMultipleStagingBuffers->front();
        mMultipleStagingBuffers->pop();
        return buffer;
    }
    wgpu::BufferDescriptor descriptor;
    descriptor.usage = wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc;
    descriptor.size = GetBufferSize();
    descriptor.mappedAtCreation = true;
    return device.CreateBuffer(&descriptor);
}

// Return a staging buffer, so that it's free to be re-used.
void UniformBufferUpdatePerf::ReturnStagingBuffer(wgpu::Buffer buffer) {
    mMultipleStagingBuffers->push(buffer);
}

void UniformBufferUpdatePerf::SetUpPerfTest() {
    // Skip all tests if the BufferMapExtendedUsages feature is not supported.
    DAWN_TEST_UNSUPPORTED_IF(GetParam().uploadMethod == UploadMethod::MapWithExtendedUsages &&
                             !device.HasFeature(wgpu::FeatureName::BufferMapExtendedUsages));

    // TODO(crbug.com/468353718): Flakily calls a pure virtual function on Mac/AMD.
    DAWN_SUPPRESS_TEST_IF(IsMacOS() && IsAMD() &&
                          GetParam().uploadMethod == UploadMethod::MultipleStagingBuffer);
    // TODO(crbug.com/468353718): Same as above, but on the integrated GPU of
    // the AMD laptops.
    DAWN_SUPPRESS_TEST_IF(IsMacOS() && IsIntel() && GetParam().uploadSize == UploadSize::Full &&
                          GetParam().uniformBuffer == UniformBuffer::Multiple);

    // Create the color / depth stencil attachments.
    wgpu::TextureDescriptor descriptor = {};
    descriptor.dimension = wgpu::TextureDimension::e2D;
    descriptor.size.width = kTextureSize;
    descriptor.size.height = kTextureSize;
    descriptor.size.depthOrArrayLayers = 1;
    descriptor.usage = wgpu::TextureUsage::RenderAttachment;
#ifdef PIXEL_CHECK
    descriptor.usage |= wgpu::TextureUsage::CopySrc;
#endif
    descriptor.format = wgpu::TextureFormat::RGBA8Unorm;
    mColorAttachmentTexture = device.CreateTexture(&descriptor);
    mColorAttachmentTextureView = mColorAttachmentTexture.CreateView();

    descriptor.format = wgpu::TextureFormat::Depth24PlusStencil8;
    mDepthStencilAttachment = device.CreateTexture(&descriptor).CreateView();

    // Create the vertex buffer
    mVertexBuffer = utils::CreateBufferFromData(device, kVertexData, sizeof(kVertexData),
                                                wgpu::BufferUsage::Vertex);

    // Create the bind group layout.
    mUniformBindGroupLayout = utils::MakeBindGroupLayout(
        device, {
                    {0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, false},
                });

    // Setup the base render pipeline descriptor.
    utils::ComboRenderPipelineDescriptor renderPipelineDesc;
    renderPipelineDesc.vertex.bufferCount = 1;
    renderPipelineDesc.cBuffers[0].arrayStride = 4 * sizeof(float);
    renderPipelineDesc.cBuffers[0].attributeCount = 1;
    renderPipelineDesc.cAttributes[0].format = wgpu::VertexFormat::Float32x4;
    renderPipelineDesc.EnableDepthStencil(wgpu::TextureFormat::Depth24PlusStencil8);
    renderPipelineDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;

    // Create the pipeline layout for the pipeline.
    wgpu::PipelineLayoutDescriptor pipelineLayoutDesc = {};
    pipelineLayoutDesc.bindGroupLayouts = &mUniformBindGroupLayout;
    pipelineLayoutDesc.bindGroupLayoutCount = 1;
    wgpu::PipelineLayout pipelineLayout = device.CreatePipelineLayout(&pipelineLayoutDesc);

    // Create the shaders for the pipeline.
    wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, kVertexShader);
    // Inject kNumIterations into the fragment shader.
    char fragmentShader[sizeof(kFragmentShader) + 16];
    DAWN_UNSAFE_TODO(
        snprintf(fragmentShader, sizeof(fragmentShader), kFragmentShader, kNumIterations));
    wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, fragmentShader);

    // Create the pipeline.
    renderPipelineDesc.layout = pipelineLayout;
    renderPipelineDesc.vertex.module = vsModule;
    renderPipelineDesc.cFragment.module = fsModule;
    mPipeline = device.CreateRenderPipeline(&renderPipelineDesc);

    std::vector<float> data(kUniformDataSize, 1.0f * (kNumIterations / 2));
    mSingleStagingBuffer = FindOrCreateStagingBuffer();
    DAWN_UNSAFE_TODO(
        memcpy(mSingleStagingBuffer.GetMappedRange(0, data.size()), data.data(), data.size()));
    mSingleStagingBuffer.Unmap();

    if (GetParam().uploadMethod == UploadMethod::MapWithExtendedUsages &&
        GetParam().uniformBuffer == UniformBuffer::Single) {
        auto buffer = FindOrCreateUniformBuffer();
        DAWN_UNSAFE_TODO(memcpy(buffer.GetMappedRange(0, data.size()), data.data(), data.size()));
        buffer.Unmap();
        ReturnUniformBuffer(buffer);
    }
}

void UniformBufferUpdatePerf::Step() {
    for (unsigned int i = 0; i < kNumIterations; ++i) {
        std::vector<float> data(kUniformDataSize, 1.0f * i);
        wgpu::CommandEncoder commands = device.CreateCommandEncoder();
        wgpu::Buffer uniformBuffer = FindOrCreateUniformBuffer();
        wgpu::Buffer stagingBuffer = nullptr;
        switch (GetParam().uploadMethod) {
            case UploadMethod::WriteBuffer:
                queue.WriteBuffer(uniformBuffer, 0, data.data(), data.size());
                break;
            case UploadMethod::SingleStagingBuffer:
                commands.CopyBufferToBuffer(mSingleStagingBuffer, 0, uniformBuffer, 0, data.size());
                break;
            case UploadMethod::MultipleStagingBuffer:
                stagingBuffer = FindOrCreateStagingBuffer();
                DAWN_UNSAFE_TODO(
                    memcpy(stagingBuffer.GetMappedRange(0, data.size()), data.data(), data.size()));
                stagingBuffer.Unmap();
                commands.CopyBufferToBuffer(stagingBuffer, 0, uniformBuffer, 0, data.size());
                break;
            case UploadMethod::MapWithExtendedUsages:
                if (GetParam().uniformBuffer == UniformBuffer::Multiple) {
                    DAWN_UNSAFE_TODO(memcpy(uniformBuffer.GetMappedRange(0, data.size()),
                                            data.data(), data.size()));
                    uniformBuffer.Unmap();
                }
                break;
        }
        utils::ComboRenderPassDescriptor renderPass({mColorAttachmentTextureView},
                                                    mDepthStencilAttachment);
        wgpu::RenderPassEncoder pass = commands.BeginRenderPass(&renderPass);
        pass.SetPipeline(mPipeline);
        pass.SetVertexBuffer(0, mVertexBuffer);
        wgpu::BindGroup bindGroup = utils::MakeBindGroup(device, mUniformBindGroupLayout,
                                                         {{0, uniformBuffer, 0, GetBufferSize()}});
        pass.SetBindGroup(0, bindGroup);
        pass.Draw(3);
        pass.End();
        wgpu::CommandBuffer commandBuffer = commands.Finish();
        queue.Submit(1, &commandBuffer);

        // Return the staging buffer once it's done with the last usage and re-mapped.
        if (GetParam().uploadMethod == UploadMethod::MultipleStagingBuffer) {
            stagingBuffer.MapAsync(
                wgpu::MapMode::Write, 0, GetBufferSize(), wgpu::CallbackMode::AllowProcessEvents,
                [this, stagingBuffer](wgpu::MapAsyncStatus status, wgpu::StringView) {
                    if (status == wgpu::MapAsyncStatus::Success) {
                        this->ReturnStagingBuffer(stagingBuffer);
                    }
                });
        }

        switch (GetParam().uniformBuffer) {
            case UniformBuffer::Single:
                // Return the uniform buffer immediately so that we always use the same one.
                ReturnUniformBuffer(uniformBuffer);
                break;
            case UniformBuffer::Multiple:
                // Return the uniform buffer once it's done with the last submit.
                if (GetParam().uploadMethod == UploadMethod::MapWithExtendedUsages) {
                    uniformBuffer.MapAsync(
                        wgpu::MapMode::Write, 0, GetBufferSize(),
                        wgpu::CallbackMode::AllowProcessEvents,
                        [this, uniformBuffer](wgpu::MapAsyncStatus status, wgpu::StringView) {
                            if (status == wgpu::MapAsyncStatus::Success) {
                                this->ReturnUniformBuffer(uniformBuffer);
                            }
                        });
                } else {
                    queue.OnSubmittedWorkDone(
                        wgpu::CallbackMode::AllowProcessEvents,
                        [this, uniformBuffer](wgpu::QueueWorkDoneStatus status, wgpu::StringView) {
                            if (status == wgpu::QueueWorkDoneStatus::Success) {
                                this->ReturnUniformBuffer(uniformBuffer);
                            }
                        });
                }
                break;
        }

#ifdef PIXEL_CHECK
        auto value = i;
        if (GetParam().uploadMethod == UploadMethod::SingleStagingBuffer) {
            value = kNumIterations / 2;
        }
        uint8_t u8 = std::floor(value * 255.0 / kNumIterations);
        utils::RGBA8 color0(u8, u8, u8, 255);
        utils::RGBA8 color1(u8 + 1, u8 + 1, u8 + 1, 255);
        EXPECT_PIXEL_RGBA8_BETWEEN(color0, color1, mColorAttachmentTexture, kTextureSize / 2,
                                   kTextureSize / 2);
#endif
    }
}

TEST_P(UniformBufferUpdatePerf, Run) {
    RunTest();
}

DAWN_INSTANTIATE_TEST_P(UniformBufferUpdatePerf,
                        {D3D11Backend(), D3D12Backend(), MetalBackend(), OpenGLBackend(),
                         OpenGLESBackend(), VulkanBackend()},
                        {UploadMethod::WriteBuffer, UploadMethod::SingleStagingBuffer,
                         UploadMethod::MultipleStagingBuffer, UploadMethod::MapWithExtendedUsages},
                        {UploadSize::Partial, UploadSize::Full},
                        {UniformBuffer::Single, UniformBuffer::Multiple});

}  // anonymous namespace
}  // namespace dawn
