cmake_minimum_required(VERSION 3.20.0)

if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
  project(libclc VERSION 0.2.0 LANGUAGES CXX C)
endif()
set(LLVM_SUBPROJECT_TITLE "libclc")

set(CMAKE_CXX_STANDARD 17)

# Add path for custom modules
list( INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules" )

enable_language( CLC )

include( AddLibclc )

include( GNUInstallDirs )

set( LIBCLC_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR} )

option(
  LIBCLC_USE_SPIRV_BACKEND "Build SPIR-V targets with the SPIR-V backend." OFF
)

# List of all supported architectures.
set( LIBCLC_ARCHS_NVPTX nvptx64 )
set( LIBCLC_ARCHS_AMDGPU amdgpu amdgcn )
set( LIBCLC_ARCHS_SPIRV spirv spirv32 spirv64 )
set( LIBCLC_ARCHS_ALL ${LIBCLC_ARCHS_AMDGPU} ${LIBCLC_ARCHS_NVPTX} ${LIBCLC_ARCHS_SPIRV} )

set(LIBCLC_TARGET ${LLVM_DEFAULT_TARGET_TRIPLE})

if(NOT LIBCLC_TARGET)
  message(FATAL_ERROR "libclc target is empty\n")
endif()

# Parse the target triple into arch and OS components.
string(REPLACE "-" ";" triple_components ${LIBCLC_TARGET})
list(GET triple_components 0 LIBCLC_TARGET_ARCH)
list(GET triple_components 2 LIBCLC_TARGET_OS)

if(NOT "${LIBCLC_TARGET_ARCH}" IN_LIST LIBCLC_ARCHS_ALL)
  message(FATAL_ERROR "Unknown libclc target architecture: ${LIBCLC_TARGET_ARCH}\n"
    "Target was: ${LIBCLC_TARGET}\n"
    "Valid architectures are: ${LIBCLC_ARCHS_ALL}\n")
endif()

if( LIBCLC_STANDALONE_BUILD OR CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR )
  set( LIBCLC_STANDALONE_BUILD TRUE )

  find_package(LLVM REQUIRED HINTS "${LLVM_CMAKE_DIR}")
  include(AddLLVM)

  foreach( tool IN ITEMS llvm-link opt )
    find_program( LLVM_TOOL_${tool} ${tool} PATHS ${LLVM_TOOLS_BINARY_DIR} NO_DEFAULT_PATH )
    set( ${tool}_exe ${LLVM_TOOL_${tool}} )
    set( ${tool}_target )
  endforeach()

  # Setup the paths where libclc runtimes should be stored.
  set( LIBCLC_OUTPUT_LIBRARY_DIR ${CMAKE_CURRENT_BINARY_DIR} )
  set( LIBCLC_INSTALL_DIR ${CMAKE_INSTALL_DATADIR}/clc )
else()
  set( LIBCLC_STANDALONE_BUILD FALSE )

  # Note that we check this later (for both build types) but we can provide a
  # more useful error message when built in-tree. We assume that LLVM tools are
  # always available so don't warn here.
  if( NOT LLVM_RUNTIMES_BUILD AND NOT clang IN_LIST LLVM_ENABLE_PROJECTS )
    message(FATAL_ERROR "Clang is not enabled, but is required to build libclc in-tree")
  endif()

  # The package version is required to find the resource directory.
  if( LLVM_PACKAGE_VERSION AND NOT PACKAGE_VERSION )
    set( PACKAGE_VERSION "${LLVM_PACKAGE_VERSION}" )
  endif()

  get_host_tool_path( llvm-link LLVM_LINK llvm-link_exe llvm-link_target )
  get_host_tool_path( opt OPT opt_exe opt_target )

  # Setup the paths where libclc runtimes should be stored. By default, in an
  # in-tree build we place the libraries in clang's resource driectory.
  include(GetClangResourceDir)
  get_clang_resource_dir( LIBCLC_INSTALL_DIR )
  cmake_path( APPEND LIBCLC_INSTALL_DIR "lib" )

  cmake_path( GET LLVM_LIBRARY_OUTPUT_INTDIR PARENT_PATH LIBCLC_OUTPUT_LIBRARY_DIR )
  cmake_path( APPEND LIBCLC_OUTPUT_LIBRARY_DIR ${LIBCLC_INSTALL_DIR} )
endif()

if( NOT LIBCLC_USE_SPIRV_BACKEND )
  # llvm-spirv is an optional dependency, used to build spirv-* targets when
  # the SPIR-V backend hasn't been requested. It may be provided in-tree or
  # externally.
  if( TARGET llvm-spirv )
    get_host_tool_path( llvm-spirv LLVM_SPIRV llvm-spirv_exe llvm-spirv_target )
  else()
    find_program( LLVM_SPIRV llvm-spirv HINTS ${LLVM_TOOLS_BINARY_DIR} )
    set( llvm-spirv_exe "${LLVM_SPIRV}" )
  endif()
endif()

message(STATUS "libclc target '${LIBCLC_TARGET}' is enabled")

# Map the LLVM target architecture to the standard directory name.
if(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_AMDGPU)
  set(LIBCLC_ARCH_DIR amdgpu)
elseif(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_NVPTX)
  set(LIBCLC_ARCH_DIR nvptx)
elseif(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_SPIRV)
  set(LIBCLC_ARCH_DIR spirv)
endif()

if(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_SPIRV)
  if(NOT LIBCLC_TARGET_OS STREQUAL vulkan AND NOT LIBCLC_USE_SPIRV_BACKEND AND NOT llvm-spirv_exe)
    message(FATAL_ERROR "SPIR-V backend or llvm-spirv is required for libclc ${LIBCLC_TARGET}")
  endif()
endif()

foreach( tool IN ITEMS opt llvm-link )
  if( NOT EXISTS "${${tool}_exe}" AND "${${tool}_target}" STREQUAL "" )
    message( FATAL_ERROR "libclc toolchain incomplete - missing tool ${tool}!" )
  endif()
endforeach()

# Determine the clang target triple.
set(clang_triple ${LIBCLC_TARGET})

# Address space values.
set(private_addrspace_val 0)
set(generic_addrspace_val 0)
if(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_AMDGPU)
  set(private_addrspace_val 5)
endif()
if(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_SPIRV AND NOT LIBCLC_TARGET_OS STREQUAL vulkan)
  set(generic_addrspace_val 4)
endif()

# Target-specific compile flags and defines.
set(target_compile_flags)
set(target_extra_defines)
set(opt_flags -O3)

if(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_SPIRV)
  if(LIBCLC_TARGET_OS STREQUAL vulkan)
    list(APPEND target_compile_flags -Wno-unknown-assumption -U__opencl_c_int64)
  else()
    list(APPEND target_compile_flags -O0 -finline-hint-functions)
    list(APPEND target_extra_defines CLC_SPIRV)
    set(opt_flags)
  endif()
elseif(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_AMDGPU)
  list(APPEND target_compile_flags "SHELL:-Xclang -mcode-object-version=none")
endif()
if(LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_AMDGPU OR LIBCLC_TARGET_ARCH IN_LIST LIBCLC_ARCHS_NVPTX)
  list(APPEND target_compile_flags -fvisibility=hidden)
  list(APPEND target_compile_flags "SHELL:-Xclang -fapply-global-visibility-to-externs")
endif()

# Common compile options shared by CLC and OpenCL libraries.
set(compile_flags
  -flto
  --target=${clang_triple}
  -nostdlib
  -nostdlibinc
  -cl-no-stdinc
  -cl-std=CL3.0
  -include opencl-c-base.h
  -Werror=undef
  -Wall
  -Wextra
  -fdiscard-value-names
  -ffp-contract=fast-honor-pragmas
  -fdenormal-fp-math=dynamic
  ${target_compile_flags}
)

set(common_defs
  ${target_extra_defines}
  __CLC_PRIVATE_ADDRSPACE_VAL=${private_addrspace_val}
  __CLC_GENERIC_ADDRSPACE_VAL=${generic_addrspace_val}
)

add_custom_target(libclc ALL)
add_custom_target(libclc-opencl-builtins COMMENT "Build libclc OpenCL builtins")
add_dependencies(libclc libclc-opencl-builtins)

# Configure the CLC internal builtins library.
set(LIBCLC_CLC_TARGET clc)
libclc_add_builtin_library(${LIBCLC_CLC_TARGET}
  COMPILE_OPTIONS ${compile_flags}
  INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/clc/include
  COMPILE_DEFINITIONS ${common_defs}
  SOURCE_SUB_DIR clc
  FOLDER "libclc/Device IR/CLC"
)

# Build, link, and install the final OpenCL builtins library.
set(LIBCLC_OPENCL_TARGET opencl)
libclc_add_library(libclc-${LIBCLC_TARGET}
  ARCH ${LIBCLC_TARGET_ARCH}
  TRIPLE ${clang_triple}
  TARGET_TRIPLE ${LIBCLC_TARGET}
  COMPILE_OPTIONS ${compile_flags} "SHELL:-Xclang -fdeclare-opencl-builtins"
  INCLUDE_DIRS
    ${CMAKE_CURRENT_SOURCE_DIR}/clc/include
    ${CMAKE_CURRENT_SOURCE_DIR}/opencl/include
  COMPILE_DEFINITIONS ${common_defs}
  INTERNALIZE_LIBRARIES ${LIBCLC_CLC_TARGET}
  OPT_FLAGS ${opt_flags}
  SOURCE_TARGET ${LIBCLC_OPENCL_TARGET}
  SOURCE_SUB_DIR opencl
  OUTPUT_FILENAME libclc
  PARENT_TARGET libclc-opencl-builtins
)

if(LLVM_INCLUDE_TESTS)
  add_subdirectory(test)
endif()
