cmake_minimum_required(VERSION 3.20)
project(SiliconScavenger
    VERSION 0.6.0
    DESCRIPTION "Zero-copy heterogeneous inference scheduler for CPU + Intel iGPU"
    LANGUAGES CXX
)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# MinGW builds otherwise produce a .pyd that dynamically depends on
# libstdc++-6.dll / libgcc_s_seh-1.dll / libwinpthread-1.dll. Python 3.8+'s
# restricted DLL search path on Windows does not search PATH for extension
# module dependencies, so those DLLs fail to load at `import` time even when
# they're perfectly findable on PATH for everything else. Statically linking
# the MinGW runtime avoids shipping/depending on those DLLs at all.
if(MINGW)
    add_link_options(-static-libgcc -static-libstdc++ -static)
endif()

# Keep build outputs in one place regardless of generator, so downstream tooling
# (pybind11 module discovery, CI artifact upload) has a stable path to look in.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

# Top-level switch for the C++ unit test suite (Catch2). On by default; CI
# always builds with this ON.
option(SCAVENGER_BUILD_TESTS "Build the C++ unit test suite (Catch2)" ON)

# Off by default so a plain `cmake -S . -B build` (no Python present) keeps
# building just the dependency-free core, exactly as validated in Phase 0
# commit 2. `pip install .` (see pyproject.toml) turns this ON explicitly.
option(SCAVENGER_BUILD_PYTHON_BINDINGS "Build the pybind11 extension module" OFF)

# Off by default: core/tools/ contains the Phase 1 hardware validation
# executables (OpenVINO GPU smoke test, llama.cpp Vulkan smoke test), which
# need the OpenVINO C++ SDK and Vulkan SDK installed - not present on a
# plain CI runner. See docs/DEVELOPMENT.md for what to install before
# turning this ON, and docs/hardware/ for validation results on real
# hardware.
option(SCAVENGER_BUILD_HARDWARE_TOOLS "Build core/tools/ hardware validation executables" OFF)

# Off by default: core/src/backends/ contains the real OpenVINO and
# llama.cpp-Vulkan IBackend implementations that the Phase 2 scheduler
# drives. Needs the same OpenVINO C++ SDK + Vulkan SDK as
# SCAVENGER_BUILD_HARDWARE_TOOLS above - kept as a separate flag because
# these are library code linked into scavenger_core, not standalone
# validation executables, but the two are commonly turned on together.
# Declared here (not in core/CMakeLists.txt) because it gates the
# third_party/llama.cpp add_subdirectory() below, which must happen before
# add_subdirectory(core).
option(SCAVENGER_BUILD_BACKENDS "Build core/src/backends/ (OpenVINO + llama.cpp-Vulkan IBackend impls)" OFF)

# Off by default: OpenVinoImageBackend (core/src/backends/
# openvino_image_backend.cpp) needs the separate OpenVINO GenAI C++ SDK -
# a much larger download than the plain OpenVINO runtime
# SCAVENGER_BUILD_BACKENDS above needs, and not required for text
# generation or embeddings at all. Kept as its own flag (rather than
# folded into SCAVENGER_BUILD_BACKENDS) so a text-gen-only build doesn't
# have to install it. See docs/DEVELOPMENT.md's "Image generation
# (OpenVINO GenAI)" section for where to get the SDK.
option(SCAVENGER_BUILD_IMAGE_GEN "Build core/src/backends/openvino_image_backend.cpp (needs OpenVINO GenAI SDK)" OFF)

# Off by default: Phase 5's Tier 2 spike (core/tools/usm_spike.cpp) - a
# standalone, throwaway measurement of OpenVINO's GPU Remote Tensor / USM
# API against the Tier 1 plain-ov::Tensor baseline. Needs the same OpenVINO
# C++ SDK as SCAVENGER_BUILD_BACKENDS plus the Khronos OpenCL C/C++ headers
# (fetched via FetchContent below - not bundled with the OpenVINO SDK
# itself) to satisfy ov/runtime/intel_gpu/ocl/ocl.hpp's <CL/cl2.hpp>
# include. Kept as its own flag: this is a one-shot research spike, not
# something the default backend build should pull in.
option(SCAVENGER_BUILD_TIER2_SPIKE "Build core/tools/usm_spike.cpp (needs OpenVINO C++ SDK + fetches OpenCL headers)" OFF)

# Off by default: the actual Tier 2 (GPU Remote Tensor / USM) code path in
# OpenVinoBackend (core/src/backends/openvino_backend.cpp), gated on top of
# SCAVENGER_BUILD_BACKENDS. Per PHASE_PLAN.md Phase 5's exit criteria
# ("Tier 1 remains the shipped default"), this is a *build-time* opt-in on
# top of OpenVinoBackend's own *runtime* `enable_usm` constructor flag
# (default false) - a machine without the fetched OpenCL headers/import
# library can still build scavenger_core with SCAVENGER_BUILD_BACKENDS=ON
# and get the unchanged Tier 1 path, no OpenCL dependency at all, by simply
# leaving this OFF. See docs/tier2_spike_log.md for why OpenCL headers/lib
# need fetching in the first place (not bundled with the OpenVINO SDK).
option(SCAVENGER_ENABLE_TIER2_USM "Build OpenVinoBackend's Tier 2 USM code path (needs OpenVINO C++ SDK + fetches OpenCL headers)" OFF)

# Off by default: docs/FUTURE_DIRECTION.md Phase O0's "wire real backends to
# the Python API" work. bindings/pybind_module.cpp always links
# scavenger_core (see bindings/CMakeLists.txt) - if SCAVENGER_BUILD_BACKENDS
# is also ON, the real OpenVinoBackend/LlamaVulkanBackend/CompositeBackend
# classes are *already* present in that library, compiled or not. This flag
# controls something different: whether pybind_module.cpp's own code is
# compiled to actually *construct* them for a caller that asks for real
# backends (PyScheduler(llama_model_path=...)), as opposed to always
# wiring scavenger::testing::MockBackend regardless of what's asked for.
# Kept OFF by default and as its own flag (not folded into
# SCAVENGER_BUILD_BACKENDS) so `pip install .` - most users, no OpenVINO/
# llama.cpp SDK - keeps working exactly as before: MockBackend-only,
# dependency-free. Requires both SCAVENGER_BUILD_BACKENDS (the real
# backend implementations themselves) and SCAVENGER_BUILD_PYTHON_BINDINGS
# (something to wire them into) - checked explicitly below with a clear
# error rather than silently compiling a Python extension that can't
# actually do what its own flag name promises.
option(SCAVENGER_BUILD_PYTHON_REAL
    "Wire bindings/pybind_module.cpp's PyScheduler to real OpenVINO/llama.cpp-Vulkan backends, not just MockBackend (needs SCAVENGER_BUILD_BACKENDS + SCAVENGER_BUILD_PYTHON_BINDINGS)"
    OFF)
if(SCAVENGER_BUILD_PYTHON_REAL AND NOT (SCAVENGER_BUILD_BACKENDS AND SCAVENGER_BUILD_PYTHON_BINDINGS))
    message(FATAL_ERROR
        "SCAVENGER_BUILD_PYTHON_REAL=ON requires both SCAVENGER_BUILD_BACKENDS=ON "
        "(the real IBackend implementations to wire in) and "
        "SCAVENGER_BUILD_PYTHON_BINDINGS=ON (the pybind11 module to wire them into). "
        "See docs/DEVELOPMENT.md's Phase O0 build section.")
endif()

# Off by default: docs/FUTURE_DIRECTION.md Phase M1's LlamaCudaBackend
# (core/src/backends/llama_cuda_backend.cpp) - real llama.cpp-CUDA text
# generation for the local.nvidia.dgpu0 scheduler slot. Needs the NVIDIA
# CUDA Toolkit (nvcc + cuBLAS; `nvcc --version` must work) installed
# separately - not bundled, and not needed at all for the default
# CPU+iGPU build. Turns on llama.cpp's own GGML_CUDA option (must be set
# before add_subdirectory(third_party/llama.cpp) below, same constraint as
# GGML_VULKAN above) and SCAVENGER_BUILD_CUDA is also threaded through as a
# compile definition on scavenger_core so llama_cuda_backend.hpp's
# implementation is actually compiled (see that header's build-guard
# comment - without this, the .cpp is silently excluded and the class is
# declared but never linkable, by design, so a caller can't accidentally
# instantiate a CUDA backend from a build that doesn't have one).
option(SCAVENGER_BUILD_CUDA
    "Build LlamaCudaBackend for the NVIDIA dGPU slot (needs the CUDA Toolkit; requires SCAVENGER_BUILD_BACKENDS)"
    OFF)
if(SCAVENGER_BUILD_CUDA AND NOT SCAVENGER_BUILD_BACKENDS)
    message(FATAL_ERROR
        "SCAVENGER_BUILD_CUDA=ON requires SCAVENGER_BUILD_BACKENDS=ON (llama.cpp is only "
        "vendored/built at all when that flag is set). See docs/DEVELOPMENT.md's Phase M1 "
        "build section.")
endif()

# Shared by SCAVENGER_BUILD_TIER2_SPIKE (core/tools/usm_spike.cpp) and
# SCAVENGER_ENABLE_TIER2_USM (core/src/backends/openvino_usm_pool.cpp) -
# declared once, here, at the top level so both consumers resolve to the
# same fetched OpenCL::Headers/OpenCL::HeadersCpp/OpenCL::OpenCL targets
# rather than each declaring (and potentially fetching twice into
# different build-tree locations) the same three repos independently.
if(SCAVENGER_BUILD_TIER2_SPIKE OR SCAVENGER_ENABLE_TIER2_USM)
    include(FetchContent)
    FetchContent_Declare(
        opencl_headers
        GIT_REPOSITORY https://github.com/KhronosGroup/OpenCL-Headers.git
        GIT_TAG v2024.10.24
    )
    FetchContent_Declare(
        opencl_icd_loader
        GIT_REPOSITORY https://github.com/KhronosGroup/OpenCL-ICD-Loader.git
        GIT_TAG v2024.10.24
    )
    # ocl.hpp's ocl_wrapper.hpp needs <CL/cl2.hpp>, the C++ wrapper - a
    # separate repo from the plain C headers above.
    FetchContent_Declare(
        opencl_clhpp
        GIT_REPOSITORY https://github.com/KhronosGroup/OpenCL-CLHPP.git
        GIT_TAG v2024.10.24
    )
    set(OPENCL_CLHPP_BUILD_TESTING OFF CACHE BOOL "" FORCE)
    set(BUILD_DOCS OFF CACHE BOOL "" FORCE)
    set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
    set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
    FetchContent_MakeAvailable(opencl_headers opencl_icd_loader opencl_clhpp)
endif()

# Spec §6.1: "Memory manager... assert no leaks (use a sanitizer build -
# AddressSanitizer - for this, not manual inspection)." Also catches
# use-after-free/data-race classes of bug across the whole scheduler/queue
# stack, not just the memory manager specifically.
#
# MSVC branch is what's actually been run on this machine: MSVC has shipped
# a real, supported /fsanitize=address since VS2019 16.9, and this repo's
# MinGW toolchain (MSYS2 GCC 15.2.0, both the ucrt64 and mingw64
# subsystems) currently does not ship libasan/libubsan at all - confirmed
# by attempting a MinGW ASan build here first and hitting
# "cannot read spec file 'libsanitizer.spec'" - so the GCC/Clang branch
# below is kept for a toolchain that does have it (e.g. Linux CI) but is
# not what was validated locally. See docs/DEVELOPMENT.md.
option(SCAVENGER_ENABLE_ASAN
    "Build core + tests with AddressSanitizer (+ UndefinedBehaviorSanitizer on GCC/Clang)"
    OFF)
if(SCAVENGER_ENABLE_ASAN)
    if(MSVC)
        add_compile_options(/fsanitize=address)
        # ASan and MSVC's default Debug-config /RTC (Run-Time Checks) are
        # mutually exclusive - strip /RTC1 if CMake's default Debug flags
        # already injected it, rather than requiring callers to remember
        # a matching CMAKE_CXX_FLAGS_DEBUG override.
        string(REPLACE "/RTC1" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
    else()
        add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1)
        add_link_options(-fsanitize=address,undefined)
    endif()
endif()

# enable_testing()/include(CTest) must be called here, at the top level,
# before any add_subdirectory() that registers tests - CTest only generates
# the CTestTestfile.cmake tree (what `ctest` actually walks) starting from
# wherever this is first invoked. Calling it only inside core/CMakeLists.txt
# silently produced zero discoverable tests despite the test binary building
# fine - caught by actually running `ctest` locally, not just checking the
# build succeeded.
if(SCAVENGER_BUILD_TESTS)
    include(CTest)
endif()

# llama.cpp is vendored as a pinned git submodule (third_party/llama.cpp)
# rather than fetched, since it's large and we want an explicit, reviewable
# pin (see docs/hardware/llama_vulkan_validation.md for the exact commit).
# Its own CMakeLists.txt already defaults every example/tool/test/server
# target OFF when consumed via add_subdirectory (LLAMA_STANDALONE becomes
# false), so this brings in only the ggml/llama libraries themselves.
# GGML_VULKAN must be set before add_subdirectory() - ggml's CMakeLists uses
# option(), which only takes effect the first time a variable is set.
if(SCAVENGER_BUILD_HARDWARE_TOOLS OR SCAVENGER_BUILD_BACKENDS)
    set(GGML_VULKAN ON CACHE BOOL "ggml: use Vulkan" FORCE)

    # Phase M1: also turn on ggml's CUDA backend when SCAVENGER_BUILD_CUDA
    # is set - see that option's comment above for why this must happen
    # before add_subdirectory(third_party/llama.cpp) (same first-set-wins
    # option() constraint as GGML_VULKAN). Vulkan stays ON regardless (the
    # iGPU path is unaffected/still needed), so a CUDA-enabled build gets
    # both ggml-vulkan and ggml-cuda linked in, exactly matching
    # LlamaVulkanBackend (kIgpu) and LlamaCudaBackend (kDgpu) each needing
    # their own ggml backend.
    if(SCAVENGER_BUILD_CUDA)
        set(GGML_CUDA ON CACHE BOOL "ggml: use CUDA" FORCE)
    endif()

    # SCAVENGER_VULKAN_DISABLE_COOPMAT2: workaround for glslc crashing when
    # compiling GL_NV_cooperative_matrix2 (CM2) shader variants. This occurs on
    # machines with an NVIDIA dGPU present (NVIDIA's driver exposes NV CM2 support
    # in glslc's Vulkan environment, but glslc 1.4.350.0 then crashes on the full
    # production shader with exit codes like STATUS_INVALID_PARAMETER). The Intel
    # iGPU reference machine in docs/hardware/environment_report.md is unaffected
    # because NVIDIA's driver is not present there. Set this ON when building on a
    # machine with an NVIDIA GPU alongside the Intel iGPU.
    option(SCAVENGER_VULKAN_DISABLE_COOPMAT2
        "Disable GL_NV_cooperative_matrix2 Vulkan shader variants (workaround for glslc crashes with NVIDIA dGPU present)"
        OFF)
    if(SCAVENGER_VULKAN_DISABLE_COOPMAT2)
        set(GGML_VULKAN_DISABLE_COOPMAT2 ON CACHE BOOL "" FORCE)
    endif()

    add_subdirectory(third_party/llama.cpp)
endif()

# core/ is the only thing that must build with nothing more than a C++17
# compiler + CMake (no OpenVINO/llama.cpp/Vulkan dependency yet) — see
# PHASE_PLAN.md Phase 0 exit criteria.
add_subdirectory(core)

if(SCAVENGER_BUILD_PYTHON_BINDINGS)
    add_subdirectory(bindings)
endif()
