// 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 <vector>

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

namespace dawn {
namespace {

class PixelLocalStorageTests : public DawnTest {
  protected:
    void GetRequiredLimits(const dawn::utils::ComboLimits& supported,
                           dawn::utils::ComboLimits& required) override {
        required.maxStorageBuffersInFragmentStage = supported.maxStorageBuffersInFragmentStage;
        required.maxStorageBuffersPerShaderStage = supported.maxStorageBuffersPerShaderStage;
    }

    void SetUp() override {
        DawnTest::SetUp();
        DAWN_TEST_UNSUPPORTED_IF(
            !device.HasFeature(wgpu::FeatureName::PixelLocalStorageCoherent) &&
            !device.HasFeature(wgpu::FeatureName::PixelLocalStorageNonCoherent));

        DAWN_TEST_UNSUPPORTED_IF(GetSupportedLimits().maxStorageBuffersInFragmentStage < 1);

        supportsCoherent = device.HasFeature(wgpu::FeatureName::PixelLocalStorageCoherent);
    }

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

    struct StorageSpec {
        uint64_t offset;
        wgpu::TextureFormat format;
        wgpu::LoadOp loadOp = wgpu::LoadOp::Clear;
        wgpu::StoreOp storeOp = wgpu::StoreOp::Store;
        wgpu::Color clearValue = {0, 0, 0, 0};
        bool discardAfterInit = false;
    };

    enum class CheckMethod {
        StorageBuffer,
        ReadStorageAttachments,
        RenderAttachment,
    };

    struct PLSSpec {
        uint64_t totalSize;
        std::vector<StorageSpec> attachments;
        CheckMethod checkMethod = CheckMethod::ReadStorageAttachments;
    };

    // Builds a shader module with multiple entry points used for testing PLS.
    //
    //  - A trivial vertex entrypoint to render a point.
    //  - Various fragment entrypoints using a pixel_local block matching the `spec`.
    //    - An accumulator entrypoint adding (slot + 1) to each pls slot so we can check that
    //      access to the PLS is correctly synchronized.
    //    - An entrypoint copying the PLS data to a storage buffer for readback.
    //    - An entrypoint copying the PLS data to a render attachment for readback.
    wgpu::ShaderModule MakeTestModule(const PLSSpec& spec) const {
        std::vector<const char*> plsTypes;
        plsTypes.resize(spec.totalSize / kPLSSlotByteSize, "u32");
        for (const auto& attachment : spec.attachments) {
            switch (attachment.format) {
                case wgpu::TextureFormat::R32Uint:
                    plsTypes[attachment.offset / kPLSSlotByteSize] = "u32";
                    break;
                case wgpu::TextureFormat::R32Sint:
                    plsTypes[attachment.offset / kPLSSlotByteSize] = "i32";
                    break;
                case wgpu::TextureFormat::R32Float:
                    plsTypes[attachment.offset / kPLSSlotByteSize] = "f32";
                    break;
                default:
                    DAWN_UNREACHABLE();
            }
        }

        std::ostringstream o;
        o << R"(
            enable chromium_experimental_pixel_local;

            @vertex fn vs() -> @builtin(position) vec4f {
                return vec4f(0, 0, 0, 0.5);
            }

        )";
        o << "struct PLS {\n";
        for (size_t i = 0; i < plsTypes.size(); i++) {
            // e.g.: a0 : u32,
            o << "  a" << i << " : " << plsTypes[i] << ",\n";
        }
        o << "}\n";
        o << "var<pixel_local> pls : PLS;\n";
        o << "@fragment fn accumulator() {\n";
        for (size_t i = 0; i < plsTypes.size(); i++) {
            // e.g.: pls.a0 = pls.a0 + 1;
            o << "    pls.a" << i << " = pls.a" << i << " + " << (i + 1) << ";\n";
        }
        o << "}\n";
        o << "\n";
        o << "@group(0) @binding(0) var<storage, read_write> readbackStorageBuffer : array<u32>;\n";
        o << "@fragment fn readbackToStorageBuffer() {\n";
        for (size_t i = 0; i < plsTypes.size(); i++) {
            // e.g.: readbackStorageBuffer[0] = u32(pls.a0);
            o << "    readbackStorageBuffer[" << i << "] = u32(pls.a" << i << ");\n";
        }
        o << "}\n";
        o << "\n";
        o << "@fragment fn copyToColorAttachment() -> @location(0) vec4f {\n";
        o << "    var result : vec4f;\n";
        for (size_t i = 0; i < plsTypes.size(); i++) {
            // e.g.: result[0] = f32(pls.a0) / 255.0;
            o << "    result[" << i << "] = f32(pls.a" << i << ") / 255.0;\n";
        }
        o << "    return result;";
        o << "}\n";

        return utils::CreateShaderModule(device, o.str().c_str());
    }

    wgpu::PipelineLayout MakeTestLayout(const PLSSpec& spec, wgpu::BindGroupLayout bgl = {}) const {
        std::vector<wgpu::PipelineLayoutStorageAttachment> storageAttachments;
        for (const auto& attachmentSpec : spec.attachments) {
            wgpu::PipelineLayoutStorageAttachment attachment;
            attachment.format = attachmentSpec.format;
            attachment.offset = attachmentSpec.offset;
            storageAttachments.push_back(attachment);
        }

        wgpu::PipelineLayoutPixelLocalStorage pls;
        pls.totalPixelLocalStorageSize = spec.totalSize;
        pls.storageAttachmentCount = storageAttachments.size();
        pls.storageAttachments = storageAttachments.data();

        wgpu::PipelineLayoutDescriptor plDesc;
        plDesc.nextInChain = &pls;
        plDesc.bindGroupLayoutCount = 0;
        if (bgl != nullptr) {
            plDesc.bindGroupLayoutCount = 1;
            plDesc.bindGroupLayouts = &bgl;
        }

        return device.CreatePipelineLayout(&plDesc);
    }

    std::vector<wgpu::Texture> MakeTestStorageAttachments(const PLSSpec& spec) const {
        std::vector<wgpu::Texture> attachments;
        for (size_t i = 0; i < spec.attachments.size(); i++) {
            const StorageSpec& attachmentSpec = spec.attachments[i];

            wgpu::TextureDescriptor desc;
            desc.format = attachmentSpec.format;
            desc.size = {1, 1};
            desc.usage = wgpu::TextureUsage::StorageAttachment | wgpu::TextureUsage::CopySrc |
                         wgpu::TextureUsage::CopyDst;
            if (attachmentSpec.discardAfterInit) {
                desc.usage |= wgpu::TextureUsage::RenderAttachment;
            }

            wgpu::Texture attachment = device.CreateTexture(&desc);

            // Initialize the attachment with 1s if LoadOp is Load, copying from another texture
            // so that we avoid adding the extra RenderAttachment usage to the storage attachment.
            if (attachmentSpec.loadOp == wgpu::LoadOp::Load) {
                desc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
                wgpu::Texture clearedTexture = device.CreateTexture(&desc);

                wgpu::CommandEncoder encoder = device.CreateCommandEncoder();

                // The pass that clears clearedTexture.
                utils::ComboRenderPassDescriptor rpDesc({clearedTexture.CreateView()});
                rpDesc.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
                rpDesc.cColorAttachments[0].clearValue = attachmentSpec.clearValue;
                wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rpDesc);
                pass.End();

                // Copy clearedTexture -> attachment.
                wgpu::TexelCopyTextureInfo src = utils::CreateTexelCopyTextureInfo(clearedTexture);
                wgpu::TexelCopyTextureInfo dst = utils::CreateTexelCopyTextureInfo(attachment);
                wgpu::Extent3D copySize = {1, 1, 1};
                encoder.CopyTextureToTexture(&src, &dst, &copySize);

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

            // Discard after initialization to check that the lazy zero init is actually triggered
            // (and it's not just that the resource happened to be zeroes already).
            if (attachmentSpec.discardAfterInit) {
                utils::ComboRenderPassDescriptor rpDesc({attachment.CreateView()});
                rpDesc.cColorAttachments[0].loadOp = wgpu::LoadOp::Load;
                rpDesc.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard;

                wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
                wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rpDesc);
                pass.End();
                wgpu::CommandBuffer commands = encoder.Finish();
                queue.Submit(1, &commands);
            }

            attachments.push_back(attachment);
        }

        return attachments;
    }

    wgpu::RenderPassEncoder BeginTestRenderPass(
        const PLSSpec& spec,
        const wgpu::CommandEncoder& encoder,
        const std::vector<wgpu::Texture>& storageAttachments,
        wgpu::Texture colorAttachment) const {
        std::vector<wgpu::RenderPassStorageAttachment> attachmentDescs;
        for (size_t i = 0; i < spec.attachments.size(); i++) {
            const StorageSpec& attachmentSpec = spec.attachments[i];

            wgpu::RenderPassStorageAttachment attachment;
            attachment.storage = storageAttachments[i].CreateView();
            attachment.offset = attachmentSpec.offset;
            attachment.loadOp = attachmentSpec.loadOp;
            attachment.storeOp = attachmentSpec.storeOp;
            attachment.clearValue = attachmentSpec.clearValue;
            attachmentDescs.push_back(attachment);
        }

        wgpu::RenderPassPixelLocalStorage rpPlsDesc;
        rpPlsDesc.totalPixelLocalStorageSize = spec.totalSize;
        rpPlsDesc.storageAttachmentCount = attachmentDescs.size();
        rpPlsDesc.storageAttachments = attachmentDescs.data();

        wgpu::RenderPassDescriptor rpDesc;
        rpDesc.nextInChain = &rpPlsDesc;
        rpDesc.colorAttachmentCount = 0;
        rpDesc.depthStencilAttachment = nullptr;

        wgpu::RenderPassColorAttachment rpColor;
        if (colorAttachment != nullptr) {
            rpColor.view = colorAttachment.CreateView();
            rpColor.loadOp = wgpu::LoadOp::Clear;
            rpColor.clearValue = {0, 0, 0, 0};
            rpColor.storeOp = wgpu::StoreOp::Store;

            rpDesc.colorAttachments = &rpColor;
            rpDesc.colorAttachmentCount = 1;
        }

        return encoder.BeginRenderPass(&rpDesc);
    }

    uint32_t ComputeExpectedValue(const PLSSpec& spec, size_t slot) {
        for (const StorageSpec& attachment : spec.attachments) {
            if (attachment.offset / kPLSSlotByteSize != slot) {
                continue;
            }

            // Compute the expected value depending on load/store ops by "replaying" the operations
            // that would be done.
            int32_t expectedValue = 0;
            if (!attachment.discardAfterInit) {
                expectedValue = attachment.clearValue.r;
            }
            expectedValue += (slot + 1) * kIterations;

            if (attachment.storeOp == wgpu::StoreOp::Discard) {
                expectedValue = 0;
            }

            return expectedValue;
        }

        // This is not an explicit storage attachment.
        return (slot + 1) * kIterations;
    }

    void CheckByReadingStorageAttachments(const PLSSpec& spec,
                                          const std::vector<wgpu::Texture>& storageAttachments) {
        for (size_t i = 0; i < spec.attachments.size(); i++) {
            const StorageSpec& attachmentSpec = spec.attachments[i];
            uint32_t slot = attachmentSpec.offset / kPLSSlotByteSize;

            uint32_t expectedValue = ComputeExpectedValue(spec, slot);

            switch (spec.attachments[i].format) {
                case wgpu::TextureFormat::R32Float:
                    EXPECT_TEXTURE_EQ(static_cast<float>(expectedValue), storageAttachments[i],
                                      {0, 0});
                    break;
                case wgpu::TextureFormat::R32Uint:
                case wgpu::TextureFormat::R32Sint:
                    EXPECT_TEXTURE_EQ(expectedValue, storageAttachments[i], {0, 0});
                    break;
                default:
                    DAWN_UNREACHABLE();
            }
        }
    }

    void CheckByReadingColorAttachment(const PLSSpec& spec, wgpu::Texture color) {
        std::array<uint32_t, 4> expected = {0, 0, 0, 0};
        for (size_t slot = 0; slot < spec.totalSize / kPLSSlotByteSize; slot++) {
            expected[slot] = ComputeExpectedValue(spec, slot);
        }

        utils::RGBA8 expectedColor(expected[0], expected[1], expected[2], expected[3]);
        EXPECT_TEXTURE_EQ(expectedColor, color, {0, 0});
    }

    void CheckByReadingStorageBuffer(const PLSSpec& spec, wgpu::Buffer buffer) {
        for (size_t slot = 0; slot < spec.totalSize / kPLSSlotByteSize; slot++) {
            uint32_t expectedValue = ComputeExpectedValue(spec, slot);
            EXPECT_BUFFER_U32_EQ(expectedValue, buffer, slot * kPLSSlotByteSize);
        }
    }

    bool RequiresColorAttachment(const PLSSpec& spec) {
        return spec.attachments.empty() || spec.checkMethod == CheckMethod::RenderAttachment;
    }

    void SetColorTargets(const PLSSpec& spec,
                         utils::ComboRenderPipelineDescriptor* desc,
                         bool writesColor) {
        if (RequiresColorAttachment(spec)) {
            desc->cFragment.targetCount = 1;
            desc->cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm;
            desc->cTargets[0].writeMask =
                writesColor ? wgpu::ColorWriteMask::All : wgpu::ColorWriteMask::None;
        } else {
            desc->cFragment.targetCount = 0;
        }
    }

    void DoTest(const PLSSpec& spec) {
        wgpu::ShaderModule module = MakeTestModule(spec);

        // Make the pipeline that will draw a point that adds i to the i-th slot of the PLS.
        wgpu::RenderPipeline accumulatorPipeline;
        {
            utils::ComboRenderPipelineDescriptor desc;
            desc.layout = MakeTestLayout(spec);
            desc.vertex.module = module;
            desc.cFragment.module = module;
            desc.cFragment.entryPoint = "accumulator";
            desc.primitive.topology = wgpu::PrimitiveTopology::PointList;
            SetColorTargets(spec, &desc, false);
            accumulatorPipeline = device.CreateRenderPipeline(&desc);
        }

        wgpu::RenderPipeline checkPipeline;
        wgpu::BindGroup checkBindGroup;
        wgpu::Buffer readbackStorageBuffer;

        if (spec.checkMethod == CheckMethod::StorageBuffer) {
            // Make the pipeline copying the PLS to the storage buffer.
            wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout(
                device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}});

            utils::ComboRenderPipelineDescriptor desc;
            desc.layout = MakeTestLayout(spec, bgl);
            desc.vertex.module = module;
            desc.cFragment.module = module;
            desc.cFragment.entryPoint = "readbackToStorageBuffer";
            desc.primitive.topology = wgpu::PrimitiveTopology::PointList;
            SetColorTargets(spec, &desc, false);
            checkPipeline = device.CreateRenderPipeline(&desc);

            wgpu::BufferDescriptor bufDesc;
            bufDesc.size = spec.totalSize;
            bufDesc.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc;
            readbackStorageBuffer = device.CreateBuffer(&bufDesc);

            checkBindGroup = utils::MakeBindGroup(device, bgl, {{0, readbackStorageBuffer}});
        }
        if (spec.checkMethod == CheckMethod::RenderAttachment) {
            // Make the pipeline copying the PLS to the render attachment.
            utils::ComboRenderPipelineDescriptor desc;
            desc.layout = MakeTestLayout(spec);
            desc.vertex.module = module;
            desc.cFragment.module = module;
            desc.cFragment.entryPoint = "copyToColorAttachment";
            desc.primitive.topology = wgpu::PrimitiveTopology::PointList;
            SetColorTargets(spec, &desc, true);
            checkPipeline = device.CreateRenderPipeline(&desc);
        }

        // Make all the attachments.
        std::vector<wgpu::Texture> storageAttachments = MakeTestStorageAttachments(spec);

        wgpu::Texture colorAttachment;
        if (RequiresColorAttachment(spec)) {
            wgpu::TextureDescriptor desc;
            desc.size = {1, 1};
            desc.format = wgpu::TextureFormat::RGBA8Unorm;
            desc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
            colorAttachment = device.CreateTexture(&desc);
        }

        {
            // Build the render pass with the specified storage attachments
            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
            wgpu::RenderPassEncoder pass =
                BeginTestRenderPass(spec, encoder, storageAttachments, colorAttachment);

            // Draw the points accumulating to PLS, with a PLS barrier if needed.
            pass.SetPipeline(accumulatorPipeline);
            if (supportsCoherent) {
                pass.Draw(kIterations);
            } else {
                for (uint32_t i = 0; i < kIterations; i++) {
                    pass.Draw(1);
                    pass.PixelLocalStorageBarrier();
                }
            }

            // Run the checkPipeline, if any.
            if (checkPipeline != nullptr) {
                pass.SetPipeline(checkPipeline);
                if (checkBindGroup != nullptr) {
                    pass.SetBindGroup(0, checkBindGroup);
                }
                pass.Draw(1);
            }

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

        switch (spec.checkMethod) {
            case CheckMethod::StorageBuffer:
                CheckByReadingStorageBuffer(spec, readbackStorageBuffer);
                break;
            case CheckMethod::ReadStorageAttachments:
                CheckByReadingStorageAttachments(spec, storageAttachments);
                break;
            case CheckMethod::RenderAttachment:
                CheckByReadingColorAttachment(spec, colorAttachment);
                break;
        }

        // Youpi!
    }

    static constexpr uint32_t kIterations = 10;
    bool supportsCoherent;
};

// Test that the various supported PLS format work for accumulation.
TEST_P(PixelLocalStorageTests, Formats) {
    for (const auto format : {wgpu::TextureFormat::R32Uint, wgpu::TextureFormat::R32Sint,
                              wgpu::TextureFormat::R32Float}) {
        PLSSpec spec = {4, {{0, format}}};
        DoTest(spec);
    }
}

// Tests the storage attachment load ops
TEST_P(PixelLocalStorageTests, LoadOp) {
    // Test LoadOp::Clear with a couple values.
    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Uint}}};
        spec.attachments[0].loadOp = wgpu::LoadOp::Clear;

        spec.attachments[0].clearValue.r = 42;
        DoTest(spec);

        spec.attachments[0].clearValue.r = 38;
        DoTest(spec);
    }

    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Sint}}};
        spec.attachments[0].loadOp = wgpu::LoadOp::Clear;

        spec.attachments[0].clearValue.r = 42;
        DoTest(spec);

        spec.attachments[0].clearValue.r = -38;
        DoTest(spec);
    }

    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Float}}};
        spec.attachments[0].loadOp = wgpu::LoadOp::Clear;

        spec.attachments[0].clearValue.r = 4.0;
        DoTest(spec);

        spec.attachments[0].clearValue.r = -3.0;
        DoTest(spec);
    }

    // Test LoadOp::Load (the test helper clears the texture to clearValue).
    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Uint}}};
        spec.attachments[0].clearValue.r = 18;
        spec.attachments[0].loadOp = wgpu::LoadOp::Load;
        DoTest(spec);
    }
}

// Tests the storage attachment store ops
TEST_P(PixelLocalStorageTests, StoreOp) {
    // Test StoreOp::Store.
    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Uint}}};
        spec.attachments[0].storeOp = wgpu::StoreOp::Store;
        DoTest(spec);
    }

    // Test StoreOp::Discard.
    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Uint}}};
        spec.attachments[0].storeOp = wgpu::StoreOp::Discard;
        DoTest(spec);
    }
}

// Test lazy zero initialization of the storage attachments.
TEST_P(PixelLocalStorageTests, ZeroInit) {
    // Discard causes the storage attachment to be lazy zeroed.
    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Uint}}};
        spec.attachments[0].storeOp = wgpu::StoreOp::Discard;
        DoTest(spec);
    }

    // Discard before using as a storage attachment, it should be lazy-cleared.
    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Uint}}};
        spec.attachments[0].loadOp = wgpu::LoadOp::Load;
        spec.attachments[0].clearValue.r = 18;
        spec.attachments[0].discardAfterInit = true;
        DoTest(spec);
    }
}

// Test many explicit storage attachments.
TEST_P(PixelLocalStorageTests, MultipleStorageAttachments) {
    PLSSpec spec = {16,
                    {
                        {0, wgpu::TextureFormat::R32Sint},
                        {4, wgpu::TextureFormat::R32Uint},
                        {8, wgpu::TextureFormat::R32Float},
                        {12, wgpu::TextureFormat::R32Sint},
                    }};
    DoTest(spec);
}

// Test explicit storage attachments in inverse offset order
TEST_P(PixelLocalStorageTests, InvertedOffsetOrder) {
    PLSSpec spec = {8,
                    {
                        {4, wgpu::TextureFormat::R32Uint},
                        {0, wgpu::TextureFormat::R32Sint},
                    }};
    DoTest(spec);
}

// Test implicit pixel local slot.
TEST_P(PixelLocalStorageTests, ImplicitSlot) {
    PLSSpec spec = {4, {}, CheckMethod::StorageBuffer};
    DoTest(spec);
}

// Test multiple implicit pixel local slot.
TEST_P(PixelLocalStorageTests, MultipleImplicitSlot) {
    PLSSpec spec = {16, {}, CheckMethod::StorageBuffer};
    DoTest(spec);
}

// Test mixed implicit / explicit pixel local slot.
TEST_P(PixelLocalStorageTests, MixedImplicitExplicit) {
    {
        PLSSpec spec = {16,
                        {{4, wgpu::TextureFormat::R32Uint}, {8, wgpu::TextureFormat::R32Float}},
                        CheckMethod::StorageBuffer};
        DoTest(spec);
    }
    {
        PLSSpec spec = {16,
                        {{4, wgpu::TextureFormat::R32Uint}, {12, wgpu::TextureFormat::R32Float}},
                        CheckMethod::StorageBuffer};
        DoTest(spec);
    }
    {
        PLSSpec spec = {16,
                        {{0, wgpu::TextureFormat::R32Uint}, {12, wgpu::TextureFormat::R32Float}},
                        CheckMethod::StorageBuffer};
        DoTest(spec);
    }
}

// Test using PLS and then copying it to a render attachment, fully implicit version.
TEST_P(PixelLocalStorageTests, CopyToRenderAttachment) {
    {
        PLSSpec spec = {4, {}, CheckMethod::RenderAttachment};
        DoTest(spec);
    }
    {
        PLSSpec spec = {16, {}, CheckMethod::RenderAttachment};
        DoTest(spec);
    }
}

// Test using PLS and then copying it to a render attachment, fully implicit version.
TEST_P(PixelLocalStorageTests, CopyToRenderAttachmentWithStorageAttachments) {
    {
        PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Float}}, CheckMethod::RenderAttachment};
        DoTest(spec);
    }
    {
        PLSSpec spec = {16, {{8, wgpu::TextureFormat::R32Uint}}, CheckMethod::RenderAttachment};
        DoTest(spec);
    }
}

// Test using PLS in multiple render passes with different sizes in one command buffer, fully
// implicit version.
TEST_P(PixelLocalStorageTests, ImplicitPLSAndLargerRenderAttachmentsInOneCommandBuffer) {
    // Prepare the shader modules with one fragment shader entry point that increases the pixel
    // local storage variables and another fragment shader entry point that computes the output to
    // the color attachment.
    wgpu::ShaderModule wgslModule = utils::CreateShaderModule(device, R"(
            enable chromium_experimental_pixel_local;
            @vertex
            fn vsMain(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4f {
                var pos = array(
                    vec2f(-1.0, -1.0),
                    vec2f(-1.0,  1.0),
                    vec2f( 1.0, -1.0),
                    vec2f( 1.0,  1.0),
                    vec2f(-1.0,  1.0),
                    vec2f( 1.0, -1.0));
                return vec4f(pos[VertexIndex % 6], 0.5, 1.0);
            }

            struct PLS {
                a : u32,
                b : u32,
            };
            var<pixel_local> pls : PLS;
            @fragment fn accumulate() -> @location(0) vec4u {
                pls.a = pls.a + 1;
                pls.b = pls.b + 2;
                return vec4u(0, 0, 0, 1u);
            }

            @fragment fn copyToColorAttachment() -> @location(0) vec4u {
                return vec4u(pls.a + pls.b, 0, 0, 1u);
            })");
    constexpr uint32_t kPixelLocalStorageSize = kPLSSlotByteSize * 2;

    // Create render pipelines in the test
    utils::ComboRenderPipelineDescriptor pipelineDescriptor;
    {
        pipelineDescriptor.vertex.module = wgslModule;
        pipelineDescriptor.cFragment.module = wgslModule;
        pipelineDescriptor.cFragment.entryPoint = "accumulate";
        pipelineDescriptor.cFragment.targetCount = 1;
        pipelineDescriptor.cTargets[0].format = wgpu::TextureFormat::R32Uint;
        pipelineDescriptor.primitive.topology = wgpu::PrimitiveTopology::TriangleList;

        // Create pipeline layout with pixel local storage
        wgpu::PipelineLayoutPixelLocalStorage pixelLocalStoragePipelineLayout;
        pixelLocalStoragePipelineLayout.totalPixelLocalStorageSize = kPixelLocalStorageSize;
        pixelLocalStoragePipelineLayout.storageAttachmentCount = 0;
        wgpu::PipelineLayoutDescriptor pipelineLayoutDesciptor;
        pipelineLayoutDesciptor.nextInChain = &pixelLocalStoragePipelineLayout;
        pipelineLayoutDesciptor.bindGroupLayoutCount = 0;
        pipelineDescriptor.layout = device.CreatePipelineLayout(&pipelineLayoutDesciptor);
    }
    // Create the render pipeline to update pixel local storage values
    wgpu::RenderPipeline accumulatePipeline = device.CreateRenderPipeline(&pipelineDescriptor);

    // Create the render pipeline to compute the final output with the current pixel local storage
    // values
    pipelineDescriptor.cFragment.entryPoint = "copyToColorAttachment";
    wgpu::RenderPipeline copyToColorAttachmentPipeline =
        device.CreateRenderPipeline(&pipelineDescriptor);

    // Create two color attachments with different sizes
    wgpu::Texture color1;
    wgpu::Texture color2;
    constexpr wgpu::Extent3D kColorSize1 = {1, 1, 1};
    constexpr wgpu::Extent3D kColorSize2 = {2, 2, 1};
    {
        wgpu::TextureDescriptor colorDescriptor;
        colorDescriptor.size = kColorSize1;
        colorDescriptor.usage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment;
        colorDescriptor.format = wgpu::TextureFormat::R32Uint;
        color1 = device.CreateTexture(&colorDescriptor);
        colorDescriptor.size = kColorSize2;
        color2 = device.CreateTexture(&colorDescriptor);
    }

    wgpu::CommandEncoder encoder = device.CreateCommandEncoder();

    // Use first color attachment with smaller size
    wgpu::RenderPassDescriptor renderPassDescriptor;
    wgpu::RenderPassPixelLocalStorage renderPassPixelLocalStorageDescriptor;
    wgpu::RenderPassColorAttachment renderPassColor;
    {
        renderPassPixelLocalStorageDescriptor.totalPixelLocalStorageSize = kPixelLocalStorageSize;
        renderPassPixelLocalStorageDescriptor.storageAttachmentCount = 0;
        renderPassDescriptor.nextInChain = &renderPassPixelLocalStorageDescriptor;
        renderPassColor.view = color1.CreateView();
        renderPassColor.loadOp = wgpu::LoadOp::Clear;
        renderPassColor.clearValue = {0, 0, 0, 0};
        renderPassColor.storeOp = wgpu::StoreOp::Store;
        renderPassDescriptor.colorAttachmentCount = 1;
        renderPassDescriptor.colorAttachments = &renderPassColor;
    }
    wgpu::RenderPassEncoder pass1 = encoder.BeginRenderPass(&renderPassDescriptor);
    pass1.SetPipeline(accumulatePipeline);
    if (supportsCoherent) {
        pass1.Draw(kIterations * 6);
    } else {
        for (uint32_t i = 0; i < kIterations; i++) {
            pass1.Draw(6);
            pass1.PixelLocalStorageBarrier();
        }
    }
    pass1.SetPipeline(copyToColorAttachmentPipeline);
    pass1.Draw(6);
    pass1.End();

    // Use second color attachment with larger size
    renderPassColor.view = color2.CreateView();
    wgpu::RenderPassEncoder pass2 = encoder.BeginRenderPass(&renderPassDescriptor);
    pass2.SetPipeline(accumulatePipeline);
    if (supportsCoherent) {
        pass2.Draw(kIterations * 6 * 2);
    } else {
        for (uint32_t i = 0; i < kIterations * 2; i++) {
            pass2.Draw(6);
            pass2.PixelLocalStorageBarrier();
        }
    }

    pass2.SetPipeline(copyToColorAttachmentPipeline);
    pass2.Draw(6);
    pass2.End();

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

    // Check the data in color1 and color2
    constexpr uint32_t kExpectedResult1 = kIterations * 3;
    EXPECT_TEXTURE_EQ(&kExpectedResult1, color1, {0, 0, 0}, {1, 1, 1});

    constexpr std::array<uint32_t, 2> kExpectedResult2 = {{kIterations * 6, kIterations * 6}};
    EXPECT_TEXTURE_EQ(kExpectedResult2.data(), color2, {0, 0, 0}, {2, 1, 1});
    EXPECT_TEXTURE_EQ(kExpectedResult2.data(), color2, {0, 1, 0}, {2, 1, 1});
}

DAWN_INSTANTIATE_TEST(PixelLocalStorageTests, D3D11Backend(), MetalBackend());

}  // anonymous namespace
}  // namespace dawn
