# Copyright (c) 2025-2026 Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT

cmake_minimum_required(VERSION 3.22)

set(_RJ_ROCM_PATH_EXPLICIT OFF)
if(DEFINED ROCM_PATH AND NOT "${ROCM_PATH}" STREQUAL "")
    set(_RJ_ROCM_PATH_EXPLICIT ON)
elseif(DEFINED ENV{ROCM_PATH} AND NOT "$ENV{ROCM_PATH}" STREQUAL "")
    set(_RJ_ROCM_PATH_EXPLICIT ON)
    set(ROCM_PATH "$ENV{ROCM_PATH}" CACHE PATH "Path to the ROCm installation")
elseif(IS_DIRECTORY "/opt/rocm")
    set(ROCM_PATH "/opt/rocm" CACHE PATH "Path to the ROCm installation")
endif()

if(_RJ_ROCM_PATH_EXPLICIT AND NOT IS_DIRECTORY "${ROCM_PATH}")
    message(FATAL_ERROR "ROCM_PATH must be set to a valid directory")
endif()
unset(_RJ_ROCM_PATH_EXPLICIT)

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(rj_default_compiler)

project(rocm_jit_suite VERSION 0.3.0)

include(rj_version)

# Generate compile_commands.json for clangd, clang-tidy, and other tooling that
# needs the exact compiler invocations from the configured build tree.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Default install prefix to /opt/rocm, matching other ROCm projects.
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT AND ROCM_PATH)
    set(CMAKE_INSTALL_PREFIX "${ROCM_PATH}" CACHE PATH "" FORCE)
endif()

include(CTest)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Hide symbols by default so the shared library (librocjitsu.so) exports only its
# intended ABI.
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)

find_package(Threads REQUIRED)

# GCC 14+ emits false-positive -Wuninitialized on reinterpret_cast<uint32_t*>
# member access in the autogenerated ISA encoding constructors. The pointer is
# always initialized from a constructor argument, but GCC's alias analysis
# misattributes the access to an uninitialized offset within `this`.
if(
    CMAKE_CXX_COMPILER_ID STREQUAL "GNU"
    AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "14"
)
    add_compile_options(-Wno-uninitialized)
    if(CMAKE_SYSTEM_PROCESSOR MATCHES "ppc64")
        add_compile_options(-Wno-maybe-uninitialized -Wno-stringop-overflow)
    endif()
endif()

# HSA/KFD headers (local copies from rocr-runtime).
set(HSA_INCLUDE_DIR
    "${PROJECT_SOURCE_DIR}/lib/rocjitsu/external_headers/hsa_headers"
)

include(rj_log)

# Install directory variables are used by subdirectories that add install rules.
include(GNUInstallDirs)

include(rj_sanitizers)

# Sanitizer and static analysis options.
option(RJ_ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(RJ_ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
option(RJ_ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(RJ_ENABLE_MSAN "Enable MemorySanitizer" OFF)
set(RJ_SANITIZER_RUNTIME
    "AUTO"
    CACHE STRING
    "Sanitizer runtime linkage policy (AUTO, SHARED, or STATIC)"
)
set_property(CACHE RJ_SANITIZER_RUNTIME PROPERTY STRINGS AUTO SHARED STATIC)
string(TOUPPER "${RJ_SANITIZER_RUNTIME}" RJ_SANITIZER_RUNTIME)
set(_rj_sanitizer_runtime_values AUTO SHARED STATIC)
if(NOT RJ_SANITIZER_RUNTIME IN_LIST _rj_sanitizer_runtime_values)
    message(
        FATAL_ERROR
        "RJ_SANITIZER_RUNTIME must be AUTO, SHARED, or STATIC; got "
        "'${RJ_SANITIZER_RUNTIME}'."
    )
endif()
unset(_rj_sanitizer_runtime_values)
option(RJ_CLANG_TIDY "Enable clang-tidy static analysis" OFF)
option(
    RJ_ENABLE_EXPENSIVE_CHECKS
    "Enable expensive exhaustive test suites (MFMA/WMMA SIMD bit-exactness)"
    OFF
)
option(RJ_INSTALL_TESTS "Install rocjitsu test binaries" OFF)
option(
    ROCJITSU_ENABLE_VFIO
    "Build the vfio-user front end that serves rocjitsu as a PCIe device to a VMM"
    OFF
)
option(
    LTO
    "Enable link-time optimization (IPO) for Release / RelWithDebInfo"
    OFF
)

set(_rj_sanitizer_kinds)
if(RJ_ENABLE_ASAN)
    list(APPEND _rj_sanitizer_kinds address)
endif()
if(RJ_ENABLE_UBSAN)
    list(APPEND _rj_sanitizer_kinds undefined)
endif()
if(RJ_ENABLE_TSAN)
    list(APPEND _rj_sanitizer_kinds thread)
endif()
if(RJ_ENABLE_MSAN)
    list(APPEND _rj_sanitizer_kinds memory)
endif()
list(LENGTH _rj_sanitizer_kinds _rj_sanitizer_count)
set(RJ_SANITIZER_USE_SHARED OFF)

if(LTO)
    if(_rj_sanitizer_count GREATER 0)
        message(FATAL_ERROR "LTO is incompatible with sanitizers. Disable one.")
    endif()
    include(CheckIPOSupported)
    check_ipo_supported(RESULT _rj_ipo_ok OUTPUT _rj_ipo_err)
    if(_rj_ipo_ok)
        set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
        set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON)
        # Force parallel LTO. CMake's default IPO flag is a bare `-flto`, which
        # makes GCC serialize LTRANS (warns: "using serial compilation of N
        # LTRANS jobs"). Override per compiler:
        #   GCC   : -flto=auto      — partition across all host cores.
        #   Clang : -flto=thin      — ThinLTO is parallel by design.
        if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
            set(_rj_lto_flag "-flto=auto")
        elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
            set(_rj_lto_flag "-flto=thin")
        else()
            set(_rj_lto_flag "")
        endif()
        if(_rj_lto_flag)
            add_compile_options(
                $<$<CONFIG:Release>:${_rj_lto_flag}>
                $<$<CONFIG:RelWithDebInfo>:${_rj_lto_flag}>
            )
            add_link_options(
                $<$<CONFIG:Release>:${_rj_lto_flag}>
                $<$<CONFIG:RelWithDebInfo>:${_rj_lto_flag}>
            )
        endif()
        message(
            STATUS
            "LTO enabled for Release and RelWithDebInfo (flag: ${_rj_lto_flag})"
        )
    else()
        message(WARNING "LTO requested but IPO not supported: ${_rj_ipo_err}")
    endif()
endif()

if(_rj_sanitizer_count GREATER 0)
    if(
        thread IN_LIST _rj_sanitizer_kinds
        AND (
            address IN_LIST _rj_sanitizer_kinds
            OR memory IN_LIST _rj_sanitizer_kinds
        )
    )
        message(
            FATAL_ERROR
            "ThreadSanitizer can not be combined with address or memory sanitizers"
        )
    endif()
    if(memory IN_LIST _rj_sanitizer_kinds AND _rj_sanitizer_count GREATER 1)
        message(
            FATAL_ERROR
            "MemorySanitizer cannot be combined with other sanitizers"
        )
    endif()
    list(JOIN _rj_sanitizer_kinds "," _rj_sanitizer_arg)
    set(RJ_SANITIZER_COMPILE_OPTIONS)
    set(RJ_SANITIZER_LINK_OPTIONS)
    set(RJ_SANITIZER_SHARED_LIBRARIES)
    set(RJ_ASAN_SHARED_LIBRARY "")
    set(RJ_TSAN_SHARED_LIBRARY "")
    message(STATUS "Sanitizer enabled: ${_rj_sanitizer_arg}")
    if(MSVC)
        # MSVC only supports AddressSanitizer; ubsan/tsan/msan are GCC/Clang-only.
        if(_rj_sanitizer_arg STREQUAL "address")
            list(APPEND RJ_SANITIZER_COMPILE_OPTIONS /fsanitize=address)
            add_compile_options(${RJ_SANITIZER_COMPILE_OPTIONS})
            # /INCREMENTAL:NO is required when using MSVC ASAN.
            add_link_options(/INCREMENTAL:NO)
        else()
            message(FATAL_ERROR "Only RJ_ENABLE_ASAN is supported by MSVC.")
        endif()
    elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
        if(
            memory IN_LIST _rj_sanitizer_kinds
            AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang"
        )
            message(FATAL_ERROR "MemorySanitizer requires Clang.")
        endif()
        if(RJ_SANITIZER_RUNTIME STREQUAL "SHARED")
            if(memory IN_LIST _rj_sanitizer_kinds)
                message(
                    FATAL_ERROR
                    "MemorySanitizer has no supported shared runtime; use "
                    "RJ_SANITIZER_RUNTIME=AUTO or STATIC."
                )
            endif()
            set(RJ_SANITIZER_USE_SHARED ON)
        elseif(
            RJ_SANITIZER_RUNTIME STREQUAL "AUTO"
            AND NOT memory IN_LIST _rj_sanitizer_kinds
        )
            set(RJ_SANITIZER_USE_SHARED ON)
        endif()
        if(RJ_SANITIZER_USE_SHARED)
            message(STATUS "Sanitizer runtime linkage: shared")
        else()
            message(STATUS "Sanitizer runtime linkage: static")
        endif()
        list(APPEND RJ_SANITIZER_COMPILE_OPTIONS "-fno-sanitize-recover=all")
        list(
            APPEND RJ_SANITIZER_COMPILE_OPTIONS
            -fsanitize=${_rj_sanitizer_arg}
        )
        list(APPEND RJ_SANITIZER_LINK_OPTIONS -fsanitize=${_rj_sanitizer_arg})
        if(
            address IN_LIST _rj_sanitizer_kinds
            OR undefined IN_LIST _rj_sanitizer_kinds
            OR memory IN_LIST _rj_sanitizer_kinds
        )
            list(APPEND RJ_SANITIZER_COMPILE_OPTIONS -fno-omit-frame-pointer)
        endif()
        if(address IN_LIST _rj_sanitizer_kinds)
            # GCC 13+ emits false-positive -Wmaybe-uninitialized in std_function.h
            # when ASAN is enabled. Suppress to avoid -Werror failures.
            if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
                add_compile_options(-Wno-maybe-uninitialized)
            endif()
        endif()
        if(RJ_SANITIZER_USE_SHARED)
            rj_find_sanitizer_shared_libraries(
                RJ_SANITIZER_SHARED_LIBRARIES
                RJ_ASAN_SHARED_LIBRARY
                RJ_TSAN_SHARED_LIBRARY
                COMPILER ${CMAKE_CXX_COMPILER}
                COMPILER_ARG1 "${CMAKE_CXX_COMPILER_ARG1}"
                COMPILER_ID "${CMAKE_CXX_COMPILER_ID}"
                SYSTEM_PROCESSOR "${CMAKE_SYSTEM_PROCESSOR}"
                SANITIZERS ${_rj_sanitizer_kinds}
            )
            foreach(
                _rj_sanitizer_shared_library
                IN
                LISTS RJ_SANITIZER_SHARED_LIBRARIES
            )
                message(
                    STATUS
                    "Sanitizer shared runtime: ${_rj_sanitizer_shared_library}"
                )
            endforeach()
        endif()
        rj_sanitizer_runtime_link_options(
            _rj_sanitizer_runtime_link_options
            COMPILER_ID "${CMAKE_CXX_COMPILER_ID}"
            SHARED "${RJ_SANITIZER_USE_SHARED}"
            SHARED_LIBRARIES ${RJ_SANITIZER_SHARED_LIBRARIES}
            SANITIZERS ${_rj_sanitizer_kinds}
        )
        list(
            APPEND RJ_SANITIZER_LINK_OPTIONS
            ${_rj_sanitizer_runtime_link_options}
        )
        add_compile_options(${RJ_SANITIZER_COMPILE_OPTIONS})
        add_link_options(${RJ_SANITIZER_LINK_OPTIONS})
        unset(_rj_sanitizer_shared_library)
        unset(_rj_sanitizer_runtime_link_options)
    else()
        message(
            FATAL_ERROR
            "Sanitizers are not supported for compiler '${CMAKE_CXX_COMPILER_ID}'."
        )
    endif()
    unset(_rj_sanitizer_arg)
endif()
unset(_rj_sanitizer_count)
unset(_rj_sanitizer_kinds)

if(RJ_CLANG_TIDY)
    find_program(CLANG_TIDY_EXE NAMES clang-tidy)
    if(CLANG_TIDY_EXE)
        message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}")
        set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}")
    else()
        message(WARNING "clang-tidy not found, RJ_CLANG_TIDY ignored")
    endif()
endif()

# Third-party dependencies (fetched into third_party/).
# Third-party dependencies (fetched into third_party/).
# Third-party dependencies (fetched into third_party/ by default).
# Override with -DFETCHCONTENT_BASE_DIR=/path for container builds.
if(NOT FETCHCONTENT_BASE_DIR)
    set(FETCHCONTENT_BASE_DIR ${PROJECT_SOURCE_DIR}/third_party)
endif()
include(FetchContent)

# Google Test
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.15.2
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)

# FlatBuffers - serialization for config, checkpoint, and wire format.
FetchContent_Declare(
    flatbuffers
    GIT_REPOSITORY https://github.com/google/flatbuffers.git
    GIT_TAG v24.3.25
)
set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FLATBUFFERS_INSTALL OFF CACHE BOOL "" FORCE)
set(FLATBUFFERS_BUILD_FLATLIB ON CACHE BOOL "" FORCE)

FetchContent_MakeAvailable(googletest flatbuffers)

include(rj_flatbuffers)

# Set include paths before adding subdirectories so all targets can find them.
set(ROCJITSU_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/lib/rocjitsu/include)
set(ROCJITSU_SRC_DIR ${PROJECT_SOURCE_DIR}/lib/rocjitsu/src)

if(ROCJITSU_ENABLE_VFIO)
    if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
        message(
            FATAL_ERROR
            "ROCJITSU_ENABLE_VFIO=ON is Linux-only: the vfio-user protocol reuses the Linux VFIO uapi definitions and the transport relies on AF_UNIX file-descriptor passing."
        )
    endif()
    include(rj_libvfio_user)
endif()

# Libraries
add_subdirectory(lib/util)
add_subdirectory(lib/simdojo)
add_subdirectory(lib/rocjitsu)
add_subdirectory(lib/rocjitsu/src/rocjitsu/vm/plugins)

# Main shared library - assembles all object libraries.
add_library(rocjitsu_shared SHARED)
target_sources(
    rocjitsu_shared
    PRIVATE $<TARGET_OBJECTS:rocjitsu_version_objects>
)
set_target_properties(rocjitsu_shared PROPERTIES OUTPUT_NAME rocjitsu)
target_link_libraries(
    rocjitsu_shared
    PRIVATE
        rocjitsu_analysis
        rocjitsu_code
        rocjitsu_isa
        rocjitsu_isa_registry
        simdojo
        rocjitsu_kmd
        rocjitsu_vm
        rocjitsu_vm_amdgpu
        # Host-side plugin loader. The logging/race plugins are no longer
        # linked in statically; they are separate librocjitsu_plugin_<name>.so
        # modules discovered and dlopen()ed at runtime.
        rocjitsu_plugin_loader
        rocjitsu_vm_risc_v
        rocjitsu_config
        rocjitsu_dbt_config
        rocjitsu_drm_headers
        util
        flatbuffers
        Threads::Threads
)
target_include_directories(
    rocjitsu_shared
    PRIVATE ${ROCJITSU_INCLUDE_DIR} ${ROCJITSU_SRC_DIR} ${GENERATED_DIR}
)
target_include_directories(rocjitsu_shared SYSTEM PRIVATE ${HSA_INCLUDE_DIR})

# Linux-only: KMD interposer (LD_PRELOAD shim) and its dependencies.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    target_sources(
        rocjitsu_shared
        PRIVATE ${ROCJITSU_SRC_DIR}/rocjitsu/kmd/linux/interposer.cpp
    )
    target_compile_definitions(rocjitsu_shared PRIVATE _GNU_SOURCE)
    target_link_libraries(rocjitsu_shared PRIVATE dl)
endif()

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    target_link_options(rocjitsu_shared PRIVATE "LINKER:--exclude-libs,ALL")
endif()

add_dependencies(rocjitsu_shared flatbuffers_schemas)

# Developer and test workflow tools.
add_subdirectory(tools)
add_subdirectory(tools/rj-ip-discovery)
add_subdirectory(fuzz/decode)

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    add_subdirectory(tools/rocjitsu)
endif()
if(BUILD_TESTING)
    add_subdirectory(tests)
endif()

# ---------------------------------------------------------------------------
# Install rules.
# ---------------------------------------------------------------------------

# Public headers: include/rocjitsu/
install(
    DIRECTORY lib/rocjitsu/include/rocjitsu
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    FILES_MATCHING
    PATTERN "*.h"
)

# Libraries: lib/
install(
    TARGETS rocjitsu_shared rocjitsu_hooks
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    # The translator ships alongside the hook: both must resolve to the SAME
    # shared object, or the cache identity they derive from it would differ and
    # neither could read what the other wrote.
    install(
        TARGETS hsa_hotswap_rocjitsu rocjitsu_gfx1250_b0_to_a0
        LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    )
endif()

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    # rj_pretranslate ships because the tier it fills is only worth having if an
    # image build or a post-install step can fill it.
    install(
        TARGETS rocjitsu_bin rj_pretranslate
        RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    )
    # And the wrapper ships with it. rj_pretranslate takes standalone code
    # objects; the script is the only piece that knows how to find them inside
    # fat binaries, offload bundles and KPACK archives, and the tool's own help
    # text points at it. Installing one without the other leaves that pointer
    # dangling and the deployed workflow with no entry point.
    install(
        PROGRAMS scripts/rocjitsu-pretranslate.py
        DESTINATION ${CMAKE_INSTALL_BINDIR}
    )
endif()

# Config files and schemas: share/rocjitsu/
install(
    DIRECTORY configs/
    DESTINATION ${CMAKE_INSTALL_DATADIR}/rocjitsu/configs
)
install(
    DIRECTORY schemas/
    DESTINATION ${CMAKE_INSTALL_DATADIR}/rocjitsu/schemas
    FILES_MATCHING
    PATTERN "*.fbs"
)
