// Copyright 2023 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 <string>
#include <vector>

#include "src/dawn/tests/DawnTest.h"
#include "src/dawn/utils/ComboRenderPipelineDescriptor.h"
#include "src/dawn/utils/WGPUHelpers.h"

namespace dawn {
namespace {

class FramebufferFetchTests : public DawnTest {
  protected:
    void SetUp() override {
        DawnTest::SetUp();
        DAWN_TEST_UNSUPPORTED_IF(!device.HasFeature(wgpu::FeatureName::FramebufferFetch));
    }

    std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
        std::vector<wgpu::FeatureName> requiredFeatures = {};
        if (SupportsFeatures({wgpu::FeatureName::FramebufferFetch})) {
            requiredFeatures.push_back(wgpu::FeatureName::FramebufferFetch);
        }
        return requiredFeatures;
    }

    void InitForSinglePoint(utils::ComboRenderPipelineDescriptor* desc) {
        desc->vertex.module = utils::CreateShaderModule(device, R"(
            @vertex fn vs() -> @builtin(position) vec4f {
                return vec4f(0, 0, 0, 1);
            }
        )");
        desc->primitive.topology = wgpu::PrimitiveTopology::PointList;
    }

    wgpu::Texture MakeAttachment(wgpu::TextureFormat format,
                                 wgpu::Extent3D size = {1, 1, 1},
                                 uint32_t sampleCount = 1) {
        wgpu::TextureDescriptor tDesc;
        tDesc.size = size;
        tDesc.sampleCount = sampleCount;
        tDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc |
                      wgpu::TextureUsage::CopyDst;
        tDesc.format = format;
        return device.CreateTexture(&tDesc);
    }

    std::string kExt = "enable chromium_experimental_framebuffer_fetch;\n";
};

// A basic test of framebuffer fetch
TEST_P(FramebufferFetchTests, Basic) {
    // Pipeline that draw a point at the center that adds one to the attachment.
    utils::ComboRenderPipelineDescriptor pDesc;
    InitForSinglePoint(&pDesc);
    pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"(
        @fragment fn main(@color(0) in : u32) -> @location(0) u32 {
            return in + 1;
        }
    )");
    pDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint;
    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc);

    wgpu::Texture texture = MakeAttachment(wgpu::TextureFormat::R32Uint);

    // Draw 10 points with the pipeline on the render target.
    utils::ComboRenderPassDescriptor passDesc({texture.CreateView()});
    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc);
    pass.SetPipeline(pipeline);
    pass.Draw(10);
    pass.End();

    wgpu::CommandBuffer commands = encoder.Finish();
    queue.Submit(1, &commands);

    // The 10 points should have successfully used framebuffer fetch to do increment ten times
    // without races.
    EXPECT_TEXTURE_EQ(uint32_t(10), texture, {0, 0});
}

// Check that it is post-blend framebuffer fetch
TEST_P(FramebufferFetchTests, PostBlend) {
    // Pipeline that draw a point at the center that outputs the current attachment value with
    // additive blend, so multiplies by two.
    utils::ComboRenderPipelineDescriptor pDesc;
    InitForSinglePoint(&pDesc);
    pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"(
        @fragment fn main(@color(0) in : vec4f) -> @location(0) vec4f {
            return in;
        }
    )");
    pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
    pDesc.cTargets[0].blend = &pDesc.cBlends[0];
    pDesc.cBlends[0].color.srcFactor = wgpu::BlendFactor::One;
    pDesc.cBlends[0].color.dstFactor = wgpu::BlendFactor::One;
    pDesc.cBlends[0].color.operation = wgpu::BlendOperation::Add;
    pDesc.cBlends[0].alpha = pDesc.cBlends[0].color;
    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc);

    wgpu::Texture texture = MakeAttachment(wgpu::TextureFormat::RGBA8Unorm);

    // Starting with small value, then do *2 4 times.
    utils::ComboRenderPassDescriptor passDesc({texture.CreateView()});
    passDesc.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
    passDesc.cColorAttachments[0].clearValue = {1 / 255.0, 2 / 255.0, 3 / 255.0, 4 / 255.0};

    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc);
    pass.SetPipeline(pipeline);
    pass.Draw(4);
    pass.End();

    wgpu::CommandBuffer commands = encoder.Finish();
    queue.Submit(1, &commands);

    // The 10 points should have successfully used framebuffer fetch to do increment ten times
    // without races.
    EXPECT_TEXTURE_EQ(utils::RGBA8(16, 32, 48, 64), texture, {0, 0});
}

// Check with multiple render attachments
TEST_P(FramebufferFetchTests, MultipleAttachments) {
    // Pipeline that draw a point at the center that adds 0/1/2/3 to multiple attachments.
    utils::ComboRenderPipelineDescriptor pDesc;
    InitForSinglePoint(&pDesc);
    pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"(
        struct Out {
            @location(0) out0 : u32,
            @location(1) out1 : u32,
            @location(2) out2 : u32,
            @location(3) out3 : u32,
        }
        @fragment fn main(
            @color(0) in0 : u32,
            @color(1) in1 : u32,
            @color(2) in2 : u32,
            @color(3) in3 : u32,
        ) -> Out {
            var out : Out;
            out.out0 = in0 + 0;
            out.out1 = in1 + 1;
            out.out2 = in2 + 2;
            out.out3 = in3 + 3;
            return out;
        }
    )");
    pDesc.cFragment.targetCount = 4;
    pDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint;
    pDesc.cTargets[1].format = wgpu::TextureFormat::R32Uint;
    pDesc.cTargets[2].format = wgpu::TextureFormat::R32Uint;
    pDesc.cTargets[3].format = wgpu::TextureFormat::R32Uint;
    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc);

    wgpu::Texture texture0 = MakeAttachment(wgpu::TextureFormat::R32Uint);
    wgpu::Texture texture1 = MakeAttachment(wgpu::TextureFormat::R32Uint);
    wgpu::Texture texture2 = MakeAttachment(wgpu::TextureFormat::R32Uint);
    wgpu::Texture texture3 = MakeAttachment(wgpu::TextureFormat::R32Uint);

    // Draw 10 points with the pipeline on the render target.
    utils::ComboRenderPassDescriptor passDesc({
        texture0.CreateView(),
        texture1.CreateView(),
        texture2.CreateView(),
        texture3.CreateView(),
    });
    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc);
    pass.SetPipeline(pipeline);
    pass.Draw(10);
    pass.End();

    wgpu::CommandBuffer commands = encoder.Finish();
    queue.Submit(1, &commands);

    // The 10 points should have successfully used framebuffer fetch to do increment ten times
    // without races.
    EXPECT_TEXTURE_EQ(uint32_t(0), texture0, {0, 0});
    EXPECT_TEXTURE_EQ(uint32_t(10), texture1, {0, 0});
    EXPECT_TEXTURE_EQ(uint32_t(20), texture2, {0, 0});
    EXPECT_TEXTURE_EQ(uint32_t(30), texture3, {0, 0});
}

// Check with multiple render attachments
TEST_P(FramebufferFetchTests, VariousFormats) {
    // Pipeline that draw a point at the center that adds / substracts one to attachment of various
    // formats.
    utils::ComboRenderPipelineDescriptor pDesc;
    InitForSinglePoint(&pDesc);
    pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"(
        struct Out {
            @location(0) out0 : u32,
            @location(1) out1 : i32,
            @location(2) out2 : f32,
            @location(3) out3 : vec4f,
        }
        @fragment fn main(
            @color(0) in0 : u32,
            @color(1) in1 : i32,
            @color(2) in2 : f32,
            @color(3) in3 : vec4f,
        ) -> Out {
            var out : Out;
            out.out0 = in0 + 1;
            out.out1 = in1 - 1;
            out.out2 = in2 + 1;
            out.out3 = in3 + vec4f(1 / 255.0);
            return out;
        }
    )");
    pDesc.cFragment.targetCount = 4;
    pDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint;
    pDesc.cTargets[1].format = wgpu::TextureFormat::R32Sint;
    pDesc.cTargets[2].format = wgpu::TextureFormat::R32Float;
    pDesc.cTargets[3].format = wgpu::TextureFormat::RGBA8Unorm;
    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc);

    wgpu::Texture texture0 = MakeAttachment(wgpu::TextureFormat::R32Uint);
    wgpu::Texture texture1 = MakeAttachment(wgpu::TextureFormat::R32Sint);
    wgpu::Texture texture2 = MakeAttachment(wgpu::TextureFormat::R32Float);
    wgpu::Texture texture3 = MakeAttachment(wgpu::TextureFormat::RGBA8Unorm);

    // Draw 10 points with the pipeline on the render target.
    utils::ComboRenderPassDescriptor passDesc({
        texture0.CreateView(),
        texture1.CreateView(),
        texture2.CreateView(),
        texture3.CreateView(),
    });
    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc);
    pass.SetPipeline(pipeline);
    pass.Draw(10);
    pass.End();

    wgpu::CommandBuffer commands = encoder.Finish();
    queue.Submit(1, &commands);

    // The 10 points should have successfully used framebuffer fetch to do in/decrement ten times
    // without races.
    EXPECT_TEXTURE_EQ(uint32_t(10), texture0, {0, 0});
    EXPECT_TEXTURE_EQ(int32_t(-10), texture1, {0, 0});
    EXPECT_TEXTURE_EQ(static_cast<float>(10), texture2, {0, 0});
    EXPECT_TEXTURE_EQ(utils::RGBA8(10, 10, 10, 10), texture3, {0, 0});
}

// Tests load and store operations
// LoadOp::Clear and StoreOp::Store are basically always tested by other tests, so this one tests
// LoadOp::Load and StoreOp::Discard
TEST_P(FramebufferFetchTests, LoadAndStoreOpsReallyItsLoadAndDiscard) {
    // Pipeline that adds one to the first attachment but also saves it to the second attachment.
    utils::ComboRenderPipelineDescriptor pDesc;
    InitForSinglePoint(&pDesc);
    pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"(
        struct Out {
            @location(0) out0 : u32,
            @location(1) out1 : u32,
        }
        @fragment fn main(@color(0) in0 : u32, @color(1) in1 : u32) -> Out {
            var out : Out;
            out.out0 = in0 + 1;
            out.out1 = out.out0;
            return out;
        }
    )");
    pDesc.cFragment.targetCount = 2;
    pDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint;
    pDesc.cTargets[1].format = wgpu::TextureFormat::R32Uint;
    wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc);

    wgpu::Texture texture0 = MakeAttachment(wgpu::TextureFormat::R32Uint);
    wgpu::Texture texture1 = MakeAttachment(wgpu::TextureFormat::R32Uint);

    // Clear the first attachment to 1789 with a render pass.
    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
    utils::ComboRenderPassDescriptor clearPassDesc({texture0.CreateView()});
    clearPassDesc.cColorAttachments[0].clearValue.r = 1789;
    wgpu::RenderPassEncoder clearPass = encoder.BeginRenderPass(&clearPassDesc);
    clearPass.End();

    // Draw 10 points with the pipeline on the render target.
    utils::ComboRenderPassDescriptor passDesc({texture0.CreateView(), texture1.CreateView()});
    passDesc.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
    passDesc.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;
    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc);
    pass.SetPipeline(pipeline);
    pass.Draw(10);
    pass.End();

    wgpu::CommandBuffer commands = encoder.Finish();
    queue.Submit(1, &commands);

    // The first attachment is discard, but it's value of (ten increments + loaded value) is in the
    // second attachment.
    EXPECT_TEXTURE_EQ(uint32_t(0), texture0, {0, 0});
    EXPECT_TEXTURE_EQ(uint32_t(10 + 1789), texture1, {0, 0});
}

// Test the behavior with multisampling.
// Checks that each sample has its own framebuffer fetch by add sample_index to that sample multiple
// times. Checks that any framebuffer fetch forces sample shading, even if all the samples have the
// same value.
TEST_P(FramebufferFetchTests, MultisamplingIsSampleRasterization) {
    wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
        device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}});

    utils::ComboRenderPipelineDescriptor pDesc;
    InitForSinglePoint(&pDesc);
    pDesc.layout = utils::MakePipelineLayout(device, {bgl});
    pDesc.multisample.count = 4;
    pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
    pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"(
        @group(0) @binding(0) var<storage, read_write> invocationCount : atomic<u32>;

        @fragment fn addOne(@color(0) in : vec4f) -> @location(0) vec4f {
            atomicAdd(&invocationCount, 1);
            return in + vec4(1 / 255.0);
        }

        @fragment fn addSampleIndex(
            @color(0) in : vec4f,
            @builtin(sample_index) sampleIndex : u32,
        ) -> @location(0) vec4f {
            atomicAdd(&invocationCount, 1);
            var out = in;
            out[sampleIndex] += f32(4 * sampleIndex) / 255.0;
            return out;
        }
    )");

    pDesc.cFragment.entryPoint = "addOne";
    wgpu::RenderPipeline addOnePipeline = device.CreateRenderPipeline(&pDesc);

    pDesc.cFragment.entryPoint = "addSampleIndex";
    wgpu::RenderPipeline addSampleCountPipeline = device.CreateRenderPipeline(&pDesc);

    // Create the buffer that will contain the number of invocations.
    wgpu::BufferDescriptor bufDesc;
    bufDesc.size = 4;
    bufDesc.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc;
    wgpu::Buffer invocationBuffer = device.CreateBuffer(&bufDesc);
    wgpu::BindGroup bg = utils::MakeBindGroup(device, bgl, {{0, invocationBuffer}});

    wgpu::Texture attachment = MakeAttachment(wgpu::TextureFormat::RGBA8Unorm, {1, 1}, 4);
    wgpu::Texture resolve = MakeAttachment(wgpu::TextureFormat::RGBA8Unorm);

    // Run the test!!
    utils::ComboRenderPassDescriptor passDesc({attachment.CreateView()});
    passDesc.cColorAttachments[0].resolveTarget = resolve.CreateView();
    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
    wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc);
    pass.SetBindGroup(0, bg);
    // This FS should use sample shading and run 4 times, adding one each time.
    pass.SetPipeline(addOnePipeline);
    pass.Draw(1);
    // This adds sample_index * 10 to each sample.
    pass.SetPipeline(addSampleCountPipeline);
    pass.Draw(10);
    // This FS should use sample shading and run 4 times, adding one each time.
    pass.SetPipeline(addOnePipeline);
    pass.Draw(1);
    pass.End();

    wgpu::CommandBuffer commands = encoder.Finish();
    queue.Submit(1, &commands);

    EXPECT_BUFFER_U32_EQ(4 + 10 * 4 + 4, invocationBuffer, 0);
    EXPECT_TEXTURE_EQ(utils::RGBA8(2 + 0 * 10, 2 + 1 * 10, 2 + 2 * 10, 2 + 3 * 10), resolve,
                      {0, 0});
}

// TODO(crbug.com/42241389): Add a test that uses FramebufferFetch and ResourceTable in the same
// pipeline.

DAWN_INSTANTIATE_TEST(FramebufferFetchTests,
                      MetalBackend(),
                      VulkanBackend({}, {"vulkan_use_dynamic_rendering"}));

}  // anonymous namespace
}  // namespace dawn
