Сначала я записываю буфер команд вычислений:
void record_compute_command_buffer() {
vk::CommandBufferBeginInfo begin_info{};
begin_info.flags = vk::CommandBufferUsageFlagBits::eSimultaneousUse;
GPU_.compute_command_buffer_.begin(begin_info);
GPU_.compute_command_buffer_.bindPipeline(vk::PipelineBindPoint::eCompute, GPU_.compute_pipeline_);
struct push_data {
alignas(16) glm::mat4 view_matrix = camera_.get_view();
alignas(16) glm::mat4 projection_matrix = glm::perspective(glm::radians(90.0f), GPU_.aspect_ratio(), 0.1f, 10.0f);
} push;
GPU_.compute_command_buffer_.pushConstants(GPU_.compute_pipeline_layout_, vk::ShaderStageFlagBits::eCompute, 0, sizeof(push), &push);
GPU_.compute_command_buffer_.bindDescriptorSets(
vk::PipelineBindPoint::eCompute,
GPU_.compute_pipeline_layout_,
0,
1,
&GPU_.compute_descriptor_sets_[0],
0,
nullptr
);
const int workgroup_size = 32;
const int groupcount = ((models.size()) / workgroup_size) + 1;
GPU_.compute_command_buffer_.dispatch(groupcount, 1, 1);
GPU_.compute_command_buffer_.end();
}
Затем в моей функции draw_frame я отправляю буфер и начинаю записывать буферы команд отрисовки с синхронизацией:
void draw_frame() {
vk::SubmitInfo compute_submit_info{};
compute_submit_info.commandBufferCount = 1;
compute_submit_info.pCommandBuffers = &GPU_.compute_command_buffer_;
GPU_.compute_queue_.submit(compute_submit_info);
GPU_.logical_device_.waitForFences(GPU_.in_flight_fences[current_frame], true, UINT64_MAX);
GPU_.logical_device_.resetFences(GPU_.in_flight_fences[current_frame]);
// ... just a standard draw-frame code with semaphores and fences
}
Я знаю, что каждый раз, когда я отправляю буфер команд вычислений, он выполняется так, как был записан. Это означает, что матрица обзора камеры всегда одинакова. Но мне это нужно для отсеивания вычислительных шейдеров! Как я могу обновить буфер команд вычислений? Должен ли я reset и record это в draw_frame? Хотя мне кажется, что это неправильно... Потому что я уже пробовал и сталкивался с проблемами синхронизации. Так есть ли какое-нибудь решение? Должен ли я использовать наборы дескрипторов вместо констант push? Мне бы не хотелось, поскольку у меня встроенный графический процессор, поэтому производительность имеет решающее значение.





Не могу поверить, что забыл о методе vk::Queue::waitIdle():
void draw_frame() {
GPU_.compute_queue_.waitIdle();
GPU_.compute_command_buffer_.reset();
record_compute_command_buffer();
vk::SubmitInfo compute_submit_info{};
compute_submit_info.commandBufferCount = 1;
compute_submit_info.pCommandBuffers = &GPU_.compute_command_buffer_;
GPU_.compute_queue_.submit(compute_submit_info);
//...
Ожидание простоя (либо устройства, либо очереди) является плохой практикой и может серьезно повлиять на производительность. Особенно если вы попытаетесь перекрыть вычисления и графику, ожидание устройства в режиме ожидания для каждого кадра полностью уничтожит любую возможность перекрытия работы, поскольку оно опустошит все (активные) очереди устройства.
Лучшим вариантом для вашего сценария (перестроение буферов команд для каждого кадра) будет использование ограждений для проверки завершения выполнения буфера команд. Вы отправляете команду с таким забором и перед следующей перезаписью проверяете, был ли сигнализирован этот забор перед повторным созданием этого командного буфера.
Использование push-констант для чего-то, что может меняться каждый кадр или, по крайней мере, каждые несколько кадров, вероятно, является плохой идеей. Вам лучше использовать постоянно отображаемый юниформ-буфер, имеющий память с DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT битами. Большинство современных графических процессоров имеют хотя бы часть этой памяти.
Даже если они этого не делают, вам, вероятно, все же лучше обновить универсальный буфер через промежуточный буфер с небольшим буфером команд передачи, выполняемым перед буфером отрисовки, а не перезаписывать весь буфер команд.
Если вы используете VMA для распределения, есть способ оптимизации для такого типа памяти: VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT. См. раздел расширенной загрузки данных на этой странице.
Push-константы более полезны, когда вы хотите выполнить несколько отрисовок, которые будут отличаться в некотором отношении и которыми можно управлять с помощью push-константы, но вы не хотите менять конвейер или что-либо перепривязывать.
Например, предположим, что у вас есть какая-то небольшая вещь, которую вы хотите изменить между вызовами, но это не меняется для каждой вершины ИЛИ для каждого экземпляра. Допустим, у вас есть сетки для шахматных фигур, и вы хотите нарисовать два их набора, белый и черный, но вы не хотите создавать отдельные буферы сетки с цветом в них, и по какой-то причине вы не хотите этого делать. поместите цвет в данные экземпляра. Push-константы были бы полезны.
Саша прав: единственный раз, когда вам хочется видеть vk::Queue::waitIdle или vk::Device::waitIdle, — это когда вы запускаете или закрываете приложение. Вы никогда не захотите этого в своем цикле рендеринга.
Я бы пошел дальше и сказал, что вам следует избегать сброса и перезаписи любого командного буфера, который в настоящее время используется:
В моем коде есть очередь удаления (я называю ее переработчиком, но на самом деле она ничего не перерабатывает), куда я помещаю любые ресурсы, которые мне больше не нужны, но которые могут использоваться командными буферами любых предыдущих кадров.
Например, когда мне нужно записать новый буфер команд, я могу поместить текущий в очередь на удаление и просто выделить новый. Когда я в конечном итоге отправляю свой кадр, я делаю это с помощью ограждения, созданного для этого кадра, и передаю ограждение в очередь на удаление, которая сообщает ему: «Когда это ограждение сигнализируется, все, что помещено в очередь удаления до этого, безопасно для удаления». удалить".
Это проще объяснить на примере кода...
предположим, что у вас есть что-то вроде этого
struct Frame {
vk::Fence submit_fence;
vk::CommandBuffer command_buffer;
void wait(const vk::Device& device) {
// Wait for fence is decorated with [[nodiscard]]
if (submit_fence) {
device.waitForFences(submit_fence, true, UINT64_MAX);
device.resetFences(submit_fence);
} else {
submit_fence = GPU_.logical_device_.createFence(vk::FenceCreateInfo{});
}
}
};
а затем в вашем классе графического процессора у вас было это...
constexpr size_t MAX_FRAMES_IN_FLIGHT = 2;
struct GPU {
...
std::array<Frame, MAX_FRAMES_IN_FLIGHT> compute_frames;
size_t current_compute_frame = 0;
};
Тогда вы могли бы переписать свою функцию draw_frame следующим образом:
void draw_frame() {
auto &frame = GPU_.compute_frames[GPU_.current_compute_frame];
frame.wait(GPU_.logical_device_);
GPU_.compute_queue_.submit(vk::SubmitInfo{
nullptr, nullptr, // no wait semapores
frame.command_buffer
}, frame.submit_fence);
GPU_.current_compute_frame = GPU_.current_compute_frame + 1 % MAX_FRAMES_IN_FLIGHT;
}
и измените начало record_compute_command_buffer(), чтобы оно выглядело так:
void record_compute_command_buffer() {
auto& frame = GPU_.compute_frames[GPU_.current_compute_frame];
// No need for simultaneous flag
frame.command_buffer.begin(vk::CommandBufferBeginInfo{});
...
Вам все равно нужно будет выяснить, какие командные буферы необходимо перезаписать, поэтому вы можете поместить флаг bool dirty в тип Frame, чтобы, когда что-то изменится, что означает, что все командные буферы недействительны, вы можно просто установить это для всех из них, а затем очистить при перезаписи каждого из них (записывая только один кадр на кадр).
Спасибо за подробный ответ! Но, как я уже упоминал ранее, я использую все возможности, которые может дать графический процессор. У меня Intel Integrated UHD 620, если я использую сопоставленные буферы или наборы дескрипторов, например. для матриц model-view-proj (которые необходимо обновлять в каждом кадре) я сталкиваюсь с проблемами производительности. 900 FPS с пуш-константами и 400-500 FPS при использовании чего-либо еще. Еще раз спасибо!! Я это запомню :)
Что ж, поскольку я не вижу вашего «всего остального», что вы пробовали, я не могу говорить о том, невозможно ли добиться более высокой производительности с помощью однородного буфера или нет. Существует множество причин, по которым ваш подход с константами push может работать быстрее, чем подходы с единым буфером, которые вы, возможно, пробовали, но это не обязательно означает, что подход с константами push всегда будет быстрее или даже самым быстрым. С графическим процессором, который вы используете, вся память будет локальной для устройства и видимой для хоста/когерентной для хоста. Есть несколько стратегий, которые вы можете использовать, особенно если вы используете более одного командного буфера.
Да, вы правы насчет свойств памяти графического процессора, я попробую!
Я видел, что
queue.waitIdleэквивалентно предоставлению действительного забора. Итак, какие проблемы с производительностью это может вызвать, если альтернативой является сам забор?