// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// A class to Manage a growing transfer buffer.

#include "gpu/command_buffer/client/transfer_buffer.h"

#include <stddef.h>
#include <stdint.h>

#include "base/bits.h"
#include "base/logging.h"
#include "base/trace_event/trace_event.h"
#include "gpu/command_buffer/client/cmd_buffer_helper.h"

namespace gpu {

TransferBuffer::TransferBuffer(CommandBufferHelper* helper)
    : helper_(helper),
      result_size_(0),
      default_buffer_size_(0),
      min_buffer_size_(0),
      max_buffer_size_(0),
      alignment_(0),
      buffer_id_(-1),
      result_buffer_(NULL),
      result_shm_offset_(0),
      usable_(true) {}

TransferBuffer::~TransferBuffer() {
  Free();
}

base::UnguessableToken TransferBuffer::shared_memory_guid() const {
  if (!HaveBuffer())
    return base::UnguessableToken();
  if (!buffer_->backing())
    return base::UnguessableToken();
  return buffer_->backing()->GetGUID();
}

bool TransferBuffer::Initialize(unsigned int default_buffer_size,
                                unsigned int result_size,
                                unsigned int min_buffer_size,
                                unsigned int max_buffer_size,
                                unsigned int alignment) {
  result_size_ = result_size;
  alignment_ = alignment;
  default_buffer_size_ = base::bits::Align(default_buffer_size, alignment);
  min_buffer_size_ = base::bits::Align(min_buffer_size, alignment);
  max_buffer_size_ = base::bits::Align(max_buffer_size, alignment);
  ReallocateRingBuffer(default_buffer_size_ - result_size);
  return HaveBuffer();
}

void TransferBuffer::Free() {
  if (HaveBuffer()) {
    TRACE_EVENT0("gpu", "TransferBuffer::Free");
    helper_->OrderingBarrier();
    helper_->command_buffer()->DestroyTransferBuffer(buffer_id_);
    buffer_id_ = -1;
    buffer_ = NULL;
    result_buffer_ = NULL;
    result_shm_offset_ = 0;
    previous_ring_buffers_.push_back(std::move(ring_buffer_));
    last_allocated_size_ = 0;
    high_water_mark_ = GetPreviousRingBufferUsedBytes();
    bytes_since_last_shrink_ = 0;
  }
}

bool TransferBuffer::HaveBuffer() const {
  DCHECK(buffer_id_ == -1 || buffer_.get());
  return buffer_id_ != -1;
}

RingBuffer::Offset TransferBuffer::GetOffset(void* pointer) const {
  return ring_buffer_->GetOffset(pointer);
}

void TransferBuffer::DiscardBlock(void* p) {
  ring_buffer_->DiscardBlock(p);
}

void TransferBuffer::FreePendingToken(void* p, unsigned int token) {
  ring_buffer_->FreePendingToken(p, token);
}

unsigned int TransferBuffer::GetSize() const {
  return HaveBuffer() ? ring_buffer_->GetLargestFreeOrPendingSize() : 0;
}

unsigned int TransferBuffer::GetFreeSize() const {
  return HaveBuffer() ? ring_buffer_->GetLargestFreeSizeNoWaiting() : 0;
}

unsigned int TransferBuffer::GetFragmentedFreeSize() const {
  return HaveBuffer() ? ring_buffer_->GetTotalFreeSizeNoWaiting() : 0;
}

void TransferBuffer::ShrinkLastBlock(unsigned int new_size) {
  ring_buffer_->ShrinkLastBlock(new_size);
}

void TransferBuffer::AllocateRingBuffer(unsigned int size) {
  for (;size >= min_buffer_size_; size /= 2) {
    int32_t id = -1;
    scoped_refptr<gpu::Buffer> buffer =
        helper_->command_buffer()->CreateTransferBuffer(size, &id);
    if (id != -1) {
      last_allocated_size_ = size;
      DCHECK(buffer.get());
      buffer_ = buffer;
      ring_buffer_ = std::make_unique<RingBuffer>(
          alignment_, result_size_, buffer_->size() - result_size_, helper_,
          static_cast<char*>(buffer_->memory()) + result_size_);
      buffer_id_ = id;
      result_buffer_ = buffer_->memory();
      result_shm_offset_ = 0;
      bytes_since_last_shrink_ = 0;
      return;
    }
    // we failed so don't try larger than this.
    max_buffer_size_ = base::bits::Align(size / 2, alignment_);
  }
  usable_ = false;
}

static unsigned int ComputePOTSize(unsigned int dimension) {
  return (dimension == 0) ? 0 : 1 << base::bits::Log2Ceiling(dimension);
}

void TransferBuffer::ReallocateRingBuffer(unsigned int size, bool shrink) {
  // What size buffer would we ask for if we needed a new one?
  unsigned int needed_buffer_size = ComputePOTSize(size + result_size_);
  DCHECK_EQ(needed_buffer_size % alignment_, 0u)
      << "Buffer size is not a multiple of alignment_";
  needed_buffer_size = std::max(needed_buffer_size, min_buffer_size_);
  if (!HaveBuffer())
    needed_buffer_size = std::max(needed_buffer_size, default_buffer_size_);
  needed_buffer_size = std::min(needed_buffer_size, max_buffer_size_);

  unsigned int current_size = HaveBuffer() ? buffer_->size() : 0;
  if (current_size == needed_buffer_size)
    return;

  if (usable_ && (shrink || needed_buffer_size > current_size)) {
    if (HaveBuffer()) {
      Free();
    }
    AllocateRingBuffer(needed_buffer_size);
  }
}

unsigned int TransferBuffer::GetPreviousRingBufferUsedBytes() {
  while (!previous_ring_buffers_.empty() &&
         previous_ring_buffers_.front()->GetUsedSize() == 0) {
    previous_ring_buffers_.pop_front();
  }
  unsigned int total = 0;
  for (auto& buffer : previous_ring_buffers_) {
    total += buffer->GetUsedSize();
  }
  return total;
}

void TransferBuffer::ShrinkOrExpandRingBufferIfNecessary(
    unsigned int size_to_allocate) {
  unsigned int available_size = GetFreeSize();
  high_water_mark_ =
      std::max(high_water_mark_, last_allocated_size_ - available_size +
                                     size_to_allocate +
                                     GetPreviousRingBufferUsedBytes());
  if (size_to_allocate > available_size) {
    // Try to expand the ring buffer.
    ReallocateRingBuffer(high_water_mark_);
  } else if (bytes_since_last_shrink_ > high_water_mark_ * kShrinkThreshold) {
    // The intent of the above check is to limit the frequency of buffer shrink
    // attempts. Unfortunately if an application uploads a large amount of data
    // once and from then on uploads only a small amount per frame, it will be a
    // very long time before we attempt to shrink (or forever, if no data is
    // uploaded).
    // TODO(jdarpinian): Change this heuristic to be based on frame number
    // instead, and consider shrinking at the end of each frame (for clients
    // that have a notion of frames).
    bytes_since_last_shrink_ = 0;
    ReallocateRingBuffer(high_water_mark_ + high_water_mark_ / 4,
                         true /* shrink */);
    high_water_mark_ = size_to_allocate + GetPreviousRingBufferUsedBytes();
  }
}

void* TransferBuffer::AllocUpTo(
    unsigned int size, unsigned int* size_allocated) {
  DCHECK(size_allocated);

  ShrinkOrExpandRingBufferIfNecessary(size);

  if (!HaveBuffer()) {
    return NULL;
  }

  unsigned int max_size = ring_buffer_->GetLargestFreeOrPendingSize();
  *size_allocated = std::min(max_size, size);
  bytes_since_last_shrink_ += *size_allocated;
  return ring_buffer_->Alloc(*size_allocated);
}

void* TransferBuffer::Alloc(unsigned int size) {
  ShrinkOrExpandRingBufferIfNecessary(size);

  if (!HaveBuffer()) {
    return NULL;
  }

  unsigned int max_size = ring_buffer_->GetLargestFreeOrPendingSize();
  if (size > max_size) {
    return NULL;
  }
  bytes_since_last_shrink_ += size;
  return ring_buffer_->Alloc(size);
}

void* TransferBuffer::GetResultBuffer() {
  ReallocateRingBuffer(result_size_);
  return result_buffer_;
}

int TransferBuffer::GetResultOffset() {
  ReallocateRingBuffer(result_size_);
  return result_shm_offset_;
}

int TransferBuffer::GetShmId() {
  ReallocateRingBuffer(result_size_);
  return buffer_id_;
}

unsigned int TransferBuffer::GetCurrentMaxAllocationWithoutRealloc() const {
  return HaveBuffer() ? ring_buffer_->GetLargestFreeOrPendingSize() : 0;
}

unsigned int TransferBuffer::GetMaxAllocation() const {
  return HaveBuffer() ? max_buffer_size_ - result_size_ : 0;
}

void ScopedTransferBufferPtr::Release() {
  if (buffer_) {
    transfer_buffer_->FreePendingToken(buffer_, helper_->InsertToken());
    buffer_ = NULL;
    size_ = 0;
  }
}

void ScopedTransferBufferPtr::Discard() {
  if (buffer_) {
    transfer_buffer_->DiscardBlock(buffer_);
    buffer_ = NULL;
    size_ = 0;
  }
}

void ScopedTransferBufferPtr::Reset(unsigned int new_size) {
  Release();
  // NOTE: we allocate buffers of size 0 so that HaveBuffer will be true, so
  // that address will return a pointer just like malloc, and so that GetShmId
  // will be valid. That has the side effect that we'll insert a token on free.
  // We could add code skip the token for a zero size buffer but it doesn't seem
  // worth the complication.
  buffer_ = transfer_buffer_->AllocUpTo(new_size, &size_);
}

void ScopedTransferBufferPtr::Shrink(unsigned int new_size) {
  if (!transfer_buffer_->HaveBuffer() || new_size >= size_)
    return;
  transfer_buffer_->ShrinkLastBlock(new_size);
  size_ = new_size;
}

}  // namespace gpu
