blob: 2213dc39c6de58b743fa10906dd472bb9ed24d71 [file] [log] [blame]
// 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_(nullptr),
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() {
DCHECK(!outstanding_result_pointer_);
if (HaveBuffer()) {
TRACE_EVENT0("gpu", "TransferBuffer::Free");
helper_->OrderingBarrier();
helper_->command_buffer()->DestroyTransferBuffer(buffer_id_);
buffer_id_ = -1;
buffer_ = nullptr;
result_buffer_ = nullptr;
result_shm_offset_ = 0;
DCHECK_EQ(ring_buffer_->NumUsedBlocks(), 0u);
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);
}
unsigned int TransferBuffer::GetMaxSize() const {
return max_buffer_size_ - result_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) {
// We should never attempt to shrink the buffer if someone has a result
// pointer that hasn't been released.
DCHECK(!shrink || !outstanding_result_pointer_);
// 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)) {
// We should never attempt to reallocate the buffer if someone has a result
// pointer that hasn't been released. This would cause a use-after-free.
DCHECK(!outstanding_result_pointer_);
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) {
// We should never attempt to shrink the buffer if someone has a result
// pointer that hasn't been released.
DCHECK(!outstanding_result_pointer_);
// Don't resize the buffer while blocks are in use to avoid throwing away
// live allocations.
if (HaveBuffer() && ring_buffer_->NumUsedBlocks() > 0)
return;
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 nullptr;
}
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 nullptr;
}
unsigned int max_size = ring_buffer_->GetLargestFreeOrPendingSize();
if (size > max_size) {
return nullptr;
}
bytes_since_last_shrink_ += size;
return ring_buffer_->Alloc(size);
}
void* TransferBuffer::AcquireResultBuffer() {
// There should never be two result pointers active at the same time. The
// previous pointer should always be released first. ScopedResultPtr helps
// ensure this invariant.
DCHECK(!outstanding_result_pointer_);
ReallocateRingBuffer(result_size_);
#if DCHECK_IS_ON()
outstanding_result_pointer_ = true;
#endif
return result_buffer_;
}
void TransferBuffer::ReleaseResultBuffer() {
DCHECK(outstanding_result_pointer_);
#if DCHECK_IS_ON()
outstanding_result_pointer_ = false;
#endif
}
int TransferBuffer::GetResultOffset() {
DCHECK(outstanding_result_pointer_);
return result_shm_offset_;
}
int TransferBuffer::GetShmId() {
ReallocateRingBuffer(result_size_);
return buffer_id_;
}
unsigned int TransferBuffer::GetCurrentMaxAllocationWithoutRealloc() const {
return HaveBuffer() ? ring_buffer_->GetLargestFreeOrPendingSize() : 0;
}
ScopedTransferBufferPtr::ScopedTransferBufferPtr(
ScopedTransferBufferPtr&& other)
: buffer_(other.buffer_),
size_(other.size_),
helper_(other.helper_),
transfer_buffer_(other.transfer_buffer_) {
other.buffer_ = nullptr;
other.size_ = 0u;
}
void ScopedTransferBufferPtr::Release() {
if (buffer_) {
transfer_buffer_->FreePendingToken(buffer_, helper_->InsertToken());
buffer_ = nullptr;
size_ = 0;
}
}
void ScopedTransferBufferPtr::Discard() {
if (buffer_) {
transfer_buffer_->DiscardBlock(buffer_);
buffer_ = nullptr;
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;
}
bool ScopedTransferBufferPtr::BelongsToBuffer(char* memory) const {
if (!buffer_)
return false;
char* start = reinterpret_cast<char*>(buffer_);
char* end = start + size_;
return memory >= start && memory <= end;
}
} // namespace gpu