growable command buffer pool

This commit is contained in:
nmzik
2026-08-07 18:26:42 +02:00
parent 67cfec9063
commit 5125cd09c7
3 changed files with 108 additions and 36 deletions
@@ -99,7 +99,10 @@ bool CommandScheduler::InDeferredOperation() noexcept {
CommandScheduler::CommandScheduler(RenderContext& context, GraphicContext& graphics) CommandScheduler::CommandScheduler(RenderContext& context, GraphicContext& graphics)
: m_master(graphics), m_context(context), m_graphics(graphics), : m_master(graphics), m_context(context), m_graphics(graphics),
m_priority_thread([this](std::stop_token stop) { PriorityOperationsThread(stop); }) {} m_priority_thread([this](std::stop_token stop) { PriorityOperationsThread(stop); }) {
m_buffers.reserve(CommandBufferGrowStep);
m_buffer_ticks.reserve(CommandBufferGrowStep);
}
CommandScheduler::~CommandScheduler() { CommandScheduler::~CommandScheduler() {
Shutdown(); Shutdown();
@@ -154,10 +157,7 @@ void CommandScheduler::Begin(HW::Context& registers, HW::UserConfig& user_config
m_shaders = &shaders; m_shaders = &shaders;
if (!Active()) { if (!Active()) {
for (auto& buffer: m_buffers) { m_current = static_cast<int>(GrowCommandBuffers());
buffer = std::make_unique<RenderCommandBuffer>(*this);
}
m_current = 0;
} }
BindCurrent(); BindCurrent();
@@ -235,7 +235,13 @@ void CommandScheduler::Wait(uint64_t tick) {
} }
void CommandScheduler::PopPendingOperations() { void CommandScheduler::PopPendingOperations() {
m_master.Refresh(); PopPendingOperations(true);
}
void CommandScheduler::PopPendingOperations(bool refresh_gpu_tick) {
if (refresh_gpu_tick) {
m_master.Refresh();
}
for (;;) { for (;;) {
Common::UniqueFunction<void> callback; Common::UniqueFunction<void> callback;
{ {
@@ -362,7 +368,7 @@ uint64_t CommandScheduler::NextSubmitSequence() noexcept {
} }
void CommandScheduler::CheckActive() const { void CommandScheduler::CheckActive() const {
EXIT_IF(!Active() || m_current >= BufferCount); EXIT_IF(!Active() || static_cast<size_t>(m_current) >= m_buffers.size());
} }
RenderCommandBuffer& CommandScheduler::Current() const { RenderCommandBuffer& CommandScheduler::Current() const {
@@ -384,15 +390,52 @@ CommandBuffer& CommandScheduler::SubmitCurrent(SubmitInfo& submit) {
const auto signal_tick = m_master.NextTick(); const auto signal_tick = m_master.NextTick();
submit.AddSignal(m_master.Handle(), signal_tick); submit.AddSignal(m_master.Handle(), signal_tick);
submitted.Execute(submit); submitted.Execute(submit);
m_recording = false; m_buffer_ticks[static_cast<size_t>(m_current)] = signal_tick;
m_recording = false;
return submitted; return submitted;
} }
int CommandScheduler::FindReusableBuffer(uint64_t gpu_tick) const {
EXIT_IF(m_buffer_ticks.size() != m_buffers.size());
const auto buffer_count = m_buffers.size();
for (size_t offset = 1; offset <= buffer_count; ++offset) {
const auto candidate = (static_cast<size_t>(m_current) + offset) % buffer_count;
if (gpu_tick >= m_buffer_ticks[candidate]) {
return static_cast<int>(candidate);
}
}
return -1;
}
size_t CommandScheduler::GrowCommandBuffers() {
const auto first = m_buffers.size();
const auto end = first + CommandBufferGrowStep;
m_buffers.reserve(end);
m_buffer_ticks.reserve(end);
for (size_t i = first; i < end; ++i) {
m_buffers.emplace_back(std::make_unique<RenderCommandBuffer>(*this));
m_buffer_ticks.push_back(0);
}
return first;
}
void CommandScheduler::BeginNext() { void CommandScheduler::BeginNext() {
EXIT_IF(m_recording); EXIT_IF(m_recording);
m_current = (m_current + 1) % BufferCount;
auto candidate = FindReusableBuffer(m_master.KnownGpuTick());
bool refreshed = false;
if (candidate < 0) {
m_master.Refresh();
refreshed = true;
candidate = FindReusableBuffer(m_master.KnownGpuTick());
}
if (candidate < 0) {
candidate = static_cast<int>(GrowCommandBuffers());
}
m_current = candidate;
Current().WaitForFenceAndReset(); Current().WaitForFenceAndReset();
PopPendingOperations(); PopPendingOperations(!refreshed);
BindCurrent(); BindCurrent();
Current().Begin(); Current().Begin();
m_recording = true; m_recording = true;
@@ -16,6 +16,7 @@
#include <queue> #include <queue>
#include <thread> #include <thread>
#include <vector>
namespace Libs::Graphics { namespace Libs::Graphics {
@@ -31,8 +32,6 @@ struct CommandSlot {
class CommandScheduler { class CommandScheduler {
public: public:
static constexpr int BufferCount = 8;
CommandScheduler(RenderContext& context, GraphicContext& graphics); CommandScheduler(RenderContext& context, GraphicContext& graphics);
~CommandScheduler(); ~CommandScheduler();
KYTY_CLASS_NO_COPY(CommandScheduler); KYTY_CLASS_NO_COPY(CommandScheduler);
@@ -65,6 +64,8 @@ public:
[[nodiscard]] GraphicContext& Graphics() const noexcept { return m_graphics; } [[nodiscard]] GraphicContext& Graphics() const noexcept { return m_graphics; }
private: private:
static constexpr size_t CommandBufferGrowStep = 4;
class CommandPool { class CommandPool {
public: public:
CommandPool() = default; CommandPool() = default;
@@ -94,30 +95,34 @@ private:
void BindCurrent() const; void BindCurrent() const;
CommandBuffer& SubmitCurrent(SubmitInfo& submit); CommandBuffer& SubmitCurrent(SubmitInfo& submit);
void BeginNext(); void BeginNext();
void PopPendingOperations(bool refresh_gpu_tick);
[[nodiscard]] int FindReusableBuffer(uint64_t gpu_tick) const;
[[nodiscard]] size_t GrowCommandBuffers();
void PriorityOperationsThread(std::stop_token stop); void PriorityOperationsThread(std::stop_token stop);
void RunOperation(Common::UniqueFunction<void>&& operation); void RunOperation(Common::UniqueFunction<void>&& operation);
[[nodiscard]] CommandSlot* AllocateCommandBuffer(); [[nodiscard]] CommandSlot* AllocateCommandBuffer();
[[nodiscard]] uint64_t NextSubmitSequence() noexcept; [[nodiscard]] uint64_t NextSubmitSequence() noexcept;
MasterSemaphore m_master; MasterSemaphore m_master;
RenderContext& m_context; RenderContext& m_context;
GraphicContext& m_graphics; GraphicContext& m_graphics;
CommandPool m_command_pool; CommandPool m_command_pool;
std::array<std::unique_ptr<RenderCommandBuffer>, BufferCount> m_buffers; std::vector<std::unique_ptr<RenderCommandBuffer>> m_buffers;
std::queue<PendingOperation> m_pending_operations; std::vector<uint64_t> m_buffer_ticks;
std::queue<PendingOperation> m_priority_operations; std::queue<PendingOperation> m_pending_operations;
std::mutex m_operation_mutex; std::queue<PendingOperation> m_priority_operations;
std::condition_variable m_operation_available; std::mutex m_operation_mutex;
std::jthread m_priority_thread; std::condition_variable m_operation_available;
bool m_priority_active = false; std::jthread m_priority_thread;
uint64_t m_priority_active_tick = 0; bool m_priority_active = false;
OperationState m_operation_state = OperationState::Open; uint64_t m_priority_active_tick = 0;
int m_current = -1; OperationState m_operation_state = OperationState::Open;
bool m_recording = false; int m_current = -1;
HW::Context* m_registers = nullptr; bool m_recording = false;
HW::UserConfig* m_user_config = nullptr; HW::Context* m_registers = nullptr;
HW::Shader* m_shaders = nullptr; HW::UserConfig* m_user_config = nullptr;
std::atomic<uint64_t> m_submit_sequence = 0; HW::Shader* m_shaders = nullptr;
std::atomic<uint64_t> m_submit_sequence = 0;
friend class CommandBuffer; friend class CommandBuffer;
}; };
+29 -5
View File
@@ -1323,10 +1323,28 @@ public:
const auto external_wait_tick = scheduler.CurrentTick(); const auto external_wait_tick = scheduler.CurrentTick();
SubmitInfo external_wait; SubmitInfo external_wait;
external_wait.AddWait(external_timeline, external_wait_value); external_wait.AddWait(external_timeline, external_wait_value);
scheduler.Flush(external_wait); constexpr size_t blocked_submission_count = 6;
Require("SchedulerTimeline", "external timeline blocked", std::array<vk::CommandBuffer, blocked_submission_count> blocked_handles {};
!scheduler.IsFree(external_wait_tick), for (size_t i = 0; i < blocked_submission_count; ++i) {
"submission ignored the external timeline wait value"); blocked_handles[i] = scheduler.Current().Handle();
if (i == 0) {
scheduler.Flush(external_wait);
} else {
scheduler.Flush();
}
}
bool distinct_handles = true;
for (size_t i = 0; i < blocked_handles.size(); ++i) {
for (size_t j = i + 1; j < blocked_handles.size(); ++j) {
distinct_handles &= blocked_handles[i] != blocked_handles[j];
}
}
const auto last_blocked_tick = scheduler.CurrentTick() - 1;
Require("SchedulerTimeline", "external timeline overflow",
distinct_handles && last_blocked_tick ==
external_wait_tick + blocked_submission_count - 1 &&
!scheduler.IsFree(last_blocked_tick),
"blocked submissions did not grow the command-buffer pool");
vk::SemaphoreSignalInfo signal_info {}; vk::SemaphoreSignalInfo signal_info {};
signal_info.sType = vk::StructureType::eSemaphoreSignalInfo; signal_info.sType = vk::StructureType::eSemaphoreSignalInfo;
signal_info.semaphore = external_timeline; signal_info.semaphore = external_timeline;
@@ -1334,8 +1352,14 @@ public:
Require("SchedulerTimeline", "external timeline signal", Require("SchedulerTimeline", "external timeline signal",
m_runtime_context.device.signalSemaphore(&signal_info) == vk::Result::eSuccess, m_runtime_context.device.signalSemaphore(&signal_info) == vk::Result::eSuccess,
"failed to signal the external timeline semaphore"); "failed to signal the external timeline semaphore");
scheduler.Wait(external_wait_tick); scheduler.Wait(last_blocked_tick);
m_runtime_context.device.destroySemaphore(external_timeline, nullptr); m_runtime_context.device.destroySemaphore(external_timeline, nullptr);
scheduler.Flush();
scheduler.Flush();
Require("SchedulerTimeline", "timeline pool reuse",
std::ranges::find(blocked_handles, scheduler.Current().Handle()) !=
blocked_handles.end(),
"completed command buffers were not reused after timeline progress");
scheduler.Shutdown(); scheduler.Shutdown();
CommandScheduler draining(Renderer(), m_runtime_context); CommandScheduler draining(Renderer(), m_runtime_context);