Flutter Impeller
command_pool_vk.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #include <memory>
8 #include <optional>
9 #include <utility>
10 
13 
14 #include "impeller/renderer/backend/vulkan/vk.h" // IWYU pragma: keep.
15 #include "vulkan/vulkan_enums.hpp"
16 #include "vulkan/vulkan_handles.hpp"
17 #include "vulkan/vulkan_structs.hpp"
18 
19 namespace impeller {
20 
21 // Holds the command pool in a background thread, recyling it when not in use.
23  public:
25 
26  // The recycler also recycles command buffers that were never used, up to a
27  // limit of 16 per frame. This number was somewhat arbitrarily chosen.
28  static constexpr size_t kUnusedCommandBufferLimit = 16u;
29 
31  vk::UniqueCommandPool&& pool,
32  std::vector<vk::UniqueCommandBuffer>&& buffers,
33  size_t unused_count,
34  std::weak_ptr<CommandPoolRecyclerVK> recycler)
35  : pool_(std::move(pool)),
36  buffers_(std::move(buffers)),
37  unused_count_(unused_count),
38  recycler_(std::move(recycler)) {}
39 
41  auto const recycler = recycler_.lock();
42 
43  // Not only does this prevent recycling when the context is being destroyed,
44  // but it also prevents the destructor from effectively being called twice;
45  // once for the original BackgroundCommandPoolVK() and once for the moved
46  // BackgroundCommandPoolVK().
47  if (!recycler) {
48  return;
49  }
50  // If there are many unused command buffers, release some of them and
51  // trim the command pool.
52  bool should_trim = unused_count_ > kUnusedCommandBufferLimit;
53  recycler->Reclaim(std::move(pool_), std::move(buffers_),
54  /*should_trim=*/should_trim);
55  }
56 
57  private:
59 
60  BackgroundCommandPoolVK& operator=(const BackgroundCommandPoolVK&) = delete;
61 
62  vk::UniqueCommandPool pool_;
63 
64  // These are retained because the destructor of the C++ UniqueCommandBuffer
65  // wrapper type will attempt to reset the cmd buffer, and doing so may be a
66  // thread safety violation as this may happen on the fence waiter thread.
67  std::vector<vk::UniqueCommandBuffer> buffers_;
68  const size_t unused_count_;
69  std::weak_ptr<CommandPoolRecyclerVK> recycler_;
70 };
71 
73  if (!pool_) {
74  return;
75  }
76 
77  std::shared_ptr<DeviceHolderVK> device_holder = device_holder_.lock();
78  if (!device_holder) {
79  pool_.release();
80  for (auto& buffer : collected_buffers_) {
81  buffer.release();
82  }
83  for (auto& buffer : unused_command_buffers_) {
84  buffer.release();
85  }
86  return;
87  }
88 
89  auto const context = context_.lock();
90  if (!context) {
91  return;
92  }
93  auto const recycler = context->GetCommandPoolRecycler();
94  if (!recycler) {
95  return;
96  }
97  // Any unused command buffers are added to the set of used command buffers.
98  // both will be reset to the initial state when the pool is reset.
99  size_t unused_count = unused_command_buffers_.size();
100  for (auto i = 0u; i < unused_command_buffers_.size(); i++) {
101  collected_buffers_.push_back(std::move(unused_command_buffers_[i]));
102  }
103  unused_command_buffers_.clear();
104 
105  auto reset_pool_when_dropped = BackgroundCommandPoolVK(
106  std::move(pool_), std::move(collected_buffers_), unused_count, recycler);
107 
109  context->GetResourceManager(), std::move(reset_pool_when_dropped));
110 }
111 
112 // TODO(matanlurey): Return a status_or<> instead of {} when we have one.
113 vk::UniqueCommandBuffer CommandPoolVK::CreateCommandBuffer() {
114  auto const context = context_.lock();
115  if (!context) {
116  return {};
117  }
118 
119  Lock lock(pool_mutex_);
120  if (!pool_) {
121  return {};
122  }
123  if (!unused_command_buffers_.empty()) {
124  vk::UniqueCommandBuffer buffer = std::move(unused_command_buffers_.back());
125  unused_command_buffers_.pop_back();
126  return buffer;
127  }
128 
129  auto const device = context->GetDevice();
130  vk::CommandBufferAllocateInfo info;
131  info.setCommandPool(pool_.get());
132  info.setCommandBufferCount(1u);
133  info.setLevel(vk::CommandBufferLevel::ePrimary);
134  auto [result, buffers] = device.allocateCommandBuffersUnique(info);
135  if (result != vk::Result::eSuccess) {
136  return {};
137  }
138  return std::move(buffers[0]);
139 }
140 
141 void CommandPoolVK::CollectCommandBuffer(vk::UniqueCommandBuffer&& buffer) {
142  Lock lock(pool_mutex_);
143  if (!pool_) {
144  // If the command pool has already been destroyed, then its buffers have
145  // already been freed.
146  buffer.release();
147  return;
148  }
149  collected_buffers_.push_back(std::move(buffer));
150 }
151 
152 void CommandPoolVK::Destroy() {
153  FML_DCHECK(device_holder_.lock());
154  Lock lock(pool_mutex_);
155  pool_.reset();
156 
157  // When the command pool is destroyed, all of its command buffers are freed.
158  // Handles allocated from that pool are now invalid and must be discarded.
159  for (auto& buffer : collected_buffers_) {
160  buffer.release();
161  }
162  for (auto& buffer : unused_command_buffers_) {
163  buffer.release();
164  }
165  unused_command_buffers_.clear();
166  collected_buffers_.clear();
167 }
168 
169 // Associates a resource with a thread and context.
171  std::unordered_map<uint64_t, std::shared_ptr<CommandPoolVK>>;
172 
173 // CommandPoolVK Lifecycle:
174 // 1. End of frame will reset the command pool (clearing this on a thread).
175 // There will still be references to the command pool from the uncompleted
176 // command buffers.
177 // 2. The last reference to the command pool will be released from the fence
178 // waiter thread, which will schedule a task on the resource
179 // manager thread, which in turn will reset the command pool and make it
180 // available for reuse ("recycle").
181 static thread_local std::unique_ptr<CommandPoolMap> tls_command_pool_map;
182 
183 // Map each context to a list of all thread-local command pools associated
184 // with that context.
186 static std::unordered_map<
187  uint64_t,
188  std::unordered_map<std::thread::id, std::weak_ptr<CommandPoolVK>>>
190 
192  const std::shared_ptr<ContextVK>& context)
193  : context_(context), context_hash_(context->GetHash()) {}
194 
195 // Visible for testing.
196 // Returns the number of pools in g_all_pools_map for the given context.
198  Lock all_pools_lock(g_all_pools_map_mutex);
199  auto it = g_all_pools_map.find(context.GetHash());
200  return it != g_all_pools_map.end() ? it->second.size() : 0;
201 }
202 
203 // TODO(matanlurey): Return a status_or<> instead of nullptr when we have one.
204 std::shared_ptr<CommandPoolVK> CommandPoolRecyclerVK::Get() {
205  auto const strong_context = context_.lock();
206  if (!strong_context) {
207  return nullptr;
208  }
209 
210  // If there is a resource in used for this thread and context, return it.
211  if (!tls_command_pool_map.get()) {
213  }
214  CommandPoolMap& pool_map = *tls_command_pool_map.get();
215  auto const it = pool_map.find(context_hash_);
216  if (it != pool_map.end()) {
217  return it->second;
218  }
219 
220  // Otherwise, create a new resource and return it.
221  auto data = Create();
222  if (!data || !data->pool) {
223  return nullptr;
224  }
225 
226  auto const resource = std::make_shared<CommandPoolVK>(
227  std::move(data->pool), std::move(data->buffers), context_,
228  strong_context->GetDeviceHolder());
229  pool_map.emplace(context_hash_, resource);
230 
231  {
232  Lock all_pools_lock(g_all_pools_map_mutex);
233  g_all_pools_map[context_hash_][std::this_thread::get_id()] = resource;
234  }
235 
236  return resource;
237 }
238 
239 // TODO(matanlurey): Return a status_or<> instead of nullopt when we have one.
240 std::optional<CommandPoolRecyclerVK::RecycledData>
241 CommandPoolRecyclerVK::Create() {
242  // If we can reuse a command pool and its buffers, do so.
243  if (auto data = Reuse()) {
244  return data;
245  }
246 
247  // Otherwise, create a new one.
248  auto context = context_.lock();
249  if (!context) {
250  return std::nullopt;
251  }
252  vk::CommandPoolCreateInfo info;
253  info.setQueueFamilyIndex(context->GetGraphicsQueue()->GetIndex().family);
254  info.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
255 
256  auto device = context->GetDevice();
257  auto [result, pool] = device.createCommandPoolUnique(info);
258  if (result != vk::Result::eSuccess) {
259  return std::nullopt;
260  }
261  return CommandPoolRecyclerVK::RecycledData{.pool = std::move(pool),
262  .buffers = {}};
263 }
264 
265 std::optional<CommandPoolRecyclerVK::RecycledData>
266 CommandPoolRecyclerVK::Reuse() {
267  // If there are no recycled pools, return nullopt.
268  Lock recycled_lock(recycled_mutex_);
269  if (recycled_.empty()) {
270  return std::nullopt;
271  }
272 
273  // Otherwise, remove and return a recycled pool.
274  auto data = std::move(recycled_.back());
275  recycled_.pop_back();
276  return std::move(data);
277 }
278 
280  vk::UniqueCommandPool&& pool,
281  std::vector<vk::UniqueCommandBuffer>&& buffers,
282  bool should_trim) {
283  // Reset the pool on a background thread.
284  auto strong_context = context_.lock();
285  if (!strong_context) {
286  return;
287  }
288  auto device = strong_context->GetDevice();
289  vk::CommandPoolResetFlags flags;
290  if (should_trim) {
291  buffers.clear();
292  flags = vk::CommandPoolResetFlagBits::eReleaseResources;
293  }
294  const auto result = device.resetCommandPool(pool.get(), flags);
295  if (result != vk::Result::eSuccess) {
296  VALIDATION_LOG << "Could not reset command pool: " << vk::to_string(result);
297  }
298 
299  // Move the pool to the recycled list.
300  Lock recycled_lock(recycled_mutex_);
301  recycled_.push_back(
302  RecycledData{.pool = std::move(pool), .buffers = std::move(buffers)});
303 }
304 
306  CommandPoolMap* pool_map = tls_command_pool_map.get();
307  if (pool_map) {
308  pool_map->erase(context_hash_);
309  }
310 
311  {
312  Lock all_pools_lock(g_all_pools_map_mutex);
313  auto found = g_all_pools_map.find(context_hash_);
314  if (found != g_all_pools_map.end()) {
315  found->second.erase(std::this_thread::get_id());
316  }
317  }
318 }
319 
321  // Delete the context's entry in this thread's command pool map.
322  if (tls_command_pool_map.get()) {
323  tls_command_pool_map.get()->erase(context_hash_);
324  }
325 
326  // Destroy all other thread-local CommandPoolVK instances associated with
327  // this context.
328  Lock all_pools_lock(g_all_pools_map_mutex);
329  auto found = g_all_pools_map.find(context_hash_);
330  if (found != g_all_pools_map.end()) {
331  for (auto& [thread_id, weak_pool] : found->second) {
332  auto pool = weak_pool.lock();
333  if (!pool) {
334  continue;
335  }
336  // Delete all objects held by this pool. The destroyed pool will still
337  // remain in its thread's TLS map until that thread exits.
338  pool->Destroy();
339  }
340  g_all_pools_map.erase(found);
341  }
342 }
343 
344 } // namespace impeller
BackgroundCommandPoolVK(BackgroundCommandPoolVK &&)=default
BackgroundCommandPoolVK(vk::UniqueCommandPool &&pool, std::vector< vk::UniqueCommandBuffer > &&buffers, size_t unused_count, std::weak_ptr< CommandPoolRecyclerVK > recycler)
static constexpr size_t kUnusedCommandBufferLimit
CommandPoolRecyclerVK(const std::shared_ptr< ContextVK > &context)
Creates a recycler for the given |ContextVK|.
void Dispose()
Clears this context's thread-local command pool.
static int GetGlobalPoolCount(const ContextVK &context)
std::shared_ptr< CommandPoolVK > Get()
Gets a command pool for the current thread.
void DestroyThreadLocalPools()
Clean up resources held by all per-thread command pools associated with the context.
void Reclaim(vk::UniqueCommandPool &&pool, std::vector< vk::UniqueCommandBuffer > &&buffers, bool should_trim=false)
Returns a command pool to be reset on a background thread.
void CollectCommandBuffer(vk::UniqueCommandBuffer &&buffer)
Collects the given |vk::CommandBuffer| to be retained.
vk::UniqueCommandBuffer CreateCommandBuffer()
Creates and returns a new |vk::CommandBuffer|.
A unique handle to a resource which will be reclaimed by the specified resource manager.
static Mutex g_all_pools_map_mutex
static thread_local std::unique_ptr< CommandPoolMap > tls_command_pool_map
std::unordered_map< uint64_t, std::shared_ptr< CommandPoolVK > > CommandPoolMap
static std::unordered_map< uint64_t, std::unordered_map< std::thread::id, std::weak_ptr< CommandPoolVK > > > g_all_pools_map IPLR_GUARDED_BY(g_all_pools_map_mutex)
Definition: comparable.h:93
std::shared_ptr< ContextGLES > context
A unique command pool and zero or more recycled command buffers.
#define VALIDATION_LOG
Definition: validation.h:91