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

#include "src/dawn/native/QueryHelper.h"
#include "src/dawn/tests/DawnTest.h"
#include "src/dawn/utils/WGPUHelpers.h"
#include "src/utils/compiler.h"

namespace dawn {
namespace {

void EncodeConvertTimestampsToNanoseconds(wgpu::CommandEncoder encoder,
                                          uint32_t queryCount,
                                          wgpu::Buffer timestamps,
                                          wgpu::Buffer params) {
    ASSERT_TRUE(native::EncodeConvertTimestampsToNanoseconds(
                    native::FromAPI(encoder.Get()), queryCount, native::FromAPI(timestamps.Get()),
                    native::FromAPI(params.Get()))
                    .IsSuccess());
}

class InternalShaderExpectation : public ::dawn::detail::Expectation {
  public:
    ~InternalShaderExpectation() override = default;

    InternalShaderExpectation(const uint64_t* values,
                              const unsigned int count,
                              const uint32_t quantizationMask) {
        mExpected.assign(values, DAWN_UNSAFE_TODO(values + count));
        mQuantizationMask = quantizationMask;
    }

    // Expect the actual results are approximately equal to the expected values.
    testing::AssertionResult Check(const void* data, size_t size) override {
        DAWN_ASSERT(size == sizeof(uint64_t) * mExpected.size());
        // The computations in the shader use a multiplier that's a 16bit integer plus a shift
        // that maximize the multiplier. This means that for the range of periods we care about
        // (1 to 2^16-1 ns per tick), the high order bit of the multiplier will always be set.
        // Intuitively this means that we have 15 bits of precision in the computation so we
        // expect that for the error tolerance.
        constexpr static double kErrorToleranceRatio = 1.0 / (1 << 15);  // about 3e-5.

        constexpr static double kUpperLimitMultiplier = 1.0 + kErrorToleranceRatio;
        constexpr static double kLowerLimitMultiplier = 1.0 - kErrorToleranceRatio;

        const uint64_t* actual = static_cast<const uint64_t*>(data);
        for (size_t i = 0; i < mExpected.size(); ++i) {
            if (mExpected[i] == 0) {
                if (DAWN_UNSAFE_TODO(actual[i]) != 0) {
                    return testing::AssertionFailure()
                           << "Expected data[" << i << "] to be 0, actual "
                           << DAWN_UNSAFE_TODO(actual[i]) << "\n";
                }
                continue;
            }

            uint64_t expected = mExpected[i];
            uint64_t upperLimit = static_cast<double>(mExpected[i]) * kUpperLimitMultiplier;
            uint64_t lowerLimit = static_cast<double>(mExpected[i]) * kLowerLimitMultiplier;

            // Quantization may make an actual value close to the lower limit go below it.
            // Take this into account by also quantizing the lower limit.
            uint32_t invertedQuantizationMask = ~mQuantizationMask;
            uint64_t quantizationMask64 = ~uint64_t(invertedQuantizationMask);
            lowerLimit &= quantizationMask64;

            if (DAWN_UNSAFE_TODO(actual[i]) < lowerLimit ||
                DAWN_UNSAFE_TODO(actual[i]) > upperLimit) {
                return testing::AssertionFailure()
                       << "Expected data[" << i << "] to be " << expected << ", actual "
                       << DAWN_UNSAFE_TODO(actual[i]) << ". Error rate is larger than "
                       << kErrorToleranceRatio << ". Upper limit is " << upperLimit
                       << ". Lower limit is " << lowerLimit << "\n";
            }

            if ((DAWN_UNSAFE_TODO(actual[i]) & ~quantizationMask64) != 0) {
                return testing::AssertionFailure()
                       << "Actual data 0x" << std::hex << DAWN_UNSAFE_TODO(actual[i])
                       << " does not match quantization mask 0x" << std::hex << mQuantizationMask
                       << "\n";
            }
        }

        return testing::AssertionSuccess();
    }

  private:
    std::vector<uint64_t> mExpected;
    uint32_t mQuantizationMask;
};

class QueryInternalShaderTests : public DawnTest {
  protected:
    void SetUp() override {
        DawnTest::SetUp();

        DAWN_TEST_UNSUPPORTED_IF(UsesWire());
        DAWN_TEST_UNSUPPORTED_IF(HasToggleEnabled("disable_timestamp_query_conversion"));

        // The test flakily fails on Mali GPUs (see crbug.com/452388097).
        DAWN_SUPPRESS_TEST_IF(IsARM());

        // If implicit device synchronization is turned on, EncodeConvertTimestampsToNanoseconds
        // will expect the device to be locked. But we are calling it directly without going through
        // wgpu API, hence the device won't be locked on this route. This would lead to assertion
        // failure. So disable the test if this feature is turned on.
        DAWN_TEST_UNSUPPORTED_IF(IsImplicitDeviceSyncEnabled());
    }

    // Original timestamp values in query set for testing
    const std::vector<uint64_t> querySetValues = {
        0,            // A zero (written prior for an unavailable query) stays zero.
        10079569507,  // t0
        10394415012,  // t1
        0,            // Same for a zero in the middle.
        11713454943,  // t2
        38912556941,  // t3 (big value)
        10080295766,  // t4 (reset)
        12159966783,  // t5 (after reset)
        12651224612,  // t6
        39872473956,  // t7
    };

    const uint32_t kQueryCount = querySetValues.size();

    const std::vector<uint64_t> GetExpectedResults(const std::vector<uint64_t>& origin,
                                                   uint32_t start,
                                                   uint32_t firstQuery,
                                                   uint32_t queryCount,
                                                   float period) {
        std::vector<uint64_t> expected(origin.begin(), origin.end());
        for (size_t i = 0; i < queryCount; i++) {
            // Maybe the timestamp * period is larger than the maximum of uint64, so cast the
            // delta value to double (higher precision than float)
            expected[start + i] =
                static_cast<uint64_t>(static_cast<double>(origin[start + i]) * period);
        }
        return expected;
    }

    void RunTest(uint32_t firstQuery,
                 uint32_t queryCount,
                 uint32_t destinationOffset,
                 uint32_t quantizationMask,
                 float period) {
        DAWN_ASSERT(destinationOffset % kQueryResolveAlignment == 0);

        uint64_t size = queryCount * sizeof(uint64_t) + destinationOffset;

        // The resolve buffer storing original timestamps and the converted values
        wgpu::BufferDescriptor timestampsDesc;
        timestampsDesc.size = size;
        timestampsDesc.usage = wgpu::BufferUsage::QueryResolve | wgpu::BufferUsage::CopySrc |
                               wgpu::BufferUsage::CopyDst;
        wgpu::Buffer timestampsBuffer = device.CreateBuffer(&timestampsDesc);

        // Set sentinel values to check the slots before the destination offset should not be
        // converted
        std::vector<uint64_t> timestampValues(size / sizeof(uint64_t), 1u);
        uint32_t start = destinationOffset / sizeof(uint64_t);
        for (uint32_t i = 0; i < queryCount; i++) {
            timestampValues[start + i] = querySetValues[firstQuery + i];
        }
        // Write sentinel values and original timestamps to timestamps buffer
        queue.WriteBuffer(timestampsBuffer, 0, timestampValues.data(), size);

        // The params uniform buffer
        native::TimestampParams params(queryCount, destinationOffset, quantizationMask, period);
        wgpu::Buffer paramsBuffer = utils::CreateBufferFromData(device, &params, sizeof(params),
                                                                wgpu::BufferUsage::Uniform);

        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
        EncodeConvertTimestampsToNanoseconds(encoder, queryCount, timestampsBuffer, paramsBuffer);
        wgpu::CommandBuffer commands = encoder.Finish();
        queue.Submit(1, &commands);

        const std::vector<uint64_t> expected =
            GetExpectedResults(timestampValues, start, firstQuery, queryCount, period);

        EXPECT_BUFFER(timestampsBuffer, 0, size,
                      new InternalShaderExpectation(expected.data(), size / sizeof(uint64_t),
                                                    quantizationMask))
            << "Conversion test for period:" << period << " firstQuery:" << firstQuery
            << " queryCount:" << queryCount << " destinationOffset:" << destinationOffset
            << " quantizationMask: 0x" << std::hex << quantizationMask;
    }
};

// Test the accuracy of timestamp compute shader which uses unsigned 32-bit integers to simulate
// unsigned 64-bit integers (timestamps) multiplied by float (period).
// The arguments pass to timestamp internal pipeline:
// - The timestamps buffer contains the original timestamps resolved from query set (created
//   manually here), and will be used to store the results processed by the compute shader.
//   Expect 0 for unavailable timestamps and nanoseconds for available timestamps in an expected
//   error tolerance ratio.
// - The availability buffer passes the data of which slot in timestamps buffer is an initialized
//    timestamp.
// - The params buffer passes the timestamp count, the offset in timestamps buffer and the
//   timestamp period (here use GPU frequency (HZ) on Intel D3D12 to calculate the period in
//   ns for testing).
TEST_P(QueryInternalShaderTests, TimestampComputeShaderMultiplication) {
    DAWN_SUPPRESS_TEST_IF(IsWARP());

    constexpr std::array<float, 5> kPeriodsToTest = {
        1,
        7,
        // A gpu frequency on Intel D3D12 (ticks/second)
        83.333,
        1042,
        65535,
    };

    const uint32_t kNoOpQuantizationBitmask = 0xFFFFFFFF;

    for (float period : kPeriodsToTest) {
        // Convert timestamps in timestamps buffer with offset 0
        // Test for ResolveQuerySet(querySet, 0, kQueryCount, timestampsBuffer, 0)
        RunTest(0, kQueryCount, 0, kNoOpQuantizationBitmask, period);

        // Convert timestamps in timestamps buffer with offset 256
        // Test for ResolveQuerySet(querySet, 1, kQueryCount - 1, timestampsBuffer, 256)
        RunTest(1, kQueryCount - 1, kQueryResolveAlignment, kNoOpQuantizationBitmask, period);

        // Convert partial timestamps in timestamps buffer with offset 256
        // Test for ResolveQuerySet(querySet, 1, 4, timestampsBuffer, 256)
        RunTest(1, 4, kQueryResolveAlignment, kNoOpQuantizationBitmask, period);
    }
}

TEST_P(QueryInternalShaderTests, TimestampComputeShaderQuantization) {
    DAWN_SUPPRESS_TEST_IF(IsWARP());
    DAWN_TEST_UNSUPPORTED_IF(!HasToggleEnabled("timestamp_quantization"));

    constexpr std::array<uint32_t, 3> kQuantizationMasksToTest = {
        0xFFFFFFFF,
        0xFFFFFF00,
        0xFFFF0000,
    };

    for (uint32_t quantizationMask : kQuantizationMasksToTest) {
        // Convert timestamps in timestamps buffer with offset 0
        // Test for ResolveQuerySet(querySet, 0, kQueryCount, timestampsBuffer, 0)
        RunTest(0, kQueryCount, 0, quantizationMask, 1);
    }
}

DAWN_INSTANTIATE_TEST(QueryInternalShaderTests, D3D12Backend(), MetalBackend(), VulkanBackend());

}  // anonymous namespace
}  // namespace dawn
