cmake_minimum_required(VERSION 3.28)
project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# clang-tidy reads the compile database. Costs nothing when unused.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

find_package(pybind11 CONFIG REQUIRED)

find_package(Vulkan REQUIRED COMPONENTS shaderc_combined)

include(FetchContent)

# glfw
# Only DOCS needs saying: EXAMPLES and TESTS already default to OFF when glfw is
# a subproject (they follow GLFW_STANDALONE), DOCS defaults ON regardless.
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
  glfw
  GIT_REPOSITORY https://github.com/glfw/glfw.git
  GIT_TAG 3.4
  GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(glfw)

#volk
FetchContent_Declare(
    volk
    GIT_REPOSITORY https://github.com/zeux/volk.git
    GIT_TAG 1.4.350
    GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(volk)

# PUBLIC, and on VOLK rather than on _core, because the platform macro changes
# the LAYOUT of VolkDeviceTable: every win32-only entry point is behind
# `#if defined(VK_USE_PLATFORM_WIN32_KHR)` in volk.h. Defining it for _core
# alone gave the two translation units two different structs — one definition
# rule broken, and every field after the guarded ones read at the wrong
# offset. Found in 0.25 by the first call that needed one of those entry
# points (vkAcquireFullScreenExclusiveModeEXT), which crashed.
#
# PUBLIC propagates to everything that links volk, so the definition cannot
# drift between them again.
if(WIN32)
    target_compile_definitions(volk PUBLIC VK_USE_PLATFORM_WIN32_KHR)
elseif(APPLE)
    # The same rule for the Metal surface entry point.
    target_compile_definitions(volk PUBLIC VK_USE_PLATFORM_METAL_EXT)
endif()

#vma
FetchContent_Declare(
    vma
    GIT_REPOSITORY https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
    GIT_TAG v3.4.0
    GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(vma)

#vk-bootstrap
FetchContent_Declare(
    vk-bootstrap
    GIT_REPOSITORY https://github.com/charles-lunarg/vk-bootstrap.git
    GIT_TAG v1.4.350
    GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(vk-bootstrap)
# Not redundant with the global CMAKE_POSITION_INDEPENDENT_CODE: vk-bootstrap
# overrides PIC from its own option (default BUILD_SHARED_LIBS, OFF here), so
# the global never reaches it. volk has no such override and needs nothing.
set_target_properties(vk-bootstrap PROPERTIES POSITION_INDEPENDENT_CODE ON)

#stb
# A commit, not a branch. stb publishes no tags, so `master` used to be the only
# spelling available -- and it made an upstream push able to break a build here
# with no local change, which is the one failure nobody can reproduce.
#
# NO GIT_SHALLOW here, unlike the four above, and the difference is the pin. A
# shallow clone fetches the tip of the default branch and nothing else, so
# checking out any other commit fails with "unable to read tree". While the pin
# WAS master's tip that never showed, which is why this stood for four releases
# and then broke on a day nothing here changed: stb pushed 2c980bb5 on
# 2026-08-02 and every shallow clone stopped containing 31c1ad37.
#
# The four tagged dependencies keep GIT_SHALLOW because CMake clones a tag by
# name, which the server resolves. Only a raw SHA needs the history. GitHub
# does serve `git fetch --depth 1 origin <sha>`, but CMake's shallow path does
# not ask that way -- it clones, then checks out. The full clone is 6 MB and one
# second, which is the whole price of never seeing this again.
FetchContent_Declare(
    stb
    GIT_REPOSITORY https://github.com/nothings/stb.git
    GIT_TAG 31c1ad37456438565541f4919958214b6e762fb4
)
FetchContent_MakeAvailable(stb)

# The binding layer is eight translation units, not one (0.20), and since 0.27
# the fat core headers are .hpp/.cpp pairs too. Header-only was an optimization
# for a consumer that does not exist -- nobody links bazalt as a C++ library, so
# every binding TU was paying to re-parse ~19k lines of implementation, and the
# include cycle it forced needed two pure-virtual interfaces with one
# implementation each just to let Context name its own members.
#
# The .hpp files are not listed here. That list used to exist for IDE display and
# drifted: HotReload.hpp, Device.hpp, SpirvReflect.hpp and ResourceTracker.hpp
# were all missing, and nothing noticed, which is what a list that serves no
# build purpose does.
add_library(_core MODULE
    src/main.cpp
    src/Buffer.cpp
    src/CommandBuffer.cpp
    src/Context.cpp
    src/DescriptorSet.cpp
    src/Graph.cpp
    src/Image.cpp
    src/Pipeline.cpp
    src/RenderTarget.cpp
    src/Renderer.cpp
    src/ShaderCompiler.cpp
    src/UploadManager.cpp
    src/window.cpp
    src/bindings/Enums.cpp
    src/bindings/Resources.cpp
    src/bindings/Pipelines.cpp
    src/bindings/Commands.cpp
    src/bindings/Graphs.cpp
    src/bindings/Windowing.cpp
    src/bindings/ContextBind.cpp
    src/bindings/Targets.cpp
    src/vma_impl.cpp
    src/stb_impl.cpp
)

target_link_libraries(_core PRIVATE
    pybind11::module
    glfw
    volk
    GPUOpen::VulkanMemoryAllocator
    vk-bootstrap::vk-bootstrap
    Vulkan::shaderc_combined
)
# On the target, not per file: every TU that includes vk_mem_alloc.h must agree,
# or the header's function-pointer table changes shape between TUs.
target_compile_definitions(_core PRIVATE VMA_STATIC_VULKAN_FUNCTIONS=0 VMA_DYNAMIC_VULKAN_FUNCTIONS=1)
# src/ on the path because the binding files sit in src/bindings/ and include the
# core headers by name.
target_include_directories(_core PRIVATE ${stb_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src)

# Parse pybind11, volk and the C++23 standard library once instead of eight times.
target_precompile_headers(_core PRIVATE src/bindings/Pch.hpp)

# target_precompile_headers applies to EVERY source of the target, and these two
# exist only to instantiate a third-party implementation. Forcing pybind11 into
# them is waste at best, and vma_impl.cpp is order-sensitive around
# VMA_IMPLEMENTATION.
set_source_files_properties(src/vma_impl.cpp src/stb_impl.cpp
    PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
set_target_properties(_core PROPERTIES PREFIX "${PYTHON_MODULE_PREFIX}" SUFFIX "${PYTHON_MODULE_EXTENSION}")

# VK_KHR_portability_subset lives in vulkan_beta.h, so its feature struct is
# invisible without this. Defined on every platform, not only Apple: the code that
# reads it is the same everywhere and only the runtime answer differs, and a struct
# that exists on one platform and not another is how you get a header that compiles
# in CI and not on a contributor's machine.
target_compile_definitions(_core PRIVATE VK_ENABLE_BETA_EXTENSIONS)

if(MSVC)
    # /bigobj: every pybind lambda is a template instantiation, and 0.14 pushed
    # the binding layer past COFF's 65,536-section limit. The 0.20 split into
    # eight files bought headroom, not immunity: any one of them can grow into it.
    #
    # /utf-8: the sources ARE UTF-8, but MSVC assumes the system code page for
    # both the source and the execution charset. Without this an em dash in an
    # error message reaches Python as a single 0x97 byte, which is not valid
    # UTF-8 — so the message arrives with a replacement character in it, or (when
    # it goes through std::format) empty. Eleven user-facing messages were
    # affected before 0.19. Telling the compiler the truth about the encoding is
    # the root fix; policing the prose for non-ASCII characters is not.
    target_compile_options(_core PRIVATE /Zc:__cplusplus /bigobj /utf-8)
else()
    target_compile_options(_core PRIVATE -Wno-interference-size)
endif()

# VK_USE_PLATFORM_WIN32_KHR / VK_USE_PLATFORM_METAL_EXT are defined on the volk
# target as PUBLIC (see above), so they reach this target too and both agree on
# what VolkDeviceTable looks like. They make volk load the platform surface entry
# points vk-bootstrap enables. Nothing else about macOS needs code -- vk-bootstrap
# adds VK_KHR_portability_enumeration and the instance flag by itself, and enables
# VK_KHR_portability_subset on the device.
if(APPLE)

    # libc++ annotates <format> and the floating-point std::to_chars it calls with
    # availability, so a deployment target below 13.3 fails to compile every
    # binding file. Say so in one sentence instead of leaving the reader to find
    # it in a hundred lines of "'format' is unavailable".
    if(CMAKE_OSX_DEPLOYMENT_TARGET AND CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 13.3)
        message(FATAL_ERROR
            "bazalt needs a macOS deployment target of 13.3 or later, because libc++ "
            "supplies std::format from that version. Set MACOSX_DEPLOYMENT_TARGET=14.0 "
            "(the version the published wheels use).")
    endif()
endif()

install(TARGETS _core DESTINATION bazalt)