cmake_minimum_required(VERSION 3.20) project(joc_core VERSION 0.1.0 LANGUAGES C CXX) # --------------------------------------------------------------------------- # Products # joc_core shared library: the execution core (E-AC-3/EMDF/JOC/OAMD # bitstream, DSP, speaker and binaural rendering, ADM-BWF/WAV # writing, file task and the streaming push/pull surface) # joc_cli command line frontend for file tasks # # Public headers: include/joc_core.h engine, telemetry and file task # include/joc_stream.h embedder-facing streaming surface # # Options # JOC_BUILD_TESTS unit tests (CTest), on by default # JOC_BUILD_DEVTOOLS in-tree verification tools, off: those sources live in # devtools/, which is not part of the repository # JOC_ENABLE_AVX2 build the runtime-dispatched AVX2 kernels, on by default # JOC_ENABLE_AVX512 build the runtime-dispatched AVX-512 kernels, on by default # # Only the MSVC toolchain is validated locally; other platforms are built by CI. # The floating-point flags of the original native library are preserved on # purpose: /fp:precise on MSVC, -fno-fast-math elsewhere, and a static CRT so # that no redistributable is required. # --------------------------------------------------------------------------- option(JOC_BUILD_TESTS "Build the unit tests" ON) option(JOC_BUILD_DEVTOOLS "Build the in-tree verification tools (needs devtools/)" OFF) # The DSP kernels are dispatched at run time (see src/simd/simd.h): the # baseline units stay on the ISA every x86-64 CPU has, and these two options # decide whether the wider units are linked in at all. Both are on by default. # The instruction set actually executed is chosen from CPUID/XGETBV (or the # AArch64 baseline) when the library is first used, so a binary carrying the # AVX-512 unit still runs on a CPU without it, and JOC_SIMD=scalar|sse2|avx2| # avx512|neon pins one tier for verification. option(JOC_ENABLE_AVX2 "Build the runtime-dispatched AVX2 kernels" ON) option(JOC_ENABLE_AVX512 "Build the runtime-dispatched AVX-512 kernels" ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif() find_package(Threads REQUIRED) # Ninja learns MSVC's header dependencies by parsing the compiler's /showIncludes # notes, and it only recognises the prefix it is told about. A localized MSVC # prints a translated prefix, and when CMake cannot detect it the notes are # silently dropped: the build then reuses stale objects after a header changes and # produces a binary that does not match its sources -- wrong, not just slow. This # only warns, because supplying the value is not always possible either: it has to # survive the cache code page to be usable, and a build driver can compensate more # reliably by dropping objects when a header is newer than they are. if(MSVC AND CMAKE_GENERATOR MATCHES "Ninja" AND NOT CMAKE_CL_SHOWINCLUDES_PREFIX) message(WARNING "No /showIncludes prefix was detected, so Ninja will not track header " "dependencies and a header change will not rebuild what includes it. " "Configure with -DCMAKE_CL_SHOWINCLUDES_PREFIX=, or make sure the compiler emits its " "messages in the language CMake probes for.") endif() # Verbatim copies of the upstream native library; see THIRD_PARTY_NOTICES.md. # These files are never edited: they are the validated DSP kernels. set(JOC_REUSED_SOURCES src/joc_core/eac3joc_core.cpp src/speaker/speaker_renderer.cpp src/binaural/binaural_renderer.cpp ) set(JOC_INTERNAL_SOURCES src/adm/adm_metadata.cpp src/adm/adm_tracks.cpp src/binaural/binaural_runtime.cpp src/binaural/sofa_binaural_renderer.cpp src/eac3_transport/eac3_reader.cpp src/emdf/emdf_parser.cpp src/foundation/bit_reader.cpp src/foundation/fft.cpp src/foundation/fs_utf8.cpp src/foundation/mini_json.cpp src/foundation/sha256.cpp src/hrtf/jochrtf.cpp src/hrtf/public_filterbank.cpp src/hrtf/rosella_model.cpp src/hrtf/rosella_renderer.cpp src/hrtf/kernel_tables.cpp src/hrtf/sofa.cpp src/hrtf/sofa_cache.cpp src/hrtf/sofa_field.cpp src/io/adm_writer.cpp src/io/hdf5.cpp src/io/inflate.cpp src/io/npy.cpp src/io/npy_writer.cpp src/io/process.cpp src/io/wav_writer.cpp src/io/zip_reader.cpp src/joc_bitstream/joc_parser.cpp src/joc_core/objects16.cpp src/oamd/oamd_parser.cpp src/speaker/speaker_layout_lookup.cpp src/speaker/speaker_step.cpp src/stream/stream.cpp src/task/task.cpp src/telemetry/event_bus.cpp src/timeline/position_timeline.cpp ) # --------------------------------------------------------------------------- # Runtime-dispatched SIMD kernels (src/simd/simd.h explains the contract). # # One flat directory, the ISA in the file name (`kernels_intrin_.cpp`), # never in a subdirectory. MSVC has no function-level ISA attribute, so every # ISA lives in its own translation unit compiled with its own flag, and # dispatch.cpp -- built for the baseline ISA -- picks one when the library is # first used. Only a `kernels_intrin_*.cpp` unit ever gets a wider flag, so a # baseline unit cannot inherit one by accident. # # The x86-64 baseline is SSE2 and there is no SSE2 unit on purpose: a 128-bit # SSE2 register is the register a scalar double already occupies, so SSE2 cannot # widen double-precision arithmetic and hand-written SSE2 would only add moves. # AArch64 needs no probe either; ASIMD is architectural, and the NEON unit is # how a vector path gets selected there. # --------------------------------------------------------------------------- set(JOC_SIMD_SOURCES src/simd/cpu_probe.cpp src/simd/dispatch.cpp src/simd/kernels_scalar.cpp ) set(JOC_SIMD_DEFINES "") set(JOC_ARCH "") if(CMAKE_SIZEOF_VOID_P EQUAL 8) if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|EM64T)$") set(JOC_ARCH x86_64) elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64|AARCH64)$") set(JOC_ARCH aarch64) endif() endif() if(NOT JOC_ARCH AND DEFINED CMAKE_CXX_COMPILER_ARCHITECTURE_ID) if(CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL "x64") set(JOC_ARCH x86_64) elseif(CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL "ARM64") set(JOC_ARCH aarch64) endif() endif() if(JOC_ARCH STREQUAL "x86_64") list(APPEND JOC_SIMD_DEFINES JOC_SIMD_HAVE_SSE2=1) if(JOC_ENABLE_AVX2) list(APPEND JOC_SIMD_SOURCES src/simd/kernels_intrin_avx2.cpp) list(APPEND JOC_SIMD_DEFINES JOC_SIMD_HAVE_AVX2=1) if(MSVC) set_source_files_properties(src/simd/kernels_intrin_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2") else() set_source_files_properties(src/simd/kernels_intrin_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2") endif() endif() if(JOC_ENABLE_AVX512) list(APPEND JOC_SIMD_SOURCES src/simd/kernels_intrin_avx512.cpp) list(APPEND JOC_SIMD_DEFINES JOC_SIMD_HAVE_AVX512=1) if(MSVC) set_source_files_properties(src/simd/kernels_intrin_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512") else() set_source_files_properties(src/simd/kernels_intrin_avx512.cpp PROPERTIES COMPILE_OPTIONS "-mavx512f") endif() endif() elseif(JOC_ARCH STREQUAL "aarch64") list(APPEND JOC_SIMD_DEFINES JOC_SIMD_HAVE_NEON=1) list(APPEND JOC_SIMD_SOURCES src/simd/kernels_intrin_neon.cpp) endif() add_library(joc_simd OBJECT ${JOC_SIMD_SOURCES}) target_include_directories(joc_simd PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_compile_definitions(joc_simd PRIVATE ${JOC_SIMD_DEFINES}) # The objects are linked into the shared library as well, so they must be # position independent even though an object library does not inherit that. set_target_properties(joc_simd PROPERTIES POSITION_INDEPENDENT_CODE ON) # Static form of the engine, used by the in-tree tools and tests so they can use # internal modules without exporting them from the shared library. if(JOC_BUILD_TESTS OR JOC_BUILD_DEVTOOLS) add_library(joc_core_impl STATIC ${JOC_INTERNAL_SOURCES} ${JOC_REUSED_SOURCES} $) target_include_directories(joc_core_impl PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" ) target_link_libraries(joc_core_impl PUBLIC Threads::Threads) endif() add_library(joc_core SHARED src/api/joc_api.cpp src/api/joc_stream_api.cpp src/api/joc_task_api.cpp ${JOC_INTERNAL_SOURCES} ${JOC_REUSED_SOURCES} $ ) target_include_directories(joc_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" ) target_compile_definitions(joc_core PRIVATE JOC_BUILD_DLL) target_link_libraries(joc_core PRIVATE Threads::Threads) set_target_properties(joc_core PROPERTIES OUTPUT_NAME "joc_core" CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES POSITION_INDEPENDENT_CODE YES ) # The frontend compiles the UTF-8 path shim itself: it is small, and the shared # library keeps its internals unexported. add_executable(joc_cli src/cli/joc_cli.cpp src/foundation/fs_utf8.cpp) target_link_libraries(joc_cli PRIVATE joc_core) target_include_directories(joc_cli PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") # The installed CLI is in bin/ and the shared library in lib/, and ELF/Mach-O # strip the build rpath on install, so the relative lookup has to be recorded # here or `bin/joc_cli` cannot find `../lib/libjoc_core.*`. if(UNIX) if(APPLE) set_target_properties(joc_cli PROPERTIES INSTALL_RPATH "@loader_path/../lib") else() set_target_properties(joc_cli PROPERTIES INSTALL_RPATH "$ORIGIN/../lib") endif() endif() if(JOC_BUILD_TESTS) enable_testing() add_executable(joc_tests tests/test_core.cpp) target_link_libraries(joc_tests PRIVATE joc_core_impl) target_include_directories(joc_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") add_test(NAME core COMMAND joc_tests) # The public headers must stay valid C and C++: these targets exist to prove it. add_library(joc_headers_c OBJECT tests/test_headers.c) target_include_directories(joc_headers_c PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") add_library(joc_headers_cpp OBJECT tests/test_headers.cpp) target_link_libraries(joc_headers_cpp PRIVATE joc_core) endif() if(JOC_BUILD_DEVTOOLS) add_executable(joc_dump devtools/joc_dump/main.cpp) target_link_libraries(joc_dump PRIVATE joc_core joc_core_impl) target_include_directories(joc_dump PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") # SOFA reader probe: the C++ side of devtools/checks/check_sofa.py. add_executable(joc_sofa_field_probe devtools/sofa_field_probe/main.cpp) add_executable(joc_dictionary_probe devtools/sofa_field_probe/dictionary_main.cpp) target_link_libraries(joc_dictionary_probe PRIVATE joc_core_impl) target_include_directories(joc_dictionary_probe PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(joc_sofa_field_probe PRIVATE joc_core_impl) target_include_directories(joc_sofa_field_probe PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") # Rosella model probe: the C++ side of devtools/checks/check_rosella.py. add_executable(joc_rosella_probe devtools/rosella_probe/main.cpp) target_link_libraries(joc_rosella_probe PRIVATE joc_core_impl) target_include_directories(joc_rosella_probe PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") add_executable(joc_sofa_probe devtools/sofa_probe/main.cpp) target_link_libraries(joc_sofa_probe PRIVATE joc_core_impl) target_include_directories(joc_sofa_probe PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") # Probe for the streaming surface: the acceptance harness and the usage # example for joc_stream.h. Not shipped: a player integrates the library. add_executable(joc_stream_probe devtools/joc_stream/main.cpp) target_link_libraries(joc_stream_probe PRIVATE joc_core) endif() set(JOC_MSVC_TARGETS joc_core joc_cli joc_simd) if(JOC_BUILD_TESTS OR JOC_BUILD_DEVTOOLS) list(APPEND JOC_MSVC_TARGETS joc_core_impl) endif() if(JOC_BUILD_TESTS) list(APPEND JOC_MSVC_TARGETS joc_tests joc_headers_c joc_headers_cpp) endif() if(JOC_BUILD_DEVTOOLS) list(APPEND JOC_MSVC_TARGETS joc_rosella_probe joc_dump joc_stream_probe joc_sofa_probe joc_sofa_field_probe joc_dictionary_probe) endif() if(MSVC) set(JOC_MSVC_FLAGS /W4 /permissive- /Zc:__cplusplus /utf-8 /EHsc /GR- /fp:precise) # The writers use std::fopen for seekable, byte-exact output. set(JOC_MSVC_DEFINES _CRT_SECURE_NO_WARNINGS) # C4324 comes from the reused kernel's std::barrier member at /W4; it is # pre-existing behaviour of a verbatim file, so the warning is silenced # rather than the file edited. set_source_files_properties(src/joc_core/eac3joc_core.cpp PROPERTIES COMPILE_OPTIONS "/wd4324") foreach(target IN LISTS JOC_MSVC_TARGETS) set_property(TARGET ${target} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") # No /arch here on purpose: the baseline units keep the architecture's # guaranteed ISA and only the src/simd `kernels_intrin_*` units carry a # wider one (their flags are set per source file above). target_compile_options(${target} PRIVATE ${JOC_MSVC_FLAGS} $<$:/O2> $<$:/Oi>) target_compile_definitions(${target} PRIVATE ${JOC_MSVC_DEFINES}) endforeach() target_link_options(joc_core PRIVATE /INCREMENTAL:NO /OPT:REF /OPT:ICF) target_link_options(joc_cli PRIVATE /INCREMENTAL:NO) else() foreach(target IN LISTS JOC_MSVC_TARGETS) target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -fno-fast-math $<$:-O3>) endforeach() endif() # Headers listed for IDE visibility. target_sources(joc_core PRIVATE include/joc_core.h include/joc_stream.h include/eac3joc_core.h src/joc_bitstream/joc_huffman_tables.h src/joc_core/qmf_tables.h src/speaker/speaker_layouts.h ) install(TARGETS joc_core joc_cli RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib )