Oh, and the documentation: It’s extensive but never tells me what I need to know.
Эта цитата взята из обсуждения CMake на Reddit, и она очень точно описывает большую часть моих проблем с CMake: когда я хочу что-то сделать документация не помогает с этим вообще — приходится искать решения в чужих проектах и статьях.
В этой статье будут разобраны проблемы, с которыми я столкнулся в процессе изучения Vulkan. Однако материал будет полезен и тем, кто настраивает любой другой проект.
Ошибки?
Если вы нашли ошибки, неточности или можете предложить лучшее решение, то добро пожаловать в комментарии — я исправлю или дополню статью.
Оглавление
Подключение библиотек через FetchContent
Много моих товарищей, которые только начинали изучение C++, тратили ужасно большое количество времени каждый раз, когда надо было установить библиотеку. Здесь я покажу только один из способов — CMake + FetchContent, однако есть и другие: vcpkg, скачивание вручную, github modules…
Стандартная последовательность действий:
-
Включить модуль FetchContent.
include(FetchContent)
-
Указать репозиторий, откуда будет загружаться библиотека, и дать этому контенту имя.
FetchContent_Declare( <name> GIT_REPOSITORY <url> GIT_TAG <tag> )
<tag>
может быть как хэшем коммита, так и тэгом.
-
Загрузить и интегрировать библиотеку (добавить цели) в проект.
FetchContent_MakeAvailable(<name>)
-
Связать библиотеку с нужной целью.
target_link_libraries(<your_target> <library_target>)
<library_target>
— это цель, которую создала библиотека. Её можно найти в примерах использования библиотеки, в README.md
(там вообще можно много найти) или в CMakeLists.txt
. В последнем случае надо искать строки вида add_library(<library_target> ...)
.
Пример
include(FetchContent) FetchContent_Declare( glfw GIT_REPOSITORY https://github.com/glfw/glfw.git GIT_TAG 3.4 ) FetchContent_MakeAvailable( glfw ) target_link_libraries(engine PRIVATE glfw)
Такая последовательность действует в большинстве случаев, но не во всех.
Библиотеки без CMakeLists.txt
Если у библиотеки нет CMakeLists.txt
, то можно её собрать самостоятельно. Нужно написать такой же код, который нужен для сборки вашего проекта с некоторыми нюансами.
Мы как и с другими библиотеками после использование FetchContent_MakeAvailable
создаём цель.
add_library(imgui_l STATIC)
Однако в нашем проекте мы обычно используем CMAKE_CURRENT_SOURCE_DIR
, чтобы включить директории с файлами, но при использовании FetchContent
, файлы библиотеки лежат где-то в папке сборки, а потому не хочется создавать файлы там.
Решить эту проблему помогает то, что FetchContent_MakeAvailable
, создаёт переменную вида <name>_SOURCE_DIR
для каждого имени из FetchContent_Declare
, которая содержит путь до скачанного репозитория. Поэтому мы можем использовать её для того, чтобы указать нужные файлы.
target_include_directories(imgui_l PRIVATE ${imgui_SOURCE_DIR}) target_sources(imgui_l PRIVATE ${imgui_SOURCE_DIR}/imgui.h ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_demo.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_widgets.cpp ${imgui_SOURCE_DIR}/backends/imgui_impl_vulkan.cpp )
А также некоторые библиотеки используют внутри другие библиотеки, поэтому их тоже нужно подключить.
target_link_libraries(imgui_l PRIVATE Vulkan::Vulkan)
И в конце как и остальные библиотеки нужно связать с целью.
target_link_libraries(engine PRIVATE imgui_l)
Однако иногда очень сложно написать самому такой скрипт, так как предполагалась сборка библиотеки с помощью других инструментов.
Скомпилированные библиотеки
Нередко разработчики предоставляют файлы .a
(используется в Unix-подобных системах (например, Linux, macOS) и компиляторами, такими как GCC ) или .lib
(используется в Windows и компиляторами, такими как MSVC ) — статические библиотеки.
Их обычно скачивают и кладут в отдельную папку lib
или external
в корне проекта, однако помимо самих файлов библиотеки ещё нужно положить куда-то заголовки этой библиотеки (обычно папка include
).
Соответственно в самом CMake нужно сделать:
target_link_libraries(engine PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib/lib-mingw-w64/libglfw3.a) # Or target_link_libraries(engine PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib/lib-vc2022/glfw3.lib) target_include_directories(engine PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
Полный пример
Здесь используется способ загрузки, описанный в Ускорение загрузки библиотек.
При использовании иных операционных систем или компиляторов нужно заменить ссылку на архив и путь до файла библиотеки.
FetchContent_Declare( glfw URL https://github.com/glfw/glfw/releases/download/3.4/glfw-3.4.bin.WIN64.zip ) FetchContent_MakeAvailable( glfw ) target_link_libraries(engine PRIVATE ${glfw_SOURCE_DIR}/lib-mingw-w64/libglfw3.a) target_include_directories(engine PRIVATE ${glfw_SOURCE_DIR}/include)
Header-only библиотеки
Ещё один возможный вариант — репозиторий только с заголовками. Однако ничего сложного тут нет: единственное, что нужно — сделать директорию с ними доступной в проекте после использования FetchContent_MakeAvailable
.
target_include_directories(engine PRIVATE ${stb_SOURCE_DIR})
Ускорение загрузки библиотек
Так как описанный изначально способ скачивания библиотек скачивает не только нужный вам коммит, а всю историю репозитория, из-за чего первый раз проект CMake может очень долго загружаться. Посмотреть, что происходит при загрузке библиотеки сначала нужно отключить FETCHCONTENT_QUIET
(по умолчанию стоит ON
), что перестанет подавлять вывод информации. А после уже в FetchContent_Declare
указать, что мы хотим получать информацию о прогрессе.
set(FETCHCONTENT_QUIET OFF) FetchContent_Declare( json GIT_REPOSITORY https://github.com/nlohmann/json GIT_TAG v3.11.3 GIT_PROGRESS ON )
Receiving objects: 100% (44890/44890), 193.92 MiB | 1.32 MiB/s, done.
Понятное дело, что нам не нужны все 200 мегабайт, а потому нам хочется скачивать только конкретный коммит. В документации можно найти GIT_SHALLOW
, который должен скачивать только нужный коммит, но по факту он скачивает намного больше, а потому не очень помогает.
Receiving objects: 100% (8981/8981), 161.64 MiB | 3.79 MiB/s, done.
Поэтому есть другое решение: скачивать архив по конкретной ссылке.
FetchContent_Declare( json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz )
Чтобы найти нужную ссылку, мы заходим на страницу репозиторийя, в раздел релизов и копируем ссылку на файл с исходниками. Так мы скачаем только нужный коммит. Остальная часть алгоритма будет прежней (FetchContent_MakeAvailable
и target_link_libraries()
).
Подключение Vulkan
Так как Vulkan — это полноценный SDK (набор инструментов разработки), то он содержит в себе библиотеки, заголовки, документацию, примеры и инструменты. Поэтому его приходиться устанавливать отдельно как программу. Стандартная последовательность действий такова:
-
Скачайте Vulkan SDK с официального сайта LunarG Vulkan SDK.
-
Установить его.
-
Найти и подключить в CMake.
find_package(Vulkan REQUIRED) target_link_libraries(engine PRIVATE Vulkan::Vulkan)
Однако CMake может не найти этот пакет, так как что-то произошло с переменной окружения VULKAN_SDK
, которая указывает путь до установленного Vulkan. Эта переменная автоматически должна быть создана при установке, поэтому если она отсутствует, то её надо создать.
Проблемы с CLion
Лично я столкнулся с немного иной проблемой: в системных переменных окружения VULKAN_SDK
была, но CLion её не подгрузил и в Settings -> Build, Execution, Deployment -> CMake в используемом профиле Environment её не было (надо нажать на кнопку странички, чтобы просмотреть переменные). Позже CLion её подгрузит, но на время можно самостоятельно добавить её.
VULKAN_SDK=<path_to_vulkan>
Настройка и компиляция исходников
Автоматическое добавление исходников к цели
Часто в проекте все файлы должны быть скомпилированы, поэтому нет смысла в перечислении их вручную. И чтобы упростить себе задачу и не заниматься этим, можно использовать команду file
вместе с GLOB_RECURSE
.
file(GLOB_RECURSE CPP_SOURCE_FILES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/*.cpp" )
GLOB_RECURSE
собирает в список все файлы, соответствующие предоставленному выражению: в нашем случае все .cpp
файлы. Стоит отметить, что GLOB_RECURSE
рекурсивно проверяет все директории, в то время как просто GLOB
этого не делает.
Ещё одна важная деталь — это CONFIGURE_DEPENDS
. Этот параметр пересобирает проект CMake, если значение переменной (CPP_SOURCE_FILES
) должно измениться.
После записи всех файлов в переменную, нужно как обычно добавить исходники к цели.
target_include_directories(engine PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
Добавление define во все исходники
Такие библиотеки как GLFW и GLM требуют перед каждым включением заголовка прописывать директиву define
для настройки каких-либо параметров, которые по факту едины для всего проекта.
#define GLFW_INCLUDE_VULKAN #include <GLFW/glfw3.h> #define GLM_FORCE_RADIANS #define GLM_FORCE_DEPTH_ZERO_TO_ONE #include <glm/glm.hpp>
Часто для решение этой проблемы создают дополнительный заголовочный файл с таким кодом и включают везде именно его. Однако в CMake существует возможность задать эти определения для цели.
target_compile_definitions(engine PRIVATE GLM_FORCE_DEPTH_ZERO_TO_ONE GLM_FORCE_RADIANS GLFW_INCLUDE_VULKAN )
Предварительная компиляция заголовков
Хороший способ сократить время компиляции — это выделить все заголовки, которые практически не меняются (часто берут именно из библиотек) и часто используются, и сделать для них предкомпиляцию.
Суть в том, что из-за включения заголовка в множество .cpp
файлов, при компиляции этих файлов мы также раз за разом компилируем один и тот же заголовок. Поэтому можно сказать компилятору, какие файлы можно предкомпилировать и не тратить на них время во время компиляции.
target_precompile_headers(engine PRIVATE <optional> <memory> <string> <vector> <unordered_map> <glm/mat4x4.hpp> <glm/vec4.hpp> <vulkan/vulkan.h> )
На просторах интернета нашёл вот такой небольшой список, подходящий моему проекту.
Компиляция шейдеров
Чтобы всё нужное в проекте собиралось по нажатию одной кнопки, не хватает сделать компиляцию шейдеров также автоматической. Аналогично добавлению исходников к цели можно собрать все файлы шейдеров и уже над ними производить манипуляции.
file(GLOB_RECURSE GLSL_SOURCE_FILES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/shaders/*.frag" "${PROJECT_SOURCE_DIR}/shaders/*.vert" "${PROJECT_SOURCE_DIR}/shaders/*.comp" )
Имея список мы можем проитерироваться по нему, скомпилировав каждый файл по отдельности.
foreach (GLSL IN LISTS GLSL_SOURCE_FILES) # ... endforeach (GLSL)
Так как мы хотим сохранить структуру директорий и названия в папке с скомпилированными шейдерами, то просто возьмём относительный путь от папки с шейдерами до файла и добавим расширение .spv
для файла.
file(RELATIVE_PATH FILE_NAME "${PROJECT_SOURCE_DIR}/shaders/" "${GLSL}") set(SPIRV "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/compiled_shaders/${FILE_NAME}.spv")
Далее нужно определить команду, которая создаст наш файл.
add_custom_command( OUTPUT ${SPIRV} COMMAND Vulkan::glslc ${GLSL} -o ${SPIRV} DEPENDS ${GLSL})
DEPENDS
— файл, при изменении которого будет исполняться команда.
COMMAND
— сама команда, которую мы фактически можем вставить в консоль с параметрами, использующимися при запуске этой команды. В данном случае CMake сам подставит вместо Vulkan::glslc
путь к исполняемому файлу.
glslc
— это компонент (программа), который был найден в процессе выполнения команды find_package(Vulkan REQUIRED)
, а конкретнее инструмент компиляции и оптимизации шейдеров. Однако этот инструмент — только обёртка для glslangValidator
(компилятор) и spirv-opt
(оптимизатор), которые также можно использовать.
OUTPUT
— файл, который будет получен в результате выполнения команды. Этот параметр указывается, чтобы CMake автоматически мог построить зависимости между командой и целью из той же области видимости. Сама по себе команда не будет выполняться, так как не прикреплена к цели, поэтому важно создать зависимость какой-либо цели от сгенерированных файлов. Для этого мы соберём их в список и создадим цель, зависящую от этих файлов (параметр DEPENDS
) и которая будет частью стандартной сборки (параметр ALL
).
foreach (GLSL IN LISTS GLSL_SOURCE_FILES) file(RELATIVE_PATH FILE_NAME "${PROJECT_SOURCE_DIR}/shaders/" "${GLSL}") set(SPIRV "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/compiled_shaders/${FILE_NAME}.spv") add_custom_command( OUTPUT ${SPIRV} COMMAND Vulkan::glslc ${GLSL} -o ${SPIRV} DEPENDS ${GLSL}) list(APPEND SPIRV_BINARY_FILES ${SPIRV}) endforeach (GLSL) add_custom_target( ShadersTarget ALL DEPENDS ${SPIRV_BINARY_FILES} )
Заключение
Я потратил довольно много времени на настройку, а не на программирование, что меня немного удручает, однако в будущем с этими знаниями я смогу намного быстрее приступить к непосредственной разработке. А потому в дополнение хочу рассказать ещё об одной вещи, на которую я потратил очень много времени.
Многие слышали фразу «исключения очень медленные». Но также многие (как я раньше или мои товарищи) неправильно её понимают: нам кажется, что простое наличие исключений в коде делает его значительно медленней (ведь там генерируется какая-то магия для их обработки), однако сейчас используется модель zero-cost exceptions, которая позволяет избавиться от накладных затрат в коде, который не выбрасывает исключения. Из этого следует, что пытаться отключить полностью их с помощью флагов компилятора практически бессмысленно (-fno-exceptions
). Однако кинуть исключение всё равно очень дорого, а значит делать это стоит только в исключительных ситуациях (а-ля не может инициализироваться какая-то критически важная библиотека), в остальных ситуациях нужно обходиться кодами возврата или Result Type.
Надеюсь кому-то эта статья поможет. Спасибо, что дочитали.
ссылка на оригинал статьи https://habr.com/ru/articles/873798/
Добавить комментарий