// Copyright 2018 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 "src/dawn/native/opengl/QueueGL.h"

#include <vector>

#include "dawn/platform/DawnPlatform.h"
#include "src/dawn/native/BlitBufferToDepthStencil.h"
#include "src/dawn/native/CommandBuffer.h"
#include "src/dawn/native/CommandEncoder.h"
#include "src/dawn/native/opengl/BufferGL.h"
#include "src/dawn/native/opengl/CommandBufferGL.h"
#include "src/dawn/native/opengl/DeviceGL.h"
#include "src/dawn/native/opengl/EGLFunctions.h"
#include "src/dawn/native/opengl/PhysicalDeviceGL.h"
#include "src/dawn/native/opengl/SharedFenceEGL.h"
#include "src/dawn/native/opengl/TextureGL.h"
#include "src/dawn/native/opengl/UtilsGL.h"
#include "src/dawn/platform/tracing/TraceEvent.h"
#include "src/utils/compiler.h"

namespace dawn::native::opengl {

namespace {
EGLenum EGLSyncTypeFromSharedFenceType(wgpu::SharedFenceType sharedFenceType) {
    switch (sharedFenceType) {
        case wgpu::SharedFenceType::SyncFD:
            return EGL_SYNC_NATIVE_FENCE_ANDROID;
        case wgpu::SharedFenceType::EGLSync:
            return EGL_SYNC_FENCE;
        default:
            DAWN_UNREACHABLE();
    }
}
}  // namespace

ResultOrError<Ref<Queue>> Queue::Create(Device* device, const QueueDescriptor* descriptor) {
    return AcquireRef(new Queue(device, descriptor));
}

Queue::Queue(Device* device, const QueueDescriptor* descriptor) : QueueBase(device, descriptor) {
    const auto& egl = device->GetEGL(false);

    if (egl.HasExt(EGLExt::NativeFenceSync)) {
        mEGLSyncType = EGL_SYNC_NATIVE_FENCE_ANDROID;
    } else if (egl.HasExt(EGLExt::FenceSync)) {
        mEGLSyncType = EGL_SYNC_FENCE;
    } else if (egl.HasExt(EGLExt::ReusableSync)) {
        mEGLSyncType = EGL_SYNC_REUSABLE_KHR;
    } else {
        DAWN_UNREACHABLE();
    }
}

MaybeError Queue::SubmitImpl(uint32_t commandCount, CommandBufferBase* const* commands) {
    Device* device = ToBackend(GetDevice());
    return device->EnqueueAndFlushGL(
        [this, commandCount, commands](const OpenGLFunctions& gl) -> MaybeError {
            TRACE_EVENT_BEGIN0(GetDevice()->GetPlatform(), Recording, "CommandBufferGL::Execute");
            for (uint32_t i = 0; i < commandCount; ++i) {
                DAWN_UNSAFE_TODO(DAWN_TRY(ToBackend(commands[i])->Execute(gl)));
            }
            TRACE_EVENT_END0(GetDevice()->GetPlatform(), Recording, "CommandBufferGL::Execute");
            return {};
        });
}

MaybeError Queue::WriteBufferImpl(BufferBase* buffer,
                                  uint64_t bufferOffset,
                                  const void* data,
                                  size_t size) {
    DAWN_TRY(ToBackend(buffer)->EnsureDataInitializedAsDestination(bufferOffset, size));
    buffer->MarkUsedInPendingCommands();
    return ToBackend(GetDevice())
        ->EnqueueGL(data, size,
                    [buffer = Ref<Buffer>(ToBackend(buffer)), bufferOffset](
                        const OpenGLFunctions& gl, const void* data, size_t size) -> MaybeError {
                        DAWN_GL_TRY(gl, BindBuffer(GL_ARRAY_BUFFER, buffer->GetHandle()));
                        DAWN_GL_TRY(gl, BufferSubData(GL_ARRAY_BUFFER, bufferOffset, size, data));
                        return {};
                    });
}

MaybeError Queue::WriteTextureImpl(const TexelCopyTextureInfo& destination,
                                   const void* data,
                                   size_t dataSize,
                                   const TexelCopyBufferLayout& dataLayout,
                                   const Extent3D& writeSizePixel) {
    TextureCopy textureCopy;
    textureCopy.texture = destination.texture;
    textureCopy.mipLevel = destination.mipLevel;
    textureCopy.origin = destination.origin;
    textureCopy.aspect = SelectFormatAspects(destination.texture->GetFormat(), destination.aspect);

    Device* device = ToBackend(GetDevice());
    if (textureCopy.aspect == Aspect::Stencil &&
        (textureCopy.texture->GetFormat().aspects & Aspect::Depth ||
         device->IsToggleEnabled(Toggle::UseBlitForStencilTextureWrite))) {
        // Workaround when write to stencil is unsupported:
        // - when the texture is stencil-only but OES_texture_stencil8 is unavailable.
        // - when the texture is depth-stencil-combined and writing to the stencil aspect.

        // Call WriteTexture to upload data to an intermediate R8Uint texture.
        TextureDescriptor dataTextureDesc = {};
        dataTextureDesc.format = wgpu::TextureFormat::R8Uint;
        dataTextureDesc.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::TextureBinding;
        dataTextureDesc.size = writeSizePixel;
        dataTextureDesc.mipLevelCount = 1;
        Ref<TextureBase> dataTexture;
        DAWN_TRY_ASSIGN(dataTexture, device->CreateTexture(&dataTextureDesc));
        {
            TexelCopyTextureInfo destinationDataTexture;
            destinationDataTexture.texture = dataTexture.Get();
            destinationDataTexture.aspect = wgpu::TextureAspect::All;
            // The size of R8Uint texture equals to writeSizePixel and only has 1 mip level.
            // So the x,y,z origins and mipLevel are always 0.
            destinationDataTexture.mipLevel = 0;
            destinationDataTexture.origin = {0, 0, 0};
            DAWN_TRY_CONTEXT(WriteTextureImpl(destinationDataTexture, data, dataSize, dataLayout,
                                              writeSizePixel),
                             "writing to stencil aspect of %s using blit workaround when writing "
                             "to an intermediate r8uint texture.",
                             textureCopy.texture.Get());
        }

        // Blit from R8Uint texture to the stencil texture.
        Ref<CommandEncoderBase> commandEncoder;
        DAWN_TRY_ASSIGN(commandEncoder, device->CreateCommandEncoder());
        DAWN_TRY_CONTEXT(BlitR8ToStencil(device, commandEncoder.Get(), dataTexture.Get(),
                                         textureCopy, writeSizePixel),
                         "writing to stencil aspect of %s using blit workaround.",
                         textureCopy.texture.Get());

        Ref<CommandBufferBase> commandBuffer;
        DAWN_TRY_ASSIGN(commandBuffer, commandEncoder->Finish());
        CommandBufferBase* commands = commandBuffer.Get();
        APISubmit(1, &commands);
        return {};
    }

    SubresourceRange range = GetSubresourcesAffectedByCopy(textureCopy, writeSizePixel);
    bool ensureInitialized = false;
    if (IsCompleteSubresourceCopiedTo(destination.texture, writeSizePixel, destination.mipLevel,
                                      destination.aspect)) {
        destination.texture->SetIsSubresourceContentInitialized(true, range);
    } else {
        ensureInitialized = true;
    }

    return device->EnqueueGL(
        data, dataSize,
        [ensureInitialized, dest = Ref<Texture>(ToBackend(destination.texture)), range, textureCopy,
         dataLayout = TexelCopyBufferLayout(dataLayout), writeSizePixel = Extent3D(writeSizePixel)](
            const OpenGLFunctions& gl, const void* data, size_t dataSize) -> MaybeError {
            if (ensureInitialized) {
                DAWN_TRY(dest->EnsureSubresourceContentInitialized(gl, range));
            }
            return DoTexSubImage(gl, textureCopy, data, dataLayout, writeSizePixel);
        });
}

void Queue::SetNeedsFenceSync() {
    mHasPendingUnsignaledCommands = true;
}

ResultOrError<ExecutionSerial> Queue::WaitForQueueSerialImpl(ExecutionSerial waitSerial,
                                                             Nanoseconds timeout) {
    Device* device = ToBackend(GetDevice());
    // This function is called after all dependent GL commands have been flushed in
    // Queue::SubmitImpl(), it's safe to use ExecuteGL().
    return device->ExecuteGL(SubmitMode::Passive, [&](const OpenGLFunctions& gl) -> auto {
        return mFencesInFlight.Use([&](auto fencesInFlight) -> ResultOrError<ExecutionSerial> {
            Ref<WrappedEGLSync> sync;
            ExecutionSerial completedSerial = kWaitSerialTimeout;
            for (auto it = fencesInFlight->begin(); it != fencesInFlight->end(); ++it) {
                if (it->second >= waitSerial) {
                    sync = it->first;
                    completedSerial = it->second;
                    break;
                }
            }
            if (sync == nullptr) {
                // Fence sync not found. This serial must have already completed.
                // Return a success status.
                return waitSerial;
            }

            // Wait for the fence sync.
            GLenum result;
            DAWN_TRY_ASSIGN(result, sync->ClientWait(gl, EGL_SYNC_FLUSH_COMMANDS_BIT, timeout));

            switch (result) {
                case EGL_TIMEOUT_EXPIRED:
                    return kWaitSerialTimeout;
                case EGL_CONDITION_SATISFIED:
                    return completedSerial;
                default:
                    DAWN_UNREACHABLE();
            }
        });
    });
}

MaybeError Queue::SubmitFenceSync() {
    if (!mHasPendingUnsignaledCommands) {
        return {};
    }
    Device* device = ToBackend(GetDevice());
    return device->EnqueueAndFlushGL(
        SubmitMode::Passive, [&](const OpenGLFunctions& gl) -> MaybeError {
            Ref<WrappedEGLSync> sync;
            DisplayEGL* display = ToBackend(device->GetPhysicalDevice())->GetDisplay();
            DAWN_TRY_ASSIGN(sync, WrappedEGLSync::Create(display, gl, mEGLSyncType, nullptr));

            // Signal the sync if it is EGL_SYNC_REUSABLE_KHR. On the other hand,
            // EGL_SYNC_FENCE_KHR has its signal scheduled on creation.
            if (mEGLSyncType == EGL_SYNC_REUSABLE_KHR) {
                DAWN_TRY(sync->Signal(gl, EGL_SIGNALED));
            }

            IncrementLastSubmittedCommandSerial();
            mFencesInFlight->emplace_back(sync, GetLastSubmittedCommandSerial());
            mHasPendingUnsignaledCommands = false;

            return {};
        });
}

ResultOrError<Ref<SharedFence>> Queue::GetOrCreateSharedFence(ExecutionSerial lastUsageSerial,
                                                              wgpu::SharedFenceType type) {
    Ref<WrappedEGLSync> sync;

    EGLenum requestedSyncType = EGLSyncTypeFromSharedFenceType(type);
    // We can use the internal syncs if their type is compatible. All internal syncs are valid for
    // SharedFenceType::EGLSync otherwise the more specific type must match
    bool internalSyncTypeIsCompatibleWithSharedFenceType =
        (type == wgpu::SharedFenceType::EGLSync) || (requestedSyncType == mEGLSyncType);

    if (internalSyncTypeIsCompatibleWithSharedFenceType) {
        // Look for an existing sync that can represent this serial.
        sync = mFencesInFlight.Use([&](auto fencesInFlight) -> Ref<WrappedEGLSync> {
            for (auto it = fencesInFlight->begin(); it != fencesInFlight->end(); ++it) {
                if (it->second >= lastUsageSerial) {
                    return it->first;
                }
            }
            return {};
        });
    }

    Device* device = ToBackend(GetDevice());

    return device->ExecuteGL(
        SubmitMode::Passive, [&](const OpenGLFunctions& gl) -> ResultOrError<Ref<SharedFence>> {
            if (sync == nullptr) {
                DisplayEGL* display = ToBackend(device->GetPhysicalDevice())->GetDisplay();
                DAWN_TRY_ASSIGN(sync,
                                WrappedEGLSync::Create(display, gl, requestedSyncType, nullptr));
            }
            DAWN_ASSERT(sync != nullptr);

            // If we are sharing this sync externally, make sure to flush all commands.
            // The FD cannot be queried before the flush and clients may hang if they try to wait on
            // the sync.
            DAWN_GL_TRY(gl, Flush());

            utils::SystemHandle handle;
            if (type == wgpu::SharedFenceType::SyncFD) {
                EGLint fd;
                DAWN_TRY_ASSIGN(fd, sync->DupFD(gl));

                handle = utils::SystemHandle::Acquire(fd);
            }
            return AcquireRef(new SharedFenceEGL(ToBackend(GetDevice()), "Internal EGLSync", type,
                                                 std::move(handle), sync));
        });
}

bool Queue::HasPendingCommands() const {
    return mHasPendingUnsignaledCommands;
}

MaybeError Queue::SubmitPendingCommandsImpl() {
    DAWN_TRY(SubmitFenceSync());
    return {};
}

ResultOrError<ExecutionSerial> Queue::CheckAndUpdateCompletedSerials() {
    // TODO(crbug.com/40643114): Revisit whether this lock is needed for this backend.
    auto deviceGuard = GetDevice()->GetGuard();

    Device* device = ToBackend(GetDevice());
    // This function is called after all dependent GL commands have been flushed in
    // Queue::SubmitImpl(), it's safe to use ExecuteGL().
    return device->ExecuteGL(SubmitMode::Passive, [&](const OpenGLFunctions& gl) -> auto {
        return mFencesInFlight.Use([&](auto fencesInFlight) -> ResultOrError<ExecutionSerial> {
            ExecutionSerial fenceSerial{0};
            while (!fencesInFlight->empty()) {
                auto [sync, tentativeSerial] = fencesInFlight->front();

                // Fence are added in order, so we can stop searching as soon
                // as we see one that's not ready.
                GLenum result;
                DAWN_TRY_ASSIGN(result,
                                sync->ClientWait(gl, EGL_SYNC_FLUSH_COMMANDS_BIT, Nanoseconds(0)));
                if (result == EGL_TIMEOUT_EXPIRED) {
                    return fenceSerial;
                }
                // Update fenceSerial since fence is ready.
                fenceSerial = tentativeSerial;

                fencesInFlight->pop_front();
            }
            return fenceSerial;
        });
    });
}

void Queue::ForceEventualFlushOfCommands() {
    SetNeedsFenceSync();
}

MaybeError Queue::WaitForIdleForDestructionImpl() {
    Device* device = ToBackend(GetDevice());
    return device->EnqueueAndFlushGL([this](const OpenGLFunctions& gl) -> MaybeError {
        DAWN_GL_TRY(gl, Finish());
        DAWN_TRY(CheckPassedSerials());
        DAWN_ASSERT(mFencesInFlight->empty());
        mHasPendingUnsignaledCommands = false;
        return {};
    });
}

}  // namespace dawn::native::opengl
