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