From 181b2af5a4c0ffa2124e97d6a6e512705ec76fc7 Mon Sep 17 00:00:00 2001 From: "Dr. Chat" Date: Fri, 25 Mar 2016 13:45:44 -0500 Subject: [PATCH] Vulkan Circular Buffer --- src/xenia/ui/vulkan/circular_buffer.cc | 258 +++++++++++++++++++++++++ src/xenia/ui/vulkan/circular_buffer.h | 85 ++++++++ 2 files changed, 343 insertions(+) create mode 100644 src/xenia/ui/vulkan/circular_buffer.cc create mode 100644 src/xenia/ui/vulkan/circular_buffer.h diff --git a/src/xenia/ui/vulkan/circular_buffer.cc b/src/xenia/ui/vulkan/circular_buffer.cc new file mode 100644 index 000000000..4cc22366f --- /dev/null +++ b/src/xenia/ui/vulkan/circular_buffer.cc @@ -0,0 +1,258 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2015 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include + +#include "xenia/base/assert.h" +#include "xenia/base/logging.h" +#include "xenia/base/math.h" + +#include "xenia/ui/vulkan/circular_buffer.h" + +namespace xe { +namespace ui { +namespace vulkan { + +CircularBuffer::CircularBuffer(VulkanDevice* device) : device_(device) {} +CircularBuffer::~CircularBuffer() { Shutdown(); } + +bool CircularBuffer::Initialize(VkDeviceSize capacity, VkBufferUsageFlags usage, + VkDeviceSize alignment) { + VkResult status = VK_SUCCESS; + capacity = xe::round_up(capacity, alignment); + + // Create our internal buffer. + VkBufferCreateInfo buffer_info; + buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buffer_info.pNext = nullptr; + buffer_info.flags = 0; + buffer_info.size = capacity; + buffer_info.usage = usage; + buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + buffer_info.queueFamilyIndexCount = 0; + buffer_info.pQueueFamilyIndices = nullptr; + status = vkCreateBuffer(*device_, &buffer_info, nullptr, &gpu_buffer_); + CheckResult(status, "vkCreateBuffer"); + if (status != VK_SUCCESS) { + return false; + } + + VkMemoryRequirements reqs; + vkGetBufferMemoryRequirements(*device_, gpu_buffer_, &reqs); + + // Allocate memory from the device to back the buffer. + assert_true(reqs.size == capacity); + reqs.alignment = std::max(alignment, reqs.alignment); + gpu_memory_ = device_->AllocateMemory(reqs); + if (!gpu_memory_) { + XELOGE("CircularBuffer::Initialize - Failed to allocate memory!"); + Shutdown(); + return false; + } + + alignment_ = reqs.alignment; + capacity_ = reqs.size; + gpu_base_ = 0; + + // Bind the buffer to its backing memory. + status = vkBindBufferMemory(*device_, gpu_buffer_, gpu_memory_, gpu_base_); + CheckResult(status, "vkBindBufferMemory"); + if (status != VK_SUCCESS) { + XELOGE("CircularBuffer::Initialize - Failed to bind memory!"); + Shutdown(); + return false; + } + + // Map the memory so we can access it. + status = vkMapMemory(*device_, gpu_memory_, gpu_base_, capacity_, 0, + reinterpret_cast(&host_base_)); + CheckResult(status, "vkMapMemory"); + if (status != VK_SUCCESS) { + XELOGE("CircularBuffer::Initialize - Failed to map memory!"); + Shutdown(); + return false; + } + + return true; +} + +void CircularBuffer::Shutdown() { + Clear(); + if (host_base_) { + vkUnmapMemory(*device_, gpu_memory_); + host_base_ = nullptr; + } + if (gpu_buffer_) { + vkDestroyBuffer(*device_, gpu_buffer_, nullptr); + gpu_buffer_ = nullptr; + } + if (gpu_memory_) { + vkFreeMemory(*device_, gpu_memory_, nullptr); + gpu_memory_ = nullptr; + } +} + +bool CircularBuffer::CanAcquire(VkDeviceSize length) { + // Make sure the length is aligned. + length = xe::round_up(length, alignment_); + if (allocations_.empty()) { + // Read head has caught up to write head (entire buffer available for write) + assert(read_head_ == write_head_); + return capacity_ > length; + } else if (write_head_ < read_head_) { + // Write head wrapped around and is behind read head. + // | write |---- read ----| + return (read_head_ - write_head_) > length; + } else { + // Read head behind write head. + // 1. Check if there's enough room from write -> capacity + // | |---- read ----| write | + if ((capacity_ - write_head_) > length) { + return true; + } + + // 2. Check if there's enough room from 0 -> read + // | write |---- read ----| | + if ((read_head_) > length) { + return true; + } + } + + return false; +} + +CircularBuffer::Allocation* CircularBuffer::Acquire( + VkDeviceSize length, std::shared_ptr fence) { + if (!CanAcquire(length)) { + return nullptr; + } + + VkDeviceSize aligned_length = xe::round_up(length, alignment_); + if (allocations_.empty()) { + // Entire buffer available. + assert(read_head_ == write_head_); + assert(capacity_ > aligned_length); + + read_head_ = 0; + write_head_ = length; + + auto alloc = new Allocation(); + alloc->host_ptr = host_base_ + 0; + alloc->gpu_memory = gpu_memory_; + alloc->offset = gpu_base_ + 0; + alloc->length = length; + alloc->aligned_length = aligned_length; + alloc->fence = fence; + allocations_.push_back(alloc); + return alloc; + } else if (write_head_ < read_head_) { + // Write head behind read head. + assert_true(read_head_ - write_head_ >= aligned_length); + + auto alloc = new Allocation(); + alloc->host_ptr = host_base_ + write_head_; + alloc->gpu_memory = gpu_memory_; + alloc->offset = gpu_base_ + write_head_; + alloc->length = length; + alloc->aligned_length = aligned_length; + alloc->fence = fence; + write_head_ += aligned_length; + allocations_.push_back(alloc); + + return alloc; + } else { + // Write head after read head + if (capacity_ - write_head_ >= aligned_length) { + // Free space from write -> capacity + auto alloc = new Allocation(); + alloc->host_ptr = host_base_ + write_head_; + alloc->gpu_memory = gpu_memory_; + alloc->offset = gpu_base_ + write_head_; + alloc->length = length; + alloc->aligned_length = aligned_length; + alloc->fence = fence; + write_head_ += aligned_length; + allocations_.push_back(alloc); + + return alloc; + } else if ((read_head_ - 0) > aligned_length) { + // Free space from begin -> read + auto alloc = new Allocation(); + alloc->host_ptr = host_base_ + write_head_; + alloc->gpu_memory = gpu_memory_; + alloc->offset = gpu_base_ + 0; + alloc->length = length; + alloc->aligned_length = aligned_length; + alloc->fence = fence; + write_head_ = aligned_length; + allocations_.push_back(alloc); + + return alloc; + } + } + + return nullptr; +} + +void CircularBuffer::Discard(Allocation* allocation) { + // TODO: Revert write_head_ (only if this is the last alloc though) + // Or maybe just disallow discards. + for (auto it = allocations_.begin(); it != allocations_.end(); ++it) { + if (*it == allocation) { + allocations_.erase(it); + break; + } + } + + delete allocation; +} + +void CircularBuffer::Flush(Allocation* allocation) { + VkMappedMemoryRange range; + range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + range.pNext = nullptr; + range.memory = gpu_memory_; + range.offset = gpu_base_ + allocation->offset; + range.size = allocation->length; + vkFlushMappedMemoryRanges(*device_, 1, &range); +} + +void CircularBuffer::Clear() { + for (auto it = allocations_.begin(); it != allocations_.end();) { + delete *it; + it = allocations_.erase(it); + } + + write_head_ = read_head_ = 0; +} + +void CircularBuffer::Scavenge() { + for (auto it = allocations_.begin(); it != allocations_.end();) { + if ((*it)->fence->status() != VK_SUCCESS) { + // Don't bother freeing following allocations to ensure proper ordering. + break; + } + + read_head_ = (read_head_ + (*it)->aligned_length) % capacity_; + delete *it; + it = allocations_.erase(it); + } + + if (allocations_.empty()) { + // Reset R/W heads. + read_head_ = write_head_ = 0; + } else { + // FIXME: Haven't verified this works correctly when actually rotating :P + assert_always(); + } +} + +} // namespace vulkan +} // namespace ui +} // namespace xe \ No newline at end of file diff --git a/src/xenia/ui/vulkan/circular_buffer.h b/src/xenia/ui/vulkan/circular_buffer.h new file mode 100644 index 000000000..2c036c685 --- /dev/null +++ b/src/xenia/ui/vulkan/circular_buffer.h @@ -0,0 +1,85 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2015 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_UI_VULKAN_CIRCULAR_BUFFER_H_ +#define XENIA_UI_VULKAN_CIRCULAR_BUFFER_H_ + +#include + +#include "xenia/ui/vulkan/vulkan.h" +#include "xenia/ui/vulkan/vulkan_device.h" + +namespace xe { +namespace ui { +namespace vulkan { + +// A circular buffer, intended to hold (fairly) temporary memory that will be +// released when a fence is signaled. Best used when allocations are taken +// in-order with command buffer submission. +// +// Allocations loop around the buffer in circles (but are not fragmented at the +// ends of the buffer), where trailing older allocations are freed after use. +class CircularBuffer { + public: + CircularBuffer(VulkanDevice* device); + ~CircularBuffer(); + + struct Allocation { + void* host_ptr; + VkDeviceMemory gpu_memory; + VkDeviceSize offset; + VkDeviceSize length; + VkDeviceSize aligned_length; + + // Allocation usage fence. This allocation will be deleted when the fence + // becomes signaled. + std::shared_ptr fence; + }; + + bool Initialize(VkDeviceSize capacity, VkBufferUsageFlags usage, + VkDeviceSize alignment = 256); + void Shutdown(); + + VkDeviceSize capacity() const { return capacity_; } + VkBuffer gpu_buffer() const { return gpu_buffer_; } + VkDeviceMemory gpu_memory() const { return gpu_memory_; } + uint8_t* host_base() const { return host_base_; } + + bool CanAcquire(VkDeviceSize length); + Allocation* Acquire(VkDeviceSize length, std::shared_ptr fence); + void Discard(Allocation* allocation); + void Flush(Allocation* allocation); + + // Clears all allocations, regardless of whether they've been consumed or not. + void Clear(); + + // Frees any allocations whose fences have been signaled. + void Scavenge(); + + private: + VkDeviceSize capacity_ = 0; + VkDeviceSize alignment_ = 0; + VkDeviceSize write_head_ = 0; + VkDeviceSize read_head_ = 0; + + VulkanDevice* device_; + VkBuffer gpu_buffer_ = nullptr; + VkDeviceMemory gpu_memory_ = nullptr; + VkDeviceSize gpu_base_ = 0; + uint8_t* host_base_ = nullptr; + + std::unordered_map allocation_cache_; + std::vector allocations_; +}; + +} // namespace vulkan +} // namespace ui +} // namespace xe + +#endif // XENIA_UI_GL_CIRCULAR_BUFFER_H_