# =============================================================================
# QHexRT Backend — RunAnywhere's PRIVATE Qualcomm Hexagon NPU runtime
#
# QHexRT runs prebuilt QNN context binaries on Snapdragon Hexagon NPUs (HTP).
# It is a closed-source, RunAnywhere-owned dependency consumed as a PREBUILT
# static archive — its source never enters this repo.
#
# Two modes:
#
#   1) Engine NOT available (default in the public repo):
#      - The prebuilt archive / header under QHEXRT_ROOT is absent, so
#        RAC_QHEXRT_ENGINE_AVAILABLE stays 0.
#      - Only the shell (rac_plugin_entry_qhexrt.cpp) compiles; it publishes a
#        not-routable, all-NULL vtable and rejects registration with
#        RAC_ERROR_BACKEND_UNAVAILABLE. No QHexRT header/lib needed. The public
#        repo links cleanly.
#
#   2) Engine IS available (internal / authorized Android + Windows-ARM64 builds):
#      - QHEXRT_ROOT points at the prebuilt tree:
#            $QHEXRT_ROOT/include/qhexrt/qhexrt_c.h
#            $QHEXRT_ROOT/lib/<abi>/<core archive>
#            $QHEXRT_ROOT/lib/<abi>/<host archive>
#        (defaults to the atomically selected engines/qhexrt/prebuilt/current,
#        whose immutable versions are gitignored).
#      - RAC_QHEXRT_ENGINE_AVAILABLE=1; qhexrt_llm_ops.cpp compiles against the
#        private header and the archive links INTO the plugin .so (no separately
#        shippable QHexRT binary; runtime symbols stay internal to the carrier).
#      - Publishes a routable vtable exposing RAC_PRIMITIVE_GENERATE_TEXT.
#
# Two host platforms carry a real Hexagon NPU, and they use different toolchains,
# so the ABI directory and archive naming differ:
#
#   Android arm64-v8a : lib/arm64-v8a/libqhexrt_{core,host}.a  (AArch64 ELF, NDK)
#   Windows on ARM64  : lib/win-arm64/qhexrt_{core,host}.lib   (ARM64 COFF, MSVC)
#
# Windows on ARM64 (Snapdragon X / X2 Elite) is NOT a simulator: QAIRT ships a
# native aarch64-windows-msvc HTP stack, so the same published v81 context
# binaries load unmodified. It differs only in packaging — flat DLL staging
# (QnnHtp.dll / QnnSystem.dll / QnnHtpPrepare.dll / QnnHtpV81Stub.dll plus the
# v81 skel AND its .cat, all in ONE directory; there is no ADSP_LIBRARY_PATH) and
# no FastRPC/rpcmem, so tensors bind clientBuf instead of the zero-copy ion path.
#
# OFF by default: the QNN runtime + NPU firmware are Snapdragon-only and absent
# on CI hosts. Downstream Android / Windows-ARM64 builds set
# -DRAC_BACKEND_QHEXRT=ON (+ optionally -DQHEXRT_ROOT=<path>).
# =============================================================================

option(RAC_BACKEND_QHEXRT "Build QHexRT (Qualcomm Hexagon NPU) engine plugin" OFF)
if(NOT RAC_BACKEND_QHEXRT)
    return()
endif()

message(STATUS "Configuring QHexRT (Qualcomm Hexagon NPU) backend...")

include(${RAC_ROOT_DIR}/cmake/plugins.cmake)

function(_qhexrt_require_json_keys json_variable label expected_csv)
    set(_json "${${json_variable}}")
    string(JSON _count ERROR_VARIABLE _error LENGTH "${_json}" ${ARGN})
    if(NOT _error STREQUAL "NOTFOUND")
        message(FATAL_ERROR "Invalid QHexRT JSON object ${label}: ${_error}")
    endif()
    set(_actual "")
    if(_count GREATER 0)
        math(EXPR _last "${_count} - 1")
        foreach(_index RANGE 0 ${_last})
            string(JSON _key MEMBER "${_json}" ${ARGN} ${_index})
            list(APPEND _actual "${_key}")
        endforeach()
    endif()
    string(REPLACE "," ";" _expected "${expected_csv}")
    list(SORT _actual)
    list(SORT _expected)
    if(NOT "${_actual}" STREQUAL "${_expected}")
        message(FATAL_ERROR
            "QHexRT JSON object ${label} has wrong keys: actual=${_actual} expected=${_expected}")
    endif()
endfunction()

# The receipt's C ABI version is provenance and a breaking-line guard, not a
# feature gate. QHexRT has kept additive, size-versioned extensions on ABI 1.0,
# so validate the exact header surface this adapter consumes instead of
# inventing a minimum minor version that the producer does not publish.
function(_qhexrt_validate_header_surface include_dir abi_major abi_minor)
    include(CheckCSourceCompiles)
    set(CMAKE_REQUIRED_INCLUDES "${include_dir}")
    set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
    unset(RAC_QHEXRT_HEADER_SURFACE_COMPATIBLE CACHE)
    check_c_source_compiles("
#include <qhexrt/qhexrt_c.h>

#if !defined(QHX_ABI_VERSION_MAJOR) || !defined(QHX_ABI_VERSION_MINOR)
#error QHexRT header omits its C ABI identity
#endif
#if QHX_ABI_VERSION_MAJOR != ${abi_major} || QHX_ABI_VERSION_MINOR != ${abi_minor}
#error QHexRT header C ABI identity differs from the staged receipt
#endif

static qhx_status (*const require_generate_ex)(
    qhx_session*, const qhx_inputs*, const qhx_gen_cfg*,
    const qhx_generate_options*, qhx_token_cb, void*, qhx_output*) = qhx_generate_ex;
static void (*const require_generate_options_default)(qhx_generate_options*) =
    qhx_generate_options_default;
static void (*const require_session_cancel)(qhx_session*) = qhx_session_cancel;

int qhexrt_header_surface_probe(void) {
    qhx_inputs inputs = {0};
    qhx_output output = {0};
    qhx_generate_options options = {0};

    inputs.mask_path = 0;
    output.embedding = 0;
    output.n_embedding = 0;
    output.image = 0;
    output.img_h = output.img_w = output.img_c = 0;
    options.struct_size = sizeof(options);
    options.disable_thinking = 0;
    options.should_cancel = 0;
    options.should_cancel_user = 0;

    return require_generate_ex == 0 || require_generate_options_default == 0 ||
           require_session_cancel == 0 || inputs.mask_path != 0 ||
           output.embedding != 0 || output.image != 0 || options.struct_size == 0;
}
" RAC_QHEXRT_HEADER_SURFACE_COMPATIBLE)
    if(NOT RAC_QHEXRT_HEADER_SURFACE_COMPATIBLE)
        message(FATAL_ERROR
            "QHexRT header does not provide the ABI surface required by the SDK adapter")
    endif()
    unset(RAC_QHEXRT_HEADER_SURFACE_COMPATIBLE CACHE)
endfunction()

# The public C ABI the SDK adapter links. One list, two inspectors: `nm` for the
# NDK's ELF archives, `dumpbin` for MSVC's COFF ones (MSVC ships no nm at all).
set(QHEXRT_REQUIRED_SYMBOLS
    qhx_runtime_create qhx_runtime_free qhx_runtime_device
    qhx_model_load qhx_model_free
    qhx_session_create qhx_session_free qhx_session_reset qhx_session_cancel
    qhx_gen_cfg_default qhx_generate_options_default
    qhx_generate qhx_generate_ex qhx_status_str qhx_version)

function(_qhexrt_require_lib_symbols archive)
    if(NOT QHEXRT_DUMPBIN)
        get_filename_component(_qhexrt_msvc_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
        find_program(QHEXRT_DUMPBIN NAMES dumpbin HINTS "${_qhexrt_msvc_bin}")
    endif()
    if(NOT QHEXRT_DUMPBIN)
        message(FATAL_ERROR
            "dumpbin is required to validate the QHexRT .lib ABI; set -DQHEXRT_DUMPBIN=<path>")
    endif()

    # /LINKERMEMBER:1 dumps the archive's own symbol directory, which lists only
    # DEFINED public symbols — the MSVC equivalent of `nm --defined-only`.
    # ARM64 COFF carries no leading-underscore decoration, so the C names match
    # the header verbatim.
    execute_process(
        COMMAND "${QHEXRT_DUMPBIN}" /NOLOGO /LINKERMEMBER:1 "${archive}"
        RESULT_VARIABLE _qhexrt_dumpbin_status
        OUTPUT_VARIABLE _qhexrt_dumpbin_output
        ERROR_VARIABLE _qhexrt_dumpbin_error)
    if(NOT _qhexrt_dumpbin_status EQUAL 0)
        message(FATAL_ERROR "Unable to inspect QHexRT archive symbols: ${_qhexrt_dumpbin_error}")
    endif()

    string(REPLACE "\r\n" "\n" _qhexrt_dumpbin_output "${_qhexrt_dumpbin_output}")
    foreach(_qhexrt_required_symbol IN LISTS QHEXRT_REQUIRED_SYMBOLS)
        # Anchored on both sides so qhx_generate does not match qhx_generate_ex.
        if(NOT _qhexrt_dumpbin_output MATCHES "[ \t]${_qhexrt_required_symbol}\n")
            message(FATAL_ERROR
                "QHexRT core archive omits required defined symbol: ${_qhexrt_required_symbol}")
        endif()
    endforeach()
endfunction()

function(_qhexrt_require_archive_symbols archive)
    if(MSVC)
        _qhexrt_require_lib_symbols("${archive}")
        return()
    endif()
    if(NOT CMAKE_NM OR NOT EXISTS "${CMAKE_NM}")
        message(FATAL_ERROR "CMAKE_NM is required to validate the QHexRT archive ABI")
    endif()

    execute_process(
        COMMAND "${CMAKE_NM}" --defined-only --extern-only "${archive}"
        RESULT_VARIABLE _qhexrt_nm_status
        OUTPUT_VARIABLE _qhexrt_nm_output
        ERROR_QUIET)
    if(NOT _qhexrt_nm_status EQUAL 0)
        execute_process(
            COMMAND "${CMAKE_NM}" -g "${archive}"
            RESULT_VARIABLE _qhexrt_nm_status
            OUTPUT_VARIABLE _qhexrt_nm_output
            ERROR_VARIABLE _qhexrt_nm_error)
    endif()
    if(NOT _qhexrt_nm_status EQUAL 0)
        message(FATAL_ERROR "Unable to inspect QHexRT archive symbols: ${_qhexrt_nm_error}")
    endif()

    string(REPLACE "\r\n" "\n" _qhexrt_nm_output "${_qhexrt_nm_output}")
    string(REPLACE "\n" ";" _qhexrt_nm_lines "${_qhexrt_nm_output}")
    foreach(_qhexrt_required_symbol IN LISTS QHEXRT_REQUIRED_SYMBOLS)
        set(_qhexrt_symbol_defined FALSE)
        foreach(_qhexrt_nm_line IN LISTS _qhexrt_nm_lines)
            if(_qhexrt_nm_line MATCHES
               "[ \t]([A-Za-z?])[ \t]+${_qhexrt_required_symbol}$" AND
               NOT CMAKE_MATCH_1 MATCHES "[uU?]")
                set(_qhexrt_symbol_defined TRUE)
                break()
            endif()
        endforeach()
        if(NOT _qhexrt_symbol_defined)
            message(FATAL_ERROR
                "QHexRT core archive omits required defined symbol: ${_qhexrt_required_symbol}")
        endif()
    endforeach()
endfunction()

get_filename_component(RAC_COMMONS_ROOT_DIR
    "${CMAKE_CURRENT_SOURCE_DIR}/../../core" ABSOLUTE)

# Prebuilt QHexRT tree. Default to the gitignored in-tree staging dir so an
# authorized checkout that ran QHexRT's verified staging script "just works".
# The script publishes immutable versions and atomically switches `current`;
# resolving it here prevents a later switch from mixing inputs in one build.
set(_qhexrt_root_explicit FALSE)
if(NOT DEFINED QHEXRT_ROOT)
    set(QHEXRT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/prebuilt/current")
else()
    set(_qhexrt_root_explicit TRUE)
endif()
set(_qhexrt_root_selected FALSE)
if(_qhexrt_root_explicit OR EXISTS "${QHEXRT_ROOT}" OR IS_SYMLINK "${QHEXRT_ROOT}")
    set(_qhexrt_root_selected TRUE)
endif()
if(NOT _qhexrt_root_explicit AND EXISTS "${QHEXRT_ROOT}" AND NOT IS_SYMLINK "${QHEXRT_ROOT}")
    message(FATAL_ERROR "Default QHexRT prebuilt/current must be an atomic symlink")
endif()
if(NOT _qhexrt_root_explicit AND IS_SYMLINK "${QHEXRT_ROOT}")
    file(READ_SYMLINK "${QHEXRT_ROOT}" _qhexrt_current_target)
    get_filename_component(_qhexrt_current_parent "${QHEXRT_ROOT}" DIRECTORY)
    # Windows selects with a junction, because the runner service account cannot
    # create symlinks. CMake does report a junction as IS_SYMLINK, but
    # READ_SYMLINK hands back its ABSOLUTE target (optionally \\?\-prefixed),
    # so normalize to the relative form both platforms then share.
    # scripts/build/_selection.py is the reference implementation.
    string(REPLACE "\\" "/" _qhexrt_current_target "${_qhexrt_current_target}")
    string(REGEX REPLACE "^//[?.]/" "" _qhexrt_current_target "${_qhexrt_current_target}")
    # Check the last two components rather than converting to a relative path:
    # file(RELATIVE_PATH) rejects a drive-letter path on a POSIX host, and
    # IS_ABSOLUTE is host-relative too, so either would leave this branch
    # untestable off-Windows. The path actually used below is REBUILT from
    # _qhexrt_current_parent and the receipt name, never the raw link text, so a
    # target pointing outside this tree cannot smuggle in a different payload.
    get_filename_component(_qhexrt_current_name "${_qhexrt_current_target}" NAME)
    get_filename_component(_qhexrt_current_dir "${_qhexrt_current_target}" DIRECTORY)
    get_filename_component(_qhexrt_current_dir_name "${_qhexrt_current_dir}" NAME)
    string(LENGTH "${_qhexrt_current_name}" _qhexrt_current_name_length)
    if(NOT _qhexrt_current_dir_name STREQUAL "versions" OR
       NOT _qhexrt_current_name MATCHES "^[0-9a-f]+$" OR
       NOT _qhexrt_current_name_length EQUAL 64)
        message(FATAL_ERROR
            "QHexRT current must directly name versions/<64-hex-receipt>, got: "
            "${_qhexrt_current_target}")
    endif()
    set(_qhexrt_selected_entry
        "${_qhexrt_current_parent}/versions/${_qhexrt_current_name}")
    if(IS_SYMLINK "${_qhexrt_selected_entry}" OR NOT IS_DIRECTORY "${_qhexrt_selected_entry}")
        message(FATAL_ERROR "QHexRT current target must be a real immutable version directory")
    endif()
endif()
if(IS_SYMLINK "${QHEXRT_ROOT}" AND NOT EXISTS "${QHEXRT_ROOT}")
    message(FATAL_ERROR "QHexRT current prebuilt symlink is broken: ${QHEXRT_ROOT}")
endif()
if(_qhexrt_root_explicit AND NOT EXISTS "${QHEXRT_ROOT}")
    message(FATAL_ERROR "Explicit QHEXRT_ROOT does not exist: ${QHEXRT_ROOT}")
endif()
if(NOT _qhexrt_root_explicit AND NOT _qhexrt_root_selected)
    get_filename_component(_qhexrt_prebuilt_parent "${QHEXRT_ROOT}" DIRECTORY)
    if(EXISTS "${_qhexrt_prebuilt_parent}/include" OR IS_SYMLINK "${_qhexrt_prebuilt_parent}/include" OR
       EXISTS "${_qhexrt_prebuilt_parent}/lib" OR IS_SYMLINK "${_qhexrt_prebuilt_parent}/lib" OR
       EXISTS "${_qhexrt_prebuilt_parent}/qhexrt-prebuilt.json" OR
       IS_SYMLINK "${_qhexrt_prebuilt_parent}/qhexrt-prebuilt.json")
        message(FATAL_ERROR
            "Legacy QHexRT prebuilt payload exists without current; rerun stage_prebuilt_for_sdk.sh")
    endif()
endif()
if(EXISTS "${QHEXRT_ROOT}")
    file(REAL_PATH "${QHEXRT_ROOT}" QHEXRT_ROOT)
endif()
if(NOT _qhexrt_root_explicit AND _qhexrt_root_selected)
    set(_qhexrt_versions "${CMAKE_CURRENT_SOURCE_DIR}/prebuilt/versions")
    if(IS_SYMLINK "${_qhexrt_versions}" OR NOT IS_DIRECTORY "${_qhexrt_versions}")
        message(FATAL_ERROR "QHexRT prebuilt/versions must be a real directory")
    endif()
    file(REAL_PATH "${_qhexrt_versions}" _qhexrt_versions_real)
    get_filename_component(_qhexrt_selected_parent "${QHEXRT_ROOT}" DIRECTORY)
    if(NOT _qhexrt_selected_parent STREQUAL _qhexrt_versions_real)
        message(FATAL_ERROR "QHexRT current must select a direct child of prebuilt/versions")
    endif()
endif()

# Target selection: which hosts can actually drive the Hexagon NPU, and what the
# prebuilt tree is named for each. Off-platform builds keep the Android naming so
# the shell's diagnostics and paths stay well-defined; they never read the files.
set(_qhexrt_platform_supported FALSE)
set(_qhexrt_core_name "libqhexrt_core.a")
set(_qhexrt_host_name "libqhexrt_host.a")
if(RAC_PLATFORM_ANDROID)
    # ABI subdir under QHEXRT_ROOT/lib (arm64-v8a is the shipped Snapdragon target).
    set(_qhexrt_abi "${ANDROID_ABI}")
    if(NOT _qhexrt_abi)
        set(_qhexrt_abi "${CMAKE_ANDROID_ARCH_ABI}")
    endif()
    if(_qhexrt_abi STREQUAL "arm64-v8a")
        set(_qhexrt_platform_supported TRUE)
    endif()
elseif(RAC_PLATFORM_WINDOWS AND
       (CMAKE_SYSTEM_PROCESSOR MATCHES "^([Aa][Rr][Mm]64|aarch64|AARCH64)$" OR
        CMAKE_GENERATOR_PLATFORM MATCHES "^([Aa][Rr][Mm]64)$"))
    # Snapdragon X / X2 Elite. MSVC emits ARM64 COFF `.lib`, not ELF `.a`.
    set(_qhexrt_platform_supported TRUE)
    set(_qhexrt_abi "win-arm64")
    set(_qhexrt_core_name "qhexrt_core.lib")
    set(_qhexrt_host_name "qhexrt_host.lib")
else()
    set(_qhexrt_abi "arm64-v8a")
endif()

set(QHEXRT_SOURCES
    rac_plugin_entry_qhexrt.cpp
    rac_backend_qhexrt_register.cpp
    qhexrt_device_capability.cpp)
set(QHEXRT_LINK_LIBS "")
set(QHEXRT_COMPILE_DEFS RAC_QHEXRT_BUILDING)
set(QHEXRT_INCLUDE_DIRS
    ${RAC_COMMONS_ROOT_DIR}/include
    ${RAC_COMMONS_ROOT_DIR}/include/rac/backends
    ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_SOURCE_DIR}/..)  # engines/ — for engines/common/rac_engine_unavailable.h

# QHexRT exposes existing generated messages only as serialized byte buffers.
# The C++ generated/runtime dependencies stay private to the engine build.
if(RAC_PROTOBUF_RUNTIME_ENABLED)
    list(APPEND QHEXRT_INCLUDE_DIRS
        ${RAC_COMMONS_ROOT_DIR}/src/generated/proto)
    list(APPEND QHEXRT_COMPILE_DEFS RAC_QHEXRT_HAVE_PROTOBUF=1)
    if(TARGET protobuf::libprotobuf)
        list(APPEND QHEXRT_LINK_LIBS protobuf::libprotobuf)
    elseif(Protobuf_LIBRARIES)
        list(APPEND QHEXRT_LINK_LIBS ${Protobuf_LIBRARIES})
        list(APPEND QHEXRT_INCLUDE_DIRS ${Protobuf_INCLUDE_DIRS})
    endif()
endif()

# Discover the prebuilt archive + private header. Once a current/explicit root
# is selected, all payloads must be present and valid; only a wholly absent
# default current selection builds the public stub. Explicit
# paths (not find_library): the Android NDK toolchain re-roots find_library
# PATHS under the sysroot (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY), which would
# never see this out-of-sysroot prebuilt tree.
set(QHEXRT_CORE_LIB "${QHEXRT_ROOT}/lib/${_qhexrt_abi}/${_qhexrt_core_name}")
set(QHEXRT_HOST_LIB "${QHEXRT_ROOT}/lib/${_qhexrt_abi}/${_qhexrt_host_name}")
set(QHEXRT_HEADER "${QHEXRT_ROOT}/include/qhexrt/qhexrt_c.h")
set(QHEXRT_IDENTITY_MANIFEST "${QHEXRT_ROOT}/qhexrt-prebuilt.json")
set(QHEXRT_BUILD_RECEIPT "${QHEXRT_ROOT}/qhexrt-build-receipt.json")
set(_qhexrt_prebuilt_valid FALSE)

if(_qhexrt_platform_supported AND _qhexrt_root_selected)
    if(NOT EXISTS "${QHEXRT_CORE_LIB}" OR NOT EXISTS "${QHEXRT_HOST_LIB}" OR
       NOT EXISTS "${QHEXRT_HEADER}" OR NOT EXISTS "${QHEXRT_IDENTITY_MANIFEST}" OR
       NOT EXISTS "${QHEXRT_BUILD_RECEIPT}")
        message(FATAL_ERROR "Partial QHexRT prebuilt under ${QHEXRT_ROOT}; refusing a mixed/unfinished payload")
    endif()
    foreach(_qhexrt_no_symlink IN ITEMS
        "${QHEXRT_ROOT}/include"
        "${QHEXRT_ROOT}/include/qhexrt"
        "${QHEXRT_HEADER}"
        "${QHEXRT_ROOT}/lib"
        "${QHEXRT_ROOT}/lib/${_qhexrt_abi}"
        "${QHEXRT_CORE_LIB}"
        "${QHEXRT_HOST_LIB}"
        "${QHEXRT_IDENTITY_MANIFEST}"
        "${QHEXRT_BUILD_RECEIPT}")
        if(IS_SYMLINK "${_qhexrt_no_symlink}")
            message(FATAL_ERROR "QHexRT selected prebuilt contains a symlink: ${_qhexrt_no_symlink}")
        endif()
    endforeach()
    file(READ "${QHEXRT_IDENTITY_MANIFEST}" _qhexrt_identity_json)
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest"
        "android_abi,build,build_receipt_sha256,c_abi,files,qhexrt_version,schema,source")
    string(JSON _qhexrt_identity_schema ERROR_VARIABLE _qhexrt_json_error
        GET "${_qhexrt_identity_json}" schema)
    if(NOT _qhexrt_json_error STREQUAL "NOTFOUND" OR
       NOT _qhexrt_identity_schema STREQUAL "qhexrt-prebuilt/v2")
        message(FATAL_ERROR "Invalid QHexRT identity manifest schema: ${_qhexrt_json_error}")
    endif()
    string(JSON _qhexrt_manifest_abi GET "${_qhexrt_identity_json}" android_abi)
    if(NOT _qhexrt_manifest_abi STREQUAL _qhexrt_abi)
        message(FATAL_ERROR
            "QHexRT prebuilt ABI mismatch: manifest=${_qhexrt_manifest_abi} build=${_qhexrt_abi}")
    endif()
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.c_abi" "major,minor" c_abi)
    string(JSON _qhexrt_abi_major_type TYPE "${_qhexrt_identity_json}" c_abi major)
    string(JSON _qhexrt_abi_minor_type TYPE "${_qhexrt_identity_json}" c_abi minor)
    string(JSON _qhexrt_abi_major GET "${_qhexrt_identity_json}" c_abi major)
    string(JSON _qhexrt_abi_minor GET "${_qhexrt_identity_json}" c_abi minor)
    if(NOT _qhexrt_abi_major_type STREQUAL "NUMBER" OR
       NOT _qhexrt_abi_minor_type STREQUAL "NUMBER" OR
       NOT _qhexrt_abi_major STREQUAL "1" OR
       NOT _qhexrt_abi_minor MATCHES "^[0-9]+$")
        message(FATAL_ERROR "QHexRT prebuilt has an incompatible C ABI object")
    endif()
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.source"
        "git_dirty,git_sha,state_sha256" source)
    # The receipt's `build` object differs by toolchain: an MSVC build binds no
    # NDK, and its archive evidence is a COFF parse rather than llvm-ar/readelf
    # hashes. Everything else is identical.
    if(_qhexrt_abi STREQUAL "win-arm64")
        _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build"
            "android_abi,archive_evidence,build_type,cmake_cache_sha256,cmake_system,compiler,qnn_sdk" build)
    else()
        _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build"
            "android_abi,archive_evidence,build_type,cmake_cache_sha256,cmake_system,compiler,ndk,qnn_sdk" build)
        _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build.ndk"
            "metadata_file,metadata_sha256" build ndk)
    endif()
    string(JSON _qhexrt_build_abi GET "${_qhexrt_identity_json}" build android_abi)
    if(NOT _qhexrt_build_abi STREQUAL _qhexrt_abi)
        message(FATAL_ERROR "QHexRT receipt build ABI does not match the selected ABI")
    endif()
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build.cmake_system"
        "crosscompiling,name,processor,state_file_sha256" build cmake_system)
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build.compiler"
        "id,sha256,state_file_sha256,version,version_output_sha256" build compiler)
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build.qnn_sdk"
        "metadata_file,metadata_sha256" build qnn_sdk)
    if(_qhexrt_abi STREQUAL "win-arm64")
        _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build.archive_evidence"
            "coff_reader,core,host" build archive_evidence)
    else()
        _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build.archive_evidence"
            "core,host,llvm_ar_sha256,llvm_readelf_sha256" build archive_evidence)
    endif()
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build.archive_evidence.core"
        "member_count" build archive_evidence core)
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.build.archive_evidence.host"
        "member_count" build archive_evidence host)
    _qhexrt_require_json_keys(_qhexrt_identity_json "manifest.files"
        "include/qhexrt/qhexrt_c.h,lib/${_qhexrt_abi}/${_qhexrt_core_name},lib/${_qhexrt_abi}/${_qhexrt_host_name}" files)
    string(JSON _qhexrt_receipt_sha ERROR_VARIABLE _qhexrt_receipt_error
        GET "${_qhexrt_identity_json}" build_receipt_sha256)
    string(LENGTH "${_qhexrt_receipt_sha}" _qhexrt_receipt_sha_length)
    get_filename_component(_qhexrt_version_name "${QHEXRT_ROOT}" NAME)
    if(NOT _qhexrt_receipt_error STREQUAL "NOTFOUND" OR
       NOT _qhexrt_receipt_sha MATCHES "^[0-9a-f]+$" OR
       NOT _qhexrt_receipt_sha_length EQUAL 64 OR
       NOT _qhexrt_version_name STREQUAL _qhexrt_receipt_sha)
        message(FATAL_ERROR "QHexRT prebuilt version directory does not match its build receipt identity")
    endif()
    file(SHA256 "${QHEXRT_BUILD_RECEIPT}" _qhexrt_actual_receipt_sha)
    if(NOT _qhexrt_actual_receipt_sha STREQUAL _qhexrt_receipt_sha)
        message(FATAL_ERROR "QHexRT sanitized build receipt hash does not match the selected version")
    endif()
    file(READ "${QHEXRT_BUILD_RECEIPT}" _qhexrt_receipt_json)
    _qhexrt_require_json_keys(_qhexrt_receipt_json "sanitized receipt"
        "artifacts,build,c_abi,qhexrt_version,schema,source")
    string(JSON _qhexrt_receipt_schema ERROR_VARIABLE _qhexrt_receipt_schema_error
        GET "${_qhexrt_receipt_json}" schema)
    if(NOT _qhexrt_receipt_schema_error STREQUAL "NOTFOUND" OR
       NOT _qhexrt_receipt_schema STREQUAL "qhexrt-build-receipt/v1")
        message(FATAL_ERROR "Invalid QHexRT sanitized build receipt schema")
    endif()
    foreach(_qhexrt_receipt_field IN ITEMS qhexrt_version c_abi source build)
        string(JSON _qhexrt_from_receipt GET "${_qhexrt_receipt_json}" "${_qhexrt_receipt_field}")
        string(JSON _qhexrt_from_manifest GET "${_qhexrt_identity_json}" "${_qhexrt_receipt_field}")
        if(NOT _qhexrt_from_receipt STREQUAL _qhexrt_from_manifest)
            message(FATAL_ERROR
                "QHexRT manifest field ${_qhexrt_receipt_field} does not derive from sanitized receipt")
        endif()
    endforeach()
    string(JSON _qhexrt_from_receipt GET "${_qhexrt_receipt_json}" artifacts)
    string(JSON _qhexrt_from_manifest GET "${_qhexrt_identity_json}" files)
    if(NOT _qhexrt_from_receipt STREQUAL _qhexrt_from_manifest)
        message(FATAL_ERROR "QHexRT manifest file inventory does not derive from sanitized receipt")
    endif()
    foreach(_qhexrt_relative IN ITEMS
        "include/qhexrt/qhexrt_c.h"
        "lib/${_qhexrt_abi}/${_qhexrt_core_name}"
        "lib/${_qhexrt_abi}/${_qhexrt_host_name}")
        set(_qhexrt_path "${QHEXRT_ROOT}/${_qhexrt_relative}")
        _qhexrt_require_json_keys(_qhexrt_identity_json
            "manifest.files.${_qhexrt_relative}" "sha256,size_bytes" files "${_qhexrt_relative}")
        string(JSON _qhexrt_expected_sha ERROR_VARIABLE _qhexrt_sha_error
            GET "${_qhexrt_identity_json}" files "${_qhexrt_relative}" sha256)
        string(JSON _qhexrt_expected_size ERROR_VARIABLE _qhexrt_size_error
            GET "${_qhexrt_identity_json}" files "${_qhexrt_relative}" size_bytes)
        if(NOT _qhexrt_sha_error STREQUAL "NOTFOUND" OR
           NOT _qhexrt_size_error STREQUAL "NOTFOUND")
            message(FATAL_ERROR "QHexRT manifest omits identity for ${_qhexrt_relative}")
        endif()
        string(JSON _qhexrt_sha_type TYPE "${_qhexrt_identity_json}" files "${_qhexrt_relative}" sha256)
        string(JSON _qhexrt_size_type TYPE "${_qhexrt_identity_json}" files "${_qhexrt_relative}" size_bytes)
        string(LENGTH "${_qhexrt_expected_sha}" _qhexrt_expected_sha_length)
        if(NOT _qhexrt_sha_type STREQUAL "STRING" OR
           NOT _qhexrt_size_type STREQUAL "NUMBER" OR
           NOT _qhexrt_expected_sha MATCHES "^[0-9a-f]+$" OR
           NOT _qhexrt_expected_sha_length EQUAL 64 OR
           NOT _qhexrt_expected_size MATCHES "^[0-9]+$" OR
           _qhexrt_expected_size LESS_EQUAL 0)
            message(FATAL_ERROR "QHexRT manifest has invalid file identity for ${_qhexrt_relative}")
        endif()
        file(SHA256 "${_qhexrt_path}" _qhexrt_actual_sha)
        file(SIZE "${_qhexrt_path}" _qhexrt_actual_size)
        if(NOT _qhexrt_actual_sha STREQUAL _qhexrt_expected_sha OR
           NOT _qhexrt_actual_size EQUAL _qhexrt_expected_size)
            message(FATAL_ERROR "QHexRT prebuilt identity mismatch for ${_qhexrt_relative}")
        endif()
    endforeach()
    _qhexrt_validate_header_surface(
        "${QHEXRT_ROOT}/include" "${_qhexrt_abi_major}" "${_qhexrt_abi_minor}")
    _qhexrt_require_archive_symbols("${QHEXRT_CORE_LIB}")
    set(_qhexrt_prebuilt_valid TRUE)
endif()

if(_qhexrt_platform_supported AND _qhexrt_prebuilt_valid)
    set(RAC_QHEXRT_ENGINE_AVAILABLE 1)
    message(STATUS "  QHexRT engine discovered:")
    message(STATUS "    header: ${QHEXRT_HEADER}")
    message(STATUS "    core:   ${QHEXRT_CORE_LIB}")
    message(STATUS "    host:   ${QHEXRT_HOST_LIB}")

    list(APPEND QHEXRT_SOURCES
        qhexrt_session.cpp
        qhexrt_llm_ops.cpp
        qhexrt_vlm_ops.cpp
        qhexrt_stt_ops.cpp
        qhexrt_tts_ops.cpp
        qhexrt_embeddings_ops.cpp
        qhexrt_diffusion_ops.cpp)
    list(APPEND QHEXRT_INCLUDE_DIRS "${QHEXRT_ROOT}/include")
    list(APPEND QHEXRT_COMPILE_DEFS RAC_QHEXRT_ENGINE_AVAILABLE=1)

    # core references host symbols. GNU ld needs an explicit link group for that
    # to resolve order-independently; MSVC's linker already resolves statics
    # across the whole input set and rejects the -Wl form. dl + pthread are
    # QHexRT's own deps (both no-ops on Windows).
    if(MSVC)
        list(APPEND QHEXRT_LINK_LIBS "${QHEXRT_CORE_LIB}" "${QHEXRT_HOST_LIB}")
    else()
        list(APPEND QHEXRT_LINK_LIBS
            -Wl,--start-group "${QHEXRT_CORE_LIB}" "${QHEXRT_HOST_LIB}" -Wl,--end-group
            ${CMAKE_DL_LIBS})
    endif()
    find_package(Threads REQUIRED)
    list(APPEND QHEXRT_LINK_LIBS Threads::Threads)

    # A Bonsai-enabled core archive pulls in the FastRPC transport stub
    # (run_main_on_hexagon_stub.c + bonsai_npu.cpp): rpcmem_* / remote_handle64_* /
    # remote_session_control, provided by libcdsprpc.so — a /vendor system lib on
    # device that the app already bundles in jniLibs. Detect the stub in the
    # prebuilt core and link the matching cdsprpc stub so the static link resolves;
    # the real vendor lib backs it at runtime. Override with -DQHEXRT_LIBCDSPRPC=.
    #
    # Android only: Windows on ARM64 has no FastRPC/rpcmem at all, so a
    # Windows-built core never carries the stub and there is nothing to resolve.
    set(_qhexrt_core_fastrpc "")
    if(RAC_PLATFORM_ANDROID)
        file(STRINGS "${QHEXRT_CORE_LIB}" _qhexrt_core_fastrpc
            REGEX "remote_handle64_open" LIMIT_COUNT 1)
    endif()
    if(_qhexrt_core_fastrpc)
        if(NOT QHEXRT_LIBCDSPRPC)
            find_file(QHEXRT_LIBCDSPRPC libcdsprpc.so
                PATHS
                    "$ENV{HEXAGON_SDK_ROOT}/ipc/fastrpc/remote/ship/android_aarch64"
                    "${RAC_ROOT_DIR}/../hexagon-sdk/root/Hexagon_SDK/6.6.0.0/ipc/fastrpc/remote/ship/android_aarch64"
                    "${CMAKE_CURRENT_SOURCE_DIR}/../../bindings/kotlin/src/main/jniLibs/arm64-v8a"
                NO_DEFAULT_PATH)
        endif()
        if(QHEXRT_LIBCDSPRPC AND EXISTS "${QHEXRT_LIBCDSPRPC}")
            message(STATUS "  QHexRT: Bonsai FastRPC core -> linking libcdsprpc: ${QHEXRT_LIBCDSPRPC}")
            list(APPEND QHEXRT_LINK_LIBS "${QHEXRT_LIBCDSPRPC}")
        else()
            message(FATAL_ERROR
                "QHexRT prebuilt core needs FastRPC (libcdsprpc.so) but none was found; "
                "set -DQHEXRT_LIBCDSPRPC=/path/to/libcdsprpc.so")
        endif()
    endif()
else()
    set(RAC_QHEXRT_ENGINE_AVAILABLE 0)
    list(APPEND QHEXRT_COMPILE_DEFS RAC_QHEXRT_ENGINE_AVAILABLE=0)
    if(NOT _qhexrt_platform_supported)
        message(STATUS "  QHexRT engine runs on Snapdragon Android and Windows on ARM64; "
                       "building shell for ${RAC_PLATFORM_NAME}")
    else()
        message(STATUS "  QHexRT engine archive absent under QHEXRT_ROOT=${QHEXRT_ROOT}")
    endif()
    message(STATUS "  -> building not-routable shell (rejects with RAC_ERROR_BACKEND_UNAVAILABLE)")
endif()

option(RAC_REQUIRE_QHEXRT_ENGINE
    "Fail configure unless a real QHexRT prebuilt is present (desktop overlay CI)"
    OFF)
if(RAC_REQUIRE_QHEXRT_ENGINE AND NOT RAC_QHEXRT_ENGINE_AVAILABLE)
    message(FATAL_ERROR
        "RAC_REQUIRE_QHEXRT_ENGINE=ON but QHexRT is not routable "
        "(QHEXRT_ROOT=${QHEXRT_ROOT}). "
        "Run: NEURUN_TOKEN=... scripts/build/download-qhexrt.sh --abi win-arm64")
endif()

rac_add_engine_plugin(qhexrt
    TARGET_NAME         rac_backend_qhexrt
    CXX_STANDARD        17
    SHARED_ONLY
    SOURCES             ${QHEXRT_SOURCES}
    LINK_LIBRARIES      ${QHEXRT_LINK_LIBS}
    INCLUDE_DIRECTORIES ${QHEXRT_INCLUDE_DIRS}
    COMPILE_DEFINITIONS ${QHEXRT_COMPILE_DEFS}
)

# Where commons' bundled static deps live.
#
# rac_commons carries `zlibstatic` (and the other bundled archives) into its
# INTERFACE link line as a BARE NAME, not a target — core's
# own CMakeLists flags this, because feeding the logical target name through
# find_package cache variables is what libarchive's internal find_package(ZLIB)
# demands. It resolves the name with a directory-scoped
# `link_directories("${RAC_BUNDLED_LIB_DIR}")`, and directory scope reaches only
# that directory and its children. `engines/` is a SIBLING of
# `core/` (the same scope gap the MSVC block below documents),
# so an engine inherits the bare name without the directory that resolves it and
# the link dies on `LNK1181: cannot open input file 'zlibstatic.lib'`.
#
# On ELF/Mach-O this stays hidden: the bundled archives are already folded into
# librac_commons and the linker never has to find them again.
target_link_directories(rac_backend_qhexrt PRIVATE "${CMAKE_BINARY_DIR}/_bundled_libs")

# =============================================================================
# Loadable-plugin file name
# =============================================================================
# The FILE NAME is a contract, not cosmetics. `rac_registry_load_plugin()`
# derives the symbol it resolves from the file name:
# `entry_symbol_from_path()` (plugin_loader.cpp) drops the directory, a `lib`
# prefix, the extension and a `runanywhere_` prefix, then prepends
# `rac_plugin_entry_`. A plugin shipped as `rac_backend_qhexrt.dll` therefore
# makes the loader look up `rac_plugin_entry_rac_backend_qhexrt`, which nothing
# defines — the load fails on a DLL that is otherwise perfectly good. The
# Electron backend package resolves the same stem
# (`pluginLibraryFileName()` in src/backend/plugin-registry.ts).
#
# QHexRT already satisfies the rest of the contract: `rac_plugin_entry_qhexrt.cpp`
# is in QHEXRT_SOURCES, and the SHARED build gets `RAC_BUILDING_PLUGIN=1` from
# rac_add_engine_plugin, so the entry is exported. Only the name was wrong, so
# renaming the OUTPUT is the whole fix.
#
# The llamacpp / onnx / sherpa pattern — a separate `runanywhere_<id>` carrier
# that links the engine — does NOT work here, and the reason is worth recording:
# the entry's vtable references the six per-modality op tables
# (`g_qhexrt_llm_ops` and friends) as DATA. A carrier in its own DLL has to
# import those across a DLL boundary, and on MSVC an imported data symbol only
# resolves through `__declspec(dllimport)` on the declaration — the declarations
# here are plain `extern "C" const`, shared with the ELF/Mach-O builds where no
# such annotation exists. `WINDOWS_EXPORT_ALL_SYMBOLS` does not rescue it: the
# generated .def exports the data, but the consumer still emits a reference to
# the plain name while the import library only offers `__imp_`, so the link dies
# on six unresolved externals. Keeping the entry in the same binary as its
# vtables sidesteps the boundary entirely.
#
# The TARGET keeps its name (`rac_backend_qhexrt`): the Android Kotlin QHexRT
# module and its JNI bridge load `librac_backend_qhexrt.so` by that name, and 50+
# in-tree references name the target. Only the emitted file is renamed, and only
# where a loader reads it.
if(RAC_STATIC_PLUGINS)
    target_sources(rac_commons PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/rac_static_register_qhexrt.cpp
    )
    target_include_directories(rac_commons PRIVATE
        ${RAC_COMMONS_ROOT_DIR}/include
    )
elseif(RAC_BUILD_SHARED AND NOT RAC_PLATFORM_ANDROID)
    set_target_properties(rac_backend_qhexrt PROPERTIES OUTPUT_NAME runanywhere_qhexrt)

    # Keep the export table to what is actually declared public.
    #
    # rac_add_engine_plugin turns WINDOWS_EXPORT_ALL_SYMBOLS ON for every
    # SHARED_ONLY engine on Windows, because the OTHER engines ship a separate
    # runanywhere_<id> carrier that has to resolve their op-vtable globals across
    # a DLL boundary. QHexRT ships no carrier — this library IS the plugin — and
    # on Windows nothing links against it at all: the loader opens it with
    # LoadLibrary and asks for one symbol. Every consumer that LINKS
    # rac_backend_qhexrt (the JNI bridge, the React Native package) is Android,
    # where the property does not apply.
    #
    # So export-all buys nothing here and costs plenty. Measured on this target,
    # win-arm64 Release: ON exports 981 symbols, most of them mangled MSVC STL
    # internals (_Compressed_pair, basic_string, filesystem::path) dragged in from
    # a DLL that statically links the closed-source QHexRT runtime; OFF exports
    # 10 — rac_plugin_entry_qhexrt plus the nine RAC_QHEXRT_API entry points
    # rac_qhexrt.h deliberately publishes. ON also emits LNK4286 (rac_qhexrt_probe
    # defined in one object, *imported* by another), so it degrades the DLL's own
    # internal linkage on the way past.
    if(WIN32)
        set_target_properties(rac_backend_qhexrt PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS OFF)
    endif()
endif()

if(RAC_PLATFORM_ANDROID)
    target_link_libraries(rac_backend_qhexrt PRIVATE log)
    target_compile_options(rac_backend_qhexrt PRIVATE -O3 -ffunction-sections -fdata-sections)
    target_link_options(rac_backend_qhexrt PRIVATE
        -Wl,--gc-sections
        -Wl,-z,max-page-size=16384
        -Wl,-z,common-page-size=16384)
elseif(MSVC)
    # engines/ is a sibling of core/, so it does NOT inherit
    # that directory's MSVC defaults. <mutex>/<filesystem> need an exception
    # model, and the sources are UTF-8.
    target_compile_options(rac_backend_qhexrt PRIVATE /EHsc /Zc:__cplusplus /utf-8)
endif()

# =============================================================================
# JNI bridge (Android) — librac_backend_qhexrt_jni.so
#
# Standalone JNI carrier the Kotlin runanywhere-core-qhexrt module loads. Owns
# JNI_OnLoad; nativeRegister calls rac_backend_qhexrt_register(); nativeProbeNpu
# calls the engine-owned rac_qhexrt_probe_proto(). Builds in
# both stub and linked modes; in stub mode register reports BACKEND_UNAVAILABLE.
# =============================================================================
if(RAC_PLATFORM_ANDROID AND RAC_BUILD_SHARED AND RAC_BUILD_JNI)
    if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
        set(_QHEXRT_NDK_HOST_TAG "darwin-x86_64")
    elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
        set(_QHEXRT_NDK_HOST_TAG "linux-x86_64")
    elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
        set(_QHEXRT_NDK_HOST_TAG "windows-x86_64")
    else()
        message(FATAL_ERROR "Unsupported host for QHexRT JNI bridge: ${CMAKE_HOST_SYSTEM_NAME}")
    endif()

    set(_QHEXRT_NDK_SYSROOT_INCLUDE
        "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${_QHEXRT_NDK_HOST_TAG}/sysroot/usr/include")
    if(NOT EXISTS "${_QHEXRT_NDK_SYSROOT_INCLUDE}/jni.h")
        message(FATAL_ERROR "Could not locate Android NDK JNI headers at ${_QHEXRT_NDK_SYSROOT_INCLUDE}")
    endif()

    add_library(rac_backend_qhexrt_jni SHARED jni/rac_backend_qhexrt_jni.cpp)
    target_include_directories(rac_backend_qhexrt_jni PRIVATE
        ${_QHEXRT_NDK_SYSROOT_INCLUDE}
        ${_QHEXRT_NDK_SYSROOT_INCLUDE}/android
        ${CMAKE_CURRENT_SOURCE_DIR}
        ${CMAKE_CURRENT_SOURCE_DIR}/include
        ${RAC_COMMONS_ROOT_DIR}/include)
    target_compile_definitions(rac_backend_qhexrt_jni PRIVATE
        RAC_QHEXRT_VERSION="${RAC_VERSION}")
    target_link_libraries(rac_backend_qhexrt_jni PRIVATE rac_backend_qhexrt log)
    target_compile_options(rac_backend_qhexrt_jni PRIVATE
        -O3 -fvisibility=hidden -ffunction-sections -fdata-sections)
    target_link_options(rac_backend_qhexrt_jni PRIVATE
        -Wl,--gc-sections
        -Wl,-z,max-page-size=16384
        -Wl,-z,common-page-size=16384)
endif()

message(STATUS "QHexRT Backend Configuration:")
message(STATUS "  Engine available: ${RAC_QHEXRT_ENGINE_AVAILABLE}")
message(STATUS "  Platform:         ${RAC_PLATFORM_NAME}")
message(STATUS "  NPU host:         ${_qhexrt_platform_supported}")
message(STATUS "  Prebuilt ABI:     ${_qhexrt_abi}")
