Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit a9a3fa6

Browse files
[Impeller] CommandPoolVK recycles command buffers too. (#50468)
Its a bit ambiguous whether or not resetting a cmd pool returns the cmd buffers resources back to the pool in a way that can be reused on subsequent frames. Since reseting the pool resets the cmd buffers to their initial state, they are safe to reuse once reset. Additionally, through profiling I can observe that there is less allocation in cmd buffer creation when the buffers themselves are recycled, at least on the Mali drivers. ### Before (Animated Advanced Blend Macrobenchmark) ![image](https://github.com/flutter/engine/assets/8975114/c0022d7a-d16e-4db5-862a-01d5a6157e18) ### After ![image](https://github.com/flutter/engine/assets/8975114/3b45f9a9-8811-4fbc-8b90-24338317df6e)
1 parent 394b81e commit a9a3fa6

File tree

4 files changed

+122
-25
lines changed

4 files changed

+122
-25
lines changed

impeller/renderer/backend/vulkan/command_pool_vk.cc

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "impeller/renderer/backend/vulkan/resource_manager_vk.h"
1313

1414
#include "impeller/renderer/backend/vulkan/vk.h" // IWYU pragma: keep.
15+
#include "vulkan/vulkan_handles.hpp"
1516
#include "vulkan/vulkan_structs.hpp"
1617

1718
namespace impeller {
@@ -21,12 +22,18 @@ class BackgroundCommandPoolVK final {
2122
public:
2223
BackgroundCommandPoolVK(BackgroundCommandPoolVK&&) = default;
2324

25+
// The recycler also recycles command buffers that were never used, up to a
26+
// limit of 16 per frame. This number was somewhat arbitrarily chosen.
27+
static constexpr size_t kUnusedCommandBufferLimit = 16u;
28+
2429
explicit BackgroundCommandPoolVK(
2530
vk::UniqueCommandPool&& pool,
2631
std::vector<vk::UniqueCommandBuffer>&& buffers,
32+
size_t unused_count,
2733
std::weak_ptr<CommandPoolRecyclerVK> recycler)
2834
: pool_(std::move(pool)),
2935
buffers_(std::move(buffers)),
36+
unused_count_(unused_count),
3037
recycler_(std::move(recycler)) {}
3138

3239
~BackgroundCommandPoolVK() {
@@ -39,9 +46,14 @@ class BackgroundCommandPoolVK final {
3946
if (!recycler) {
4047
return;
4148
}
42-
buffers_.clear();
49+
// If there are many unused command buffers, release some of them.
50+
if (unused_count_ > kUnusedCommandBufferLimit) {
51+
for (auto i = 0u; i < unused_count_; i++) {
52+
buffers_.pop_back();
53+
}
54+
}
4355

44-
recycler->Reclaim(std::move(pool_));
56+
recycler->Reclaim(std::move(pool_), std::move(buffers_));
4557
}
4658

4759
private:
@@ -55,6 +67,7 @@ class BackgroundCommandPoolVK final {
5567
// wrapper type will attempt to reset the cmd buffer, and doing so may be a
5668
// thread safety violation as this may happen on the fence waiter thread.
5769
std::vector<vk::UniqueCommandBuffer> buffers_;
70+
const size_t unused_count_;
5871
std::weak_ptr<CommandPoolRecyclerVK> recycler_;
5972
};
6073

@@ -71,9 +84,16 @@ CommandPoolVK::~CommandPoolVK() {
7184
if (!recycler) {
7285
return;
7386
}
87+
// Any unused command buffers are added to the set of used command buffers.
88+
// both will be reset to the initial state when the pool is reset.
89+
size_t unused_count = unused_command_buffers_.size();
90+
for (auto i = 0u; i < unused_command_buffers_.size(); i++) {
91+
collected_buffers_.push_back(std::move(unused_command_buffers_[i]));
92+
}
93+
unused_command_buffers_.clear();
7494

7595
auto reset_pool_when_dropped = BackgroundCommandPoolVK(
76-
std::move(pool_), std::move(collected_buffers_), recycler);
96+
std::move(pool_), std::move(collected_buffers_), unused_count, recycler);
7797

7898
UniqueResourceVKT<BackgroundCommandPoolVK> pool(
7999
context->GetResourceManager(), std::move(reset_pool_when_dropped));
@@ -90,6 +110,11 @@ vk::UniqueCommandBuffer CommandPoolVK::CreateCommandBuffer() {
90110
if (!pool_) {
91111
return {};
92112
}
113+
if (!unused_command_buffers_.empty()) {
114+
vk::UniqueCommandBuffer buffer = std::move(unused_command_buffers_.back());
115+
unused_command_buffers_.pop_back();
116+
return buffer;
117+
}
93118

94119
auto const device = context->GetDevice();
95120
vk::CommandBufferAllocateInfo info;
@@ -123,6 +148,10 @@ void CommandPoolVK::Destroy() {
123148
for (auto& buffer : collected_buffers_) {
124149
buffer.release();
125150
}
151+
for (auto& buffer : unused_command_buffers_) {
152+
buffer.release();
153+
}
154+
unused_command_buffers_.clear();
126155
collected_buffers_.clear();
127156
}
128157

@@ -158,13 +187,13 @@ std::shared_ptr<CommandPoolVK> CommandPoolRecyclerVK::Get() {
158187
}
159188

160189
// Otherwise, create a new resource and return it.
161-
auto pool = Create();
162-
if (!pool) {
190+
auto data = Create();
191+
if (!data || !data->pool) {
163192
return nullptr;
164193
}
165194

166-
auto const resource =
167-
std::make_shared<CommandPoolVK>(std::move(*pool), context_);
195+
auto const resource = std::make_shared<CommandPoolVK>(
196+
std::move(data->pool), std::move(data->buffers), context_);
168197
pool_map.emplace(hash, resource);
169198

170199
{
@@ -176,10 +205,11 @@ std::shared_ptr<CommandPoolVK> CommandPoolRecyclerVK::Get() {
176205
}
177206

178207
// TODO(matanlurey): Return a status_or<> instead of nullopt when we have one.
179-
std::optional<vk::UniqueCommandPool> CommandPoolRecyclerVK::Create() {
180-
// If we can reuse a command pool, do so.
181-
if (auto pool = Reuse()) {
182-
return pool;
208+
std::optional<CommandPoolRecyclerVK::RecycledData>
209+
CommandPoolRecyclerVK::Create() {
210+
// If we can reuse a command pool and its buffers, do so.
211+
if (auto data = Reuse()) {
212+
return data;
183213
}
184214

185215
// Otherwise, create a new one.
@@ -196,23 +226,27 @@ std::optional<vk::UniqueCommandPool> CommandPoolRecyclerVK::Create() {
196226
if (result != vk::Result::eSuccess) {
197227
return std::nullopt;
198228
}
199-
return std::move(pool);
229+
return CommandPoolRecyclerVK::RecycledData{.pool = std::move(pool),
230+
.buffers = {}};
200231
}
201232

202-
std::optional<vk::UniqueCommandPool> CommandPoolRecyclerVK::Reuse() {
233+
std::optional<CommandPoolRecyclerVK::RecycledData>
234+
CommandPoolRecyclerVK::Reuse() {
203235
// If there are no recycled pools, return nullopt.
204236
Lock recycled_lock(recycled_mutex_);
205237
if (recycled_.empty()) {
206238
return std::nullopt;
207239
}
208240

209241
// Otherwise, remove and return a recycled pool.
210-
auto pool = std::move(recycled_.back());
242+
auto data = std::move(recycled_.back());
211243
recycled_.pop_back();
212-
return std::move(pool);
244+
return std::move(data);
213245
}
214246

215-
void CommandPoolRecyclerVK::Reclaim(vk::UniqueCommandPool&& pool) {
247+
void CommandPoolRecyclerVK::Reclaim(
248+
vk::UniqueCommandPool&& pool,
249+
std::vector<vk::UniqueCommandBuffer>&& buffers) {
216250
// Reset the pool on a background thread.
217251
auto strong_context = context_.lock();
218252
if (!strong_context) {
@@ -223,7 +257,8 @@ void CommandPoolRecyclerVK::Reclaim(vk::UniqueCommandPool&& pool) {
223257

224258
// Move the pool to the recycled list.
225259
Lock recycled_lock(recycled_mutex_);
226-
recycled_.push_back(std::move(pool));
260+
recycled_.push_back(
261+
RecycledData{.pool = std::move(pool), .buffers = std::move(buffers)});
227262
}
228263

229264
CommandPoolRecyclerVK::~CommandPoolRecyclerVK() {

impeller/renderer/backend/vulkan/command_pool_vk.h

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include "impeller/base/thread.h"
1313
#include "impeller/renderer/backend/vulkan/vk.h" // IWYU pragma: keep.
14+
#include "vulkan/vulkan_handles.hpp"
1415

1516
namespace impeller {
1617

@@ -34,10 +35,14 @@ class CommandPoolVK final {
3435
/// @brief Creates a resource that manages the life of a command pool.
3536
///
3637
/// @param[in] pool The command pool to manage.
38+
/// @param[in] buffers Zero or more command buffers in an initial state.
3739
/// @param[in] recycler The context that will be notified on destruction.
38-
explicit CommandPoolVK(vk::UniqueCommandPool pool,
39-
std::weak_ptr<ContextVK>& context)
40-
: pool_(std::move(pool)), context_(context) {}
40+
CommandPoolVK(vk::UniqueCommandPool pool,
41+
std::vector<vk::UniqueCommandBuffer>&& buffers,
42+
std::weak_ptr<ContextVK>& context)
43+
: pool_(std::move(pool)),
44+
unused_command_buffers_(std::move(buffers)),
45+
context_(context) {}
4146

4247
/// @brief Creates and returns a new |vk::CommandBuffer|.
4348
///
@@ -63,6 +68,7 @@ class CommandPoolVK final {
6368

6469
Mutex pool_mutex_;
6570
vk::UniqueCommandPool pool_ IPLR_GUARDED_BY(pool_mutex_);
71+
std::vector<vk::UniqueCommandBuffer> unused_command_buffers_;
6672
std::weak_ptr<ContextVK>& context_;
6773

6874
// Used to retain a reference on these until the pool is reset.
@@ -99,6 +105,12 @@ class CommandPoolRecyclerVK final
99105
public:
100106
~CommandPoolRecyclerVK();
101107

108+
/// A unique command pool and zero or more recycled command buffers.
109+
struct RecycledData {
110+
vk::UniqueCommandPool pool;
111+
std::vector<vk::UniqueCommandBuffer> buffers;
112+
};
113+
102114
/// @brief Clean up resources held by all per-thread command pools
103115
/// associated with the given context.
104116
///
@@ -119,7 +131,8 @@ class CommandPoolRecyclerVK final
119131
/// @brief Returns a command pool to be reset on a background thread.
120132
///
121133
/// @param[in] pool The pool to recycler.
122-
void Reclaim(vk::UniqueCommandPool&& pool);
134+
void Reclaim(vk::UniqueCommandPool&& pool,
135+
std::vector<vk::UniqueCommandBuffer>&& buffers);
123136

124137
/// @brief Clears all recycled command pools to let them be reclaimed.
125138
void Dispose();
@@ -128,17 +141,17 @@ class CommandPoolRecyclerVK final
128141
std::weak_ptr<ContextVK> context_;
129142

130143
Mutex recycled_mutex_;
131-
std::vector<vk::UniqueCommandPool> recycled_ IPLR_GUARDED_BY(recycled_mutex_);
144+
std::vector<RecycledData> recycled_ IPLR_GUARDED_BY(recycled_mutex_);
132145

133146
/// @brief Creates a new |vk::CommandPool|.
134147
///
135148
/// @returns Returns a |std::nullopt| if a pool could not be created.
136-
std::optional<vk::UniqueCommandPool> Create();
149+
std::optional<CommandPoolRecyclerVK::RecycledData> Create();
137150

138-
/// @brief Reuses a recycled |vk::CommandPool|, if available.
151+
/// @brief Reuses a recycled |RecycledData|, if available.
139152
///
140153
/// @returns Returns a |std::nullopt| if a pool was not available.
141-
std::optional<vk::UniqueCommandPool> Reuse();
154+
std::optional<RecycledData> Reuse();
142155

143156
CommandPoolRecyclerVK(const CommandPoolRecyclerVK&) = delete;
144157

impeller/renderer/backend/vulkan/command_pool_vk_unittests.cc

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,53 @@ TEST(CommandPoolRecyclerVKTest, ReclaimMakesCommandPoolAvailable) {
112112
context->Shutdown();
113113
}
114114

115+
TEST(CommandPoolRecyclerVKTest, CommandBuffersAreRecycled) {
116+
auto const context = MockVulkanContextBuilder().Build();
117+
118+
{
119+
// Fetch a pool (which will be created).
120+
auto const recycler = context->GetCommandPoolRecycler();
121+
auto pool = recycler->Get();
122+
123+
auto buffer = pool->CreateCommandBuffer();
124+
pool->CollectCommandBuffer(std::move(buffer));
125+
126+
// This normally is called at the end of a frame.
127+
recycler->Dispose();
128+
}
129+
130+
// Wait for the pool to be reclaimed.
131+
auto waiter = fml::AutoResetWaitableEvent();
132+
auto rattle = DeathRattle([&waiter]() { waiter.Signal(); });
133+
{
134+
UniqueResourceVKT<DeathRattle> resource(context->GetResourceManager(),
135+
std::move(rattle));
136+
}
137+
waiter.Wait();
138+
139+
{
140+
// Create a second pool and command buffer, which should reused the existing
141+
// pool and cmd buffer.
142+
auto const recycler = context->GetCommandPoolRecycler();
143+
auto pool = recycler->Get();
144+
145+
auto buffer = pool->CreateCommandBuffer();
146+
pool->CollectCommandBuffer(std::move(buffer));
147+
148+
// This normally is called at the end of a frame.
149+
recycler->Dispose();
150+
}
151+
152+
// Now check that we only ever created one pool and one command buffer.
153+
auto const called = GetMockVulkanFunctions(context->GetDevice());
154+
EXPECT_EQ(std::count(called->begin(), called->end(), "vkCreateCommandPool"),
155+
1u);
156+
EXPECT_EQ(
157+
std::count(called->begin(), called->end(), "vkAllocateCommandBuffers"),
158+
1u);
159+
160+
context->Shutdown();
161+
}
162+
115163
} // namespace testing
116164
} // namespace impeller

impeller/renderer/backend/vulkan/test/mock_vulkan.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ VkResult vkAllocateCommandBuffers(
261261
const VkCommandBufferAllocateInfo* pAllocateInfo,
262262
VkCommandBuffer* pCommandBuffers) {
263263
MockDevice* mock_device = reinterpret_cast<MockDevice*>(device);
264+
mock_device->AddCalledFunction("vkAllocateCommandBuffers");
264265
*pCommandBuffers =
265266
reinterpret_cast<VkCommandBuffer>(mock_device->NewCommandBuffer());
266267
return VK_SUCCESS;

0 commit comments

Comments
 (0)