Finding Vulkan SDK
Vulkan provides a modern, low-overhead graphics and compute API. CMake ships a first-class FindVulkan module that discovers the Vulkan SDK — provided by LunarG on Windows/Linux or bundled with MoltenVK on macOS. The module locates headers, the loader library, and optional tools like glslc and glslangValidator.
# CMakeLists.txt — Basic Vulkan project
cmake_minimum_required(VERSION 3.20)
project(VulkanApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find Vulkan SDK — uses VULKAN_SDK environment variable
find_package(Vulkan REQUIRED)
add_executable(vulkan_app
src/main.cpp
src/renderer.cpp
src/pipeline.cpp
)
target_link_libraries(vulkan_app PRIVATE Vulkan::Vulkan)
# Print discovered information
message(STATUS "Vulkan found: ${Vulkan_FOUND}")
message(STATUS "Vulkan version: ${Vulkan_VERSION}")
message(STATUS "Vulkan include: ${Vulkan_INCLUDE_DIRS}")
message(STATUS "Vulkan glslc: ${Vulkan_GLSLC_EXECUTABLE}")
VULKAN_SDK environment variable is the primary discovery mechanism. On Windows, the LunarG installer sets this automatically. On Linux, source the SDK's setup-env.sh script or set it manually in your CI environment.
CMake's FindVulkan module provides several imported targets beyond the core loader:
# Available Vulkan targets (CMake 3.24+)
find_package(Vulkan REQUIRED COMPONENTS
glslc # SPIR-V shader compiler
glslangValidator # Reference compiler
shaderc_combined # Shaderc library for runtime compilation
)
target_link_libraries(vulkan_app PRIVATE
Vulkan::Vulkan # Core loader + headers
Vulkan::shaderc_combined # Runtime shader compilation
)
# Check for optional components
if(TARGET Vulkan::glslc)
message(STATUS "glslc found at: ${Vulkan_GLSLC_EXECUTABLE}")
endif()
Finding OpenGL
OpenGL discovery in CMake uses the FindOpenGL module. Since CMake 3.10, this module provides modern imported targets and distinguishes between the legacy GL library and the newer GLVND (GL Vendor Neutral Dispatch) implementation on Linux.
# CMakeLists.txt — OpenGL project setup
cmake_minimum_required(VERSION 3.20)
project(OpenGLApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Prefer GLVND on Linux (modern approach)
set(OpenGL_GL_PREFERENCE GLVND)
find_package(OpenGL REQUIRED)
add_executable(gl_app src/main.cpp src/shader.cpp)
# Use the modern target — includes both GL and GLX/EGL as needed
target_link_libraries(gl_app PRIVATE OpenGL::GL)
# On Linux, you may also need:
# OpenGL::GLX — for GLX context creation
# OpenGL::EGL — for EGL context (Wayland, headless)
# OpenGL::OpenGL — GLVND libOpenGL (dispatch only)
OpenGL_GL_PREFERENCE to LEGACY unless you must support very old drivers. Legacy mode links against libGL.so directly, which can cause conflicts with NVIDIA's proprietary driver on Linux systems using GLVND.
GLFW/GLEW/glad Integration
Graphics applications need a windowing library (GLFW) and an extension loader (GLEW or glad). GLFW provides cross-platform window creation and input handling; glad/GLEW load OpenGL function pointers at runtime.
# CMakeLists.txt — GLFW + glad via FetchContent
cmake_minimum_required(VERSION 3.20)
project(GraphicsDemo LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
# GLFW — windowing library
FetchContent_Declare(glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.4
)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(glfw)
# glad — OpenGL loader (generated from glad.dav1d.de)
add_library(glad STATIC
third_party/glad/src/glad.c
)
target_include_directories(glad PUBLIC
third_party/glad/include
)
find_package(OpenGL REQUIRED)
add_executable(demo src/main.cpp)
target_link_libraries(demo PRIVATE
glfw
glad
OpenGL::GL
)
For GLEW as an alternative extension loader with system installation:
# Using system-installed GLEW
find_package(GLEW REQUIRED)
add_executable(glew_demo src/main.cpp)
target_link_libraries(glew_demo PRIVATE
glfw
GLEW::GLEW
OpenGL::GL
)
GLSL Shader Compilation at Build Time
Modern Vulkan applications compile GLSL shaders to SPIR-V bytecode before runtime. CMake's add_custom_command integrates the glslc compiler directly into the build graph, ensuring shaders recompile when source changes.
# CMakeLists.txt — GLSL to SPIR-V compilation
find_package(Vulkan REQUIRED COMPONENTS glslc)
# Function to compile a single shader
function(compile_shader TARGET SHADER_SOURCE)
get_filename_component(SHADER_NAME ${SHADER_SOURCE} NAME)
set(SPIRV_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/shaders/${SHADER_NAME}.spv")
add_custom_command(
OUTPUT ${SPIRV_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/shaders"
COMMAND Vulkan::glslc ${SHADER_SOURCE} -o ${SPIRV_OUTPUT}
DEPENDS ${SHADER_SOURCE}
COMMENT "Compiling shader: ${SHADER_NAME}"
VERBATIM
)
target_sources(${TARGET} PRIVATE ${SPIRV_OUTPUT})
endfunction()
# Usage
add_executable(vulkan_app src/main.cpp)
target_link_libraries(vulkan_app PRIVATE Vulkan::Vulkan)
compile_shader(vulkan_app ${CMAKE_CURRENT_SOURCE_DIR}/shaders/triangle.vert)
compile_shader(vulkan_app ${CMAKE_CURRENT_SOURCE_DIR}/shaders/triangle.frag)
compile_shader(vulkan_app ${CMAKE_CURRENT_SOURCE_DIR}/shaders/compute.comp)
For projects with many shaders, use a glob-based approach with a custom target:
# Batch shader compilation with optimization flags
file(GLOB SHADER_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.vert"
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.frag"
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.comp"
"${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.geom"
)
set(SPIRV_OUTPUTS "")
foreach(SHADER ${SHADER_SOURCES})
get_filename_component(SHADER_NAME ${SHADER} NAME)
set(SPIRV "${CMAKE_CURRENT_BINARY_DIR}/shaders/${SHADER_NAME}.spv")
add_custom_command(
OUTPUT ${SPIRV}
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/shaders"
COMMAND Vulkan::glslc
-O # Optimize
--target-env=vulkan1.3 # Target Vulkan version
-Werror # Treat warnings as errors
${SHADER} -o ${SPIRV}
DEPENDS ${SHADER}
COMMENT "Compiling SPIR-V: ${SHADER_NAME}"
VERBATIM
)
list(APPEND SPIRV_OUTPUTS ${SPIRV})
endforeach()
add_custom_target(compile_shaders DEPENDS ${SPIRV_OUTPUTS})
add_dependencies(vulkan_app compile_shaders)
add_custom_command with explicit DEPENDS ensures shaders only recompile when their source changes. The VERBATIM flag prevents shell interpretation issues across platforms.
Vulkan Validation Layers
Vulkan validation layers intercept API calls to catch errors, performance issues, and best-practice violations. They are essential during development but should be disabled in release builds for performance.
# CMakeLists.txt — Validation layer configuration
option(ENABLE_VULKAN_VALIDATION "Enable Vulkan validation layers" ON)
if(ENABLE_VULKAN_VALIDATION)
target_compile_definitions(vulkan_app PRIVATE
ENABLE_VALIDATION_LAYERS=1
)
message(STATUS "Vulkan validation layers: ENABLED")
else()
target_compile_definitions(vulkan_app PRIVATE
ENABLE_VALIDATION_LAYERS=0
)
message(STATUS "Vulkan validation layers: DISABLED")
endif()
# Only enable validation in Debug builds by default
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(ENABLE_VULKAN_VALIDATION ON CACHE BOOL "" FORCE)
endif()
// src/validation.cpp — Runtime layer activation
#include <vulkan/vulkan.h>
#include <vector>
#include <iostream>
std::vector<const char*> getValidationLayers() {
std::vector<const char*> layers;
#if ENABLE_VALIDATION_LAYERS
layers.push_back("VK_LAYER_KHRONOS_validation");
// Verify layer availability
uint32_t layerCount = 0;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> available(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, available.data());
for (const char* name : layers) {
bool found = false;
for (const auto& prop : available) {
if (strcmp(name, prop.layerName) == 0) {
found = true;
break;
}
}
if (!found) {
std::cerr << "Validation layer not found: " << name << "\n";
}
}
#endif
return layers;
}
OpenGL Version Selection
Different platforms support different maximum OpenGL versions. CMake can detect capabilities and configure your application to request the appropriate context version via GLFW hints.
# CMakeLists.txt — Platform-aware OpenGL version
if(APPLE)
# macOS supports up to OpenGL 4.1 (deprecated)
set(GL_VERSION_MAJOR 4)
set(GL_VERSION_MINOR 1)
set(GL_PROFILE "CORE")
elseif(WIN32)
set(GL_VERSION_MAJOR 4)
set(GL_VERSION_MINOR 6)
set(GL_PROFILE "CORE")
else()
# Linux — mesa or proprietary drivers
set(GL_VERSION_MAJOR 4)
set(GL_VERSION_MINOR 6)
set(GL_PROFILE "CORE")
endif()
target_compile_definitions(demo PRIVATE
GL_VERSION_MAJOR=${GL_VERSION_MAJOR}
GL_VERSION_MINOR=${GL_VERSION_MINOR}
GL_PROFILE_CORE=1
)
// src/context.cpp — GLFW context creation with version hints
#include <GLFW/glfw3.h>
#include <iostream>
GLFWwindow* createWindow(int width, int height, const char* title) {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, GL_VERSION_MAJOR);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, GL_VERSION_MINOR);
#if GL_PROFILE_CORE
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#endif
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
GLFWwindow* window = glfwCreateWindow(width, height, title, nullptr, nullptr);
if (!window) {
std::cerr << "Failed to create GLFW window\n";
glfwTerminate();
return nullptr;
}
glfwMakeContextCurrent(window);
return window;
}
Metal Fallback on macOS
Apple deprecated OpenGL on macOS and does not provide a native Vulkan driver. MoltenVK translates Vulkan calls to Metal. CMake can detect the platform and configure the appropriate graphics backend.
# CMakeLists.txt — Cross-platform graphics backend
cmake_minimum_required(VERSION 3.20)
project(CrossPlatformRenderer LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
if(APPLE)
# Use MoltenVK for Vulkan-on-Metal translation
find_package(Vulkan REQUIRED)
# MoltenVK requires linking to Metal and related frameworks
find_library(METAL_FRAMEWORK Metal REQUIRED)
find_library(QUARTZCORE_FRAMEWORK QuartzCore REQUIRED)
find_library(IOSURFACE_FRAMEWORK IOSurface REQUIRED)
target_link_libraries(renderer PRIVATE
Vulkan::Vulkan
${METAL_FRAMEWORK}
${QUARTZCORE_FRAMEWORK}
${IOSURFACE_FRAMEWORK}
)
target_compile_definitions(renderer PRIVATE
USE_MOLTENVK=1
VK_USE_PLATFORM_MACOS_MVK=1
)
else()
find_package(Vulkan REQUIRED)
target_link_libraries(renderer PRIVATE Vulkan::Vulkan)
if(WIN32)
target_compile_definitions(renderer PRIVATE VK_USE_PLATFORM_WIN32_KHR=1)
else()
target_compile_definitions(renderer PRIVATE VK_USE_PLATFORM_XCB_KHR=1)
endif()
endif()
VK_KHR_portability_subset extension and enable VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR when creating the Vulkan instance on macOS to properly enumerate MoltenVK devices.
Rendering Pipeline Setup
A complete CMake setup for a production rendering pipeline combines all the elements above — Vulkan/OpenGL discovery, windowing, shader compilation, and platform abstraction:
# CMakeLists.txt — Complete rendering pipeline project
cmake_minimum_required(VERSION 3.20)
project(RenderEngine VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# Options
option(USE_VULKAN "Use Vulkan backend" ON)
option(USE_OPENGL "Use OpenGL backend (fallback)" OFF)
option(BUILD_EXAMPLES "Build example applications" ON)
# Windowing — GLFW
FetchContent_Declare(glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.4
)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(glfw)
# Math library — GLM
FetchContent_Declare(glm
GIT_REPOSITORY https://github.com/g-truc/glm.git
GIT_TAG 1.0.1
)
FetchContent_MakeAvailable(glm)
# Image loading — stb
FetchContent_Declare(stb
GIT_REPOSITORY https://github.com/nothings/stb.git
GIT_TAG master
)
FetchContent_MakeAvailable(stb)
# Core renderer library
add_library(renderer STATIC
src/renderer/context.cpp
src/renderer/pipeline.cpp
src/renderer/buffer.cpp
src/renderer/texture.cpp
)
target_include_directories(renderer PUBLIC include)
target_link_libraries(renderer PUBLIC glfw glm::glm)
if(USE_VULKAN)
find_package(Vulkan REQUIRED COMPONENTS glslc)
target_link_libraries(renderer PUBLIC Vulkan::Vulkan)
target_compile_definitions(renderer PUBLIC BACKEND_VULKAN=1)
elseif(USE_OPENGL)
find_package(OpenGL REQUIRED)
target_link_libraries(renderer PUBLIC OpenGL::GL)
target_compile_definitions(renderer PUBLIC BACKEND_OPENGL=1)
endif()
# Shader compilation (Vulkan only)
if(USE_VULKAN AND TARGET Vulkan::glslc)
file(GLOB_RECURSE SHADER_FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/*")
foreach(SHADER ${SHADER_FILES})
get_filename_component(SHADER_NAME ${SHADER} NAME)
set(SPIRV "${CMAKE_CURRENT_BINARY_DIR}/shaders/${SHADER_NAME}.spv")
add_custom_command(
OUTPUT ${SPIRV}
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/shaders"
COMMAND Vulkan::glslc --target-env=vulkan1.3 -O ${SHADER} -o ${SPIRV}
DEPENDS ${SHADER}
VERBATIM
)
list(APPEND SPIRV_BINARIES ${SPIRV})
endforeach()
add_custom_target(shaders ALL DEPENDS ${SPIRV_BINARIES})
add_dependencies(renderer shaders)
endif()
if(BUILD_EXAMPLES)
add_executable(triangle examples/triangle.cpp)
target_link_libraries(triangle PRIVATE renderer)
endif()
BACKEND_VULKAN or BACKEND_OPENGL compile definition lets application code use #if BACKEND_VULKAN preprocessor guards — keeping the build system in control of backend selection rather than scattering platform checks through source code.