/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include "vulkan_stream.h"

#include "../../../support/utils.h"
#include "vulkan_device.h"

namespace tvm {
namespace runtime {
namespace vulkan {

VulkanStream::VulkanStream(const VulkanDevice* device)
    : device_(device), state_(new VulkanStreamState()) {
  // create command pool
  VkCommandPoolCreateInfo cmd_pool_cinfo;
  cmd_pool_cinfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
  cmd_pool_cinfo.pNext = nullptr;
  cmd_pool_cinfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
  cmd_pool_cinfo.queueFamilyIndex = device_->queue_family_index;
  VULKAN_CALL(vkCreateCommandPool(*device_, &cmd_pool_cinfo, nullptr, &cmd_pool_));

  VkCommandBufferAllocateInfo buffer_alloc_info;
  buffer_alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  buffer_alloc_info.pNext = nullptr;
  buffer_alloc_info.commandPool = cmd_pool_;
  buffer_alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  buffer_alloc_info.commandBufferCount = 1;
  VULKAN_CALL(vkAllocateCommandBuffers(*device_, &buffer_alloc_info, &(state_->cmd_buffer_)));

  VkFenceCreateInfo fence_cinfo;
  fence_cinfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
  fence_cinfo.pNext = nullptr;
  fence_cinfo.flags = 0;  // VK_FENCE_CREATE_SIGNALED_BIT;
  VULKAN_CALL(vkCreateFence(*device_, &fence_cinfo, nullptr, &(state_->fence_)));

  VkCommandBufferBeginInfo cb_begin;
  cb_begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  cb_begin.pNext = nullptr;
  cb_begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
  cb_begin.pInheritanceInfo = nullptr;
  VULKAN_CALL(vkBeginCommandBuffer(state_->cmd_buffer_, &cb_begin));

  if (support::BoolEnvironmentVar("TVM_USE_AMD_RGP")) {
    profiler_ = new AmdRgpProfiler(device_);
  }
}

VulkanStream::~VulkanStream() {
  vkDestroyFence(*device_, state_->fence_, nullptr);
  vkDestroyCommandPool(*device_, cmd_pool_, nullptr);

  if (profiler_) {
    delete (profiler_);
  }
}

void VulkanStream::Launch(const std::function<void(VulkanStreamState*)>& kernel) {
  if (device_->UseImmediate()) {
    kernel(state_.get());
  } else {
    deferred_kernels_.push_back(kernel);
  }
}

void VulkanStream::LaunchDeferred(const std::function<void()>& deferred_initializer,
                                  const std::function<void(VulkanStreamState*)>& deferred_kernel,
                                  const VulkanStreamToken& deferred_token) {
  TVM_FFI_ICHECK(!device_->UseImmediate());

  // If the new kernel uses the same descriptor set as one of the
  // kernels already in the command buffer, we need to synchronize
  // first.
  if (std::any_of(deferred_tokens_[deferred_token.descriptor_set_].begin(),
                  deferred_tokens_[deferred_token.descriptor_set_].end(),
                  [&](const VulkanStreamToken& token) {
                    TVM_FFI_DCHECK(token.descriptor_set_ == deferred_token.descriptor_set_);
                    return token.descriptor_set_ == deferred_token.descriptor_set_ &&
                           token.buffers_ != deferred_token.buffers_;
                  })) {
    Synchronize();
  }

  // If the new kernel uses the same buffers in the same descriptor
  // set as an already-queued kernel, we don't need to initialize it
  // again.  Since every VulkanWrappedFunc owns a single descriptor
  // set, unless the same function is called with the same buffer
  // arguments, deferred_initializer() will always be called.
  if (!std::any_of(deferred_tokens_[deferred_token.descriptor_set_].begin(),
                   deferred_tokens_[deferred_token.descriptor_set_].end(),
                   [&](const VulkanStreamToken& token) {
                     TVM_FFI_DCHECK(token.descriptor_set_ == deferred_token.descriptor_set_);
                     return token.descriptor_set_ == deferred_token.descriptor_set_ &&
                            token.buffers_ == deferred_token.buffers_;
                   })) {
    deferred_initializer();
  }

  // Save the kernel itself to be called later.
  deferred_kernels_.push_back(deferred_kernel);
  deferred_tokens_[deferred_token.descriptor_set_].push_back(deferred_token);
}

void VulkanStream::Synchronize() {
  if (!device_->UseImmediate()) {
    for (const auto& deferred_kernel : deferred_kernels_) {
      deferred_kernel(state_.get());
    }
    deferred_kernels_.clear();
    deferred_tokens_.clear();
  } else {
    TVM_FFI_DCHECK_EQ(deferred_kernels_.size(), 0);
    TVM_FFI_DCHECK_EQ(deferred_tokens_.size(), 0);
  }

  VULKAN_CALL(vkEndCommandBuffer(state_->cmd_buffer_));
  VkSubmitInfo cb_submit;
  cb_submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
  cb_submit.pNext = nullptr;
  cb_submit.waitSemaphoreCount = 0;
  cb_submit.pWaitSemaphores = nullptr;
  cb_submit.pWaitDstStageMask = nullptr;
  cb_submit.commandBufferCount = 1;
  cb_submit.pCommandBuffers = &(state_->cmd_buffer_);
  cb_submit.signalSemaphoreCount = 0;
  cb_submit.pSignalSemaphores = nullptr;

  if (profiler_) {
    profiler_->capture();
  }

  device_->QueueSubmit(cb_submit, state_->fence_);

  uint64_t timeout = 1UL << 30UL;
  VkResult res;
  do {
    res = vkWaitForFences(*device_, 1, &(state_->fence_), 0, timeout);
  } while (res == VK_TIMEOUT);
  VULKAN_CHECK_ERROR(res);
  VULKAN_CALL(vkResetCommandBuffer(state_->cmd_buffer_, 0));
  VULKAN_CALL(vkResetFences(*device_, 1, &(state_->fence_)));

  // Re-initialize the command buffer
  VkCommandBufferBeginInfo cb_begin;
  cb_begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  cb_begin.pNext = nullptr;
  cb_begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
  cb_begin.pInheritanceInfo = nullptr;
  VULKAN_CALL(vkBeginCommandBuffer(state_->cmd_buffer_, &cb_begin));
}

}  // namespace vulkan
}  // namespace runtime
}  // namespace tvm
