diff --git a/.github/workflows/build_cpp.yml b/.github/workflows/build_cpp.yml index 226d24d4bf..8964e12b70 100644 --- a/.github/workflows/build_cpp.yml +++ b/.github/workflows/build_cpp.yml @@ -207,6 +207,21 @@ jobs: - name: Build ffmpeg example run: cmake --build build --config Release --target ffmpeg_video_muxing_ex --parallel 4 + windows-arm64: + runs-on: windows-11-arm + steps: + - uses: actions/checkout@v4 + - name: Check SIMD target detection + run: cmake -P ../cmake_utils/test_for_simd_options.cmake + - name: Configure + run: | + pip3 install cmake + cmake . -B build -A ARM64 + - name: Build just tests + run: cmake --build build --config Release --target dtest --parallel 4 + - name: Test SIMD and image processing + run: build/Release/dtest.exe --test_simd --test_fhog -q + windows-latest: runs-on: 'windows-latest' steps: diff --git a/.github/workflows/build_python.yml b/.github/workflows/build_python.yml index 5e3049564e..c9759bb08c 100644 --- a/.github/workflows/build_python.yml +++ b/.github/workflows/build_python.yml @@ -32,13 +32,20 @@ defaults: jobs: Windows: - runs-on: 'windows-latest' + strategy: + matrix: + os: + - windows-latest + - windows-11-arm + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v6 + with: + python-version: '3.13' - name: Install python deps run: | - pip install pytest numpy + pip install pytest numpy setuptools packaging pip3 install cmake - name: Build run: | @@ -53,7 +60,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 - name: Install python deps - run: pip install pytest numpy + run: pip install pytest numpy setuptools packaging - name: Build run: | pip install . @@ -68,7 +75,7 @@ jobs: with: python-version: '3.13t' - name: Install python deps - run: pip install pytest numpy + run: pip install pytest numpy setuptools packaging - name: Build run: pip install . - name: Verify free-threading @@ -84,7 +91,7 @@ jobs: with: python-version: '3.8' - name: Install python deps - run: pip install pytest numpy + run: pip install pytest numpy setuptools packaging - name: Build run: pip install . - name: Test diff --git a/dlib/cmake_utils/set_compiler_specific_options.cmake b/dlib/cmake_utils/set_compiler_specific_options.cmake index be05c88f45..b2347f9d85 100644 --- a/dlib/cmake_utils/set_compiler_specific_options.cmake +++ b/dlib/cmake_utils/set_compiler_specific_options.cmake @@ -23,13 +23,20 @@ endif() set(gcc_like_compilers GNU Clang Intel) -set(intel_archs x86_64 i386 i686 AMD64 amd64 x86) +set(intel_archs x86_64 i386 i686 AMD64 amd64 x86 x64 X86) set(DLIB_SIMD_LEVEL NONE) +# With Visual Studio, CMAKE_SYSTEM_PROCESSOR can describe the host even when +# -A selects a different target. Use the architecture identified by the compiler. +set(dlib_target_processor "${CMAKE_SYSTEM_PROCESSOR}") +if (MSVC AND CMAKE_CXX_COMPILER_ARCHITECTURE_ID) + set(dlib_target_processor "${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}") +endif() + # Setup some options to allow a user to enable SSE and AVX instruction use. if ((";${gcc_like_compilers};" MATCHES ";${CMAKE_CXX_COMPILER_ID};") AND - (";${intel_archs};" MATCHES ";${CMAKE_SYSTEM_PROCESSOR};") AND NOT USE_AUTO_VECTOR) + (";${intel_archs};" MATCHES ";${dlib_target_processor};") AND NOT USE_AUTO_VECTOR) option(USE_SSE2_INSTRUCTIONS "Compile your program with SSE2 instructions" OFF) option(USE_SSE4_INSTRUCTIONS "Compile your program with SSE4 instructions" OFF) option(USE_AVX_INSTRUCTIONS "Compile your program with AVX instructions" OFF) @@ -46,6 +53,13 @@ if ((";${gcc_like_compilers};" MATCHES ";${CMAKE_CXX_COMPILER_ID};") AND list(APPEND active_compile_opts -msse2) message(STATUS "Enabling SSE2 instructions") endif() +elseif ((MSVC OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") AND + "${dlib_target_processor}" MATCHES "^(arm64|ARM64|aarch64)$") + option(USE_NEON_INSTRUCTIONS "Compile your program with ARM-NEON instructions" ON) + if (USE_NEON_INSTRUCTIONS) + set(DLIB_SIMD_LEVEL NEON) + message(STATUS "Enabling ARM-NEON instructions") + endif() elseif (MSVC OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") # else if using Visual Studio # Use SSE2 by default when using Visual Studio. option(USE_SSE2_INSTRUCTIONS "Compile your program with SSE2 instructions" ON) diff --git a/dlib/cmake_utils/test_for_simd_options.cmake b/dlib/cmake_utils/test_for_simd_options.cmake new file mode 100644 index 0000000000..9ce8d27e33 --- /dev/null +++ b/dlib/cmake_utils/test_for_simd_options.cmake @@ -0,0 +1,37 @@ +# Exercise target selection independently of the machine running this check. +cmake_minimum_required(VERSION 3.10) + +function(check_simd_options compiler host target expected) + set(MSVC TRUE) + set(CMAKE_CXX_COMPILER_ID "${compiler}") + set(CMAKE_CXX_COMPILER_VERSION 19.0) + set(CMAKE_SYSTEM_PROCESSOR "${host}") + set(CMAKE_CXX_COMPILER_ARCHITECTURE_ID "${target}") + + # Supply the results of the unrelated header and pointer-size probes. + set(HAVE_SYS_TYPES_H TRUE) + set(HAVE_STDINT_H TRUE) + set(HAVE_STDDEF_H TRUE) + set(HAVE_SIZE_OF_VOID_PTR TRUE) + set(SIZE_OF_VOID_PTR 8) + set(USE_SSE2_INSTRUCTIONS ON CACHE BOOL "" FORCE) + set(USE_SSE4_INSTRUCTIONS OFF CACHE BOOL "" FORCE) + set(USE_AVX_INSTRUCTIONS ON CACHE BOOL "" FORCE) + set(USE_NEON_INSTRUCTIONS ON CACHE BOOL "" FORCE) + + include("${CMAKE_CURRENT_LIST_DIR}/set_compiler_specific_options.cmake") + if (NOT DLIB_SIMD_LEVEL STREQUAL expected) + message(FATAL_ERROR "${compiler} on ${host} targeting ${target}: expected ${expected}, got ${DLIB_SIMD_LEVEL}") + endif() + if (expected STREQUAL "NEON" AND + "${active_compile_opts};${active_preprocessor_switches}" MATCHES "[Ss][Ss][Ee]|[Aa][Vv][Xx]") + message(FATAL_ERROR "ARM64 target received x86 SIMD options") + endif() +endfunction() + +check_simd_options(MSVC AMD64 ARM64 NEON) +check_simd_options(MSVC ARM64 ARM64 NEON) +check_simd_options(MSVC ARM64 x64 AVX) +check_simd_options(MSVC AMD64 x64 AVX) +check_simd_options(Clang AMD64 ARM64 NEON) +check_simd_options(Clang ARM64 x64 AVX) diff --git a/dlib/simd/simd4f.h b/dlib/simd/simd4f.h index e333c5e932..c5425ecfce 100644 --- a/dlib/simd/simd4f.h +++ b/dlib/simd/simd4f.h @@ -146,7 +146,7 @@ namespace dlib inline simd4f(float f) { x = vdupq_n_f32(f); } inline simd4f(float r0, float r1, float r2, float r3) { - float __attribute__ ((aligned (16))) data[4] = { r0, r1, r2, r3 }; + alignas(16) float data[4] = { r0, r1, r2, r3 }; x = vld1q_f32(data); } inline simd4f(const float32x4_t& val):x(val) {} @@ -172,8 +172,12 @@ namespace dlib inline operator float32x4_t() const { return x; } - // truncate to 32bit integers - inline operator int32x4_t() const { return vcvtq_s32_f32(x); } + // Preserve the raw integer conversion when NEON has distinct vector types. + // Conversion to simd4i always uses its numeric constructor below. + template ::value && + !std::is_same::value, int>::type = 0> + inline operator T() const { return vcvtq_s32_f32(x); } inline void load_aligned(const type* ptr) { x = vld1q_f32(ptr); } inline void store_aligned(type* ptr) const { vst1q_f32(ptr, x); } @@ -192,6 +196,13 @@ namespace dlib float32x4_t x; }; + inline simd4i::simd4i(const simd4f& val) : x(vcvtq_s32_f32(val)) {} + + inline simd4i& simd4i::operator=(const simd4f& val) + { + x = vcvtq_s32_f32(val); + return *this; + } typedef simd4i simd4f_bool; #else diff --git a/dlib/simd/simd4i.h b/dlib/simd/simd4i.h index 155540bee0..9e871dc28f 100644 --- a/dlib/simd/simd4i.h +++ b/dlib/simd/simd4i.h @@ -5,6 +5,7 @@ #include "simd_check.h" #include "../uintn.h" +#include namespace dlib { @@ -92,6 +93,8 @@ namespace dlib #elif defined(DLIB_HAVE_NEON) + class simd4f; + class simd4i { public: @@ -101,11 +104,27 @@ namespace dlib inline simd4i(int32 f) { x = vdupq_n_s32(f); } inline simd4i(int32 r0, int32 r1, int32 r2, int32 r3) { - int32 __attribute__((aligned(16))) data[4] = { r0, r1, r2, r3 }; + alignas(16) int32 data[4] = { r0, r1, r2, r3 }; x = vld1q_s32(data); } inline simd4i(const int32x4_t& val):x(val) {} + inline explicit simd4i(const simd4f& val); + /*! + requires + - Every lane of val is finite and its truncated value fits in int32. + ensures + - Each lane contains the corresponding value in val truncated toward zero. + !*/ + + inline simd4i& operator=(const simd4f& val); + /*! + requires + - Every lane of val is finite and its truncated value fits in int32. + ensures + - Assigns simd4i(val) to *this and returns *this. + !*/ + inline simd4i& operator=(const int32x4_t& val) { x = val; @@ -113,7 +132,12 @@ namespace dlib } inline operator int32x4_t() const { return x; } - inline operator uint32x4_t() const { return (uint32x4_t)x; } + + // Some NEON implementations use the same type for signed and unsigned vectors. + template ::value && + !std::is_same::value, int>::type = 0> + inline operator T() const { return vreinterpretq_u32_s32(x); } inline void load_aligned(const type* ptr) { x = vld1q_s32(ptr); } inline void store_aligned(type* ptr) const { vst1q_s32(ptr, x); } diff --git a/dlib/simd/simd_check.h b/dlib/simd/simd_check.h index 9d36e0e7b6..5901cd77d0 100644 --- a/dlib/simd/simd_check.h +++ b/dlib/simd/simd_check.h @@ -10,7 +10,11 @@ // figure out which SIMD instructions we can use. #ifndef DLIB_DO_NOT_USE_SIMD - #if defined(_MSC_VER) + #if defined(_MSC_VER) && defined(_M_ARM64) + #ifndef DLIB_HAVE_NEON + #define DLIB_HAVE_NEON + #endif + #elif defined(_MSC_VER) #ifdef __AVX__ #ifndef DLIB_HAVE_SSE2 #define DLIB_HAVE_SSE2 diff --git a/dlib/test/CMakeLists.txt b/dlib/test/CMakeLists.txt index 49e44fe2d4..0767551022 100644 --- a/dlib/test/CMakeLists.txt +++ b/dlib/test/CMakeLists.txt @@ -128,6 +128,7 @@ add_executable(${target_name} main.cpp tester.cpp sequence_segmenter.cpp serialize.cpp set.cpp + simd.cpp sldf.cpp sliding_buffer.cpp sockets2.cpp diff --git a/dlib/test/simd.cpp b/dlib/test/simd.cpp new file mode 100644 index 0000000000..1e8a286e5a --- /dev/null +++ b/dlib/test/simd.cpp @@ -0,0 +1,129 @@ +// Copyright (C) 2026 Davis E. King (davis@dlib.net) +// License: Boost Software License See LICENSE.txt for the full license. + +#include "tester.h" +#include +#include +#include +#include + +namespace +{ + using namespace test; + using namespace dlib; + + template + void test_conversions() + { + const float input[] = { + 1.75f, -2.5f, 0.0f, 17.0f, + 0.6875f, -0.6875f, 1.0625f, -1.0625f, + 2147483520.0f, -2147483648.0f, 16777215.0f, -16777215.0f, + 0.0f, -0.0f, 123.875f, -123.875f + }; + float_vector values; + for (unsigned int offset = 0; offset < 16; offset += values.size()) + { + values.load(input + offset); + const int_vector converted(values); + const float_vector restored(converted); + const float_vector fractions = values - converted; + for (unsigned int i = 0; i < values.size(); ++i) + { + const int32 expected = static_cast(input[offset+i]); + DLIB_TEST(converted[i] == expected); + DLIB_TEST(restored[i] == static_cast(expected)); + DLIB_TEST(fractions[i] == input[offset+i] - static_cast(expected)); + } + } + } + + void test_resize() + { + // Bilinear interpolation must preserve a linear ramp. These dimensions + // exercise fractional coordinates, complete SIMD groups, and the scalar tail. + array2d gray(17, 19), gray_out(12, 14); + array2d rgb(17, 19), rgb_out(12, 14); + for (long r = 0; r < gray.nr(); ++r) + { + for (long c = 0; c < gray.nc(); ++c) + { + gray[r][c] = static_cast(8*r + c); + rgb[r][c] = rgb_pixel( + static_cast(4*r + 2*c), + static_cast(2*r + 4*c), + static_cast(3*r + c)); + } + } + resize_image(gray, gray_out, interpolate_bilinear()); + resize_image(rgb, rgb_out, interpolate_bilinear()); + for (long r = 0; r < gray_out.nr(); ++r) + { + for (long c = 0; c < gray_out.nc(); ++c) + { + const double y = r*16.0/11; + const double x = c*18.0/13; + DLIB_TEST(std::abs(gray_out[r][c] - (8*y + x)) < 1e-4); + DLIB_TEST(std::abs(rgb_out[r][c].red - (4*y + 2*x)) <= 1); + DLIB_TEST(std::abs(rgb_out[r][c].green - (2*y + 4*x)) <= 1); + DLIB_TEST(std::abs(rgb_out[r][c].blue - (3*y + x)) <= 1); + } + } + } + + class simd_tester : public tester + { + public: + simd_tester() : tester("test_simd", "Run tests on SIMD conversions.") {} + + void perform_test() override + { + test_conversions(); + test_conversions(); + test_resize(); + + const simd4f values(1.75f, -2.5f, 0.0f, 17.0f); + const simd4i converted(values); + DLIB_TEST(converted[0] == 1); + DLIB_TEST(converted[1] == -2); + DLIB_TEST(converted[2] == 0); + DLIB_TEST(converted[3] == 17); + + const simd4i integers(1, -2, 0, 17); + for (unsigned int i = 0; i < 4; ++i) + DLIB_TEST(integers[i] == converted[i]); + +#ifdef DLIB_HAVE_NEON + simd4i assigned; + assigned = values; + for (unsigned int i = 0; i < 4; ++i) + DLIB_TEST(assigned[i] == converted[i]); + + // Both raw integer conversions remain available, even if their types alias. + const int32x4_t signed_vector = integers; + const uint32x4_t unsigned_vector = integers; + int32 signed_lanes[4]; + uint32 unsigned_lanes[4]; + vst1q_s32(signed_lanes, signed_vector); + vst1q_u32(unsigned_lanes, unsigned_vector); + for (unsigned int i = 0; i < 4; ++i) + DLIB_TEST(unsigned_lanes[i] == static_cast(signed_lanes[i])); + + const float32x4_t float_vector = values; + float float_lanes[4]; + vst1q_f32(float_lanes, float_vector); + for (unsigned int i = 0; i < 4; ++i) + DLIB_TEST(float_lanes[i] == values[i]); + + // A separate raw float-to-int conversion is possible only with distinct types. + if (!std::is_same::value) + { + const int32x4_t raw_converted = values; + vst1q_s32(signed_lanes, raw_converted); + for (unsigned int i = 0; i < 4; ++i) + DLIB_TEST(signed_lanes[i] == converted[i]); + } +#endif + } + } a; +} diff --git a/setup.py b/setup.py index 519c5d415f..60eb2f0289 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ import stat import subprocess import sys +import sysconfig from math import floor from packaging.version import Version, parse as parse_version @@ -190,7 +191,11 @@ def build_extension(self, ext): if platform.system() == "Windows": cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)] if sys.maxsize > 2**32: - cmake_args += ['-A', 'x64'] + # Match the interpreter, including x64 Python emulated on ARM64. + if sysconfig.get_platform() == 'win-arm64': + cmake_args += ['-A', 'ARM64'] + else: + cmake_args += ['-A', 'x64'] # Do a parallel build build_args += ['--', '/m'] else: diff --git a/tools/python/test/test_setup.py b/tools/python/test/test_setup.py new file mode 100644 index 0000000000..84833ab5c6 --- /dev/null +++ b/tools/python/test/test_setup.py @@ -0,0 +1,46 @@ +import pathlib +import platform +import runpy +import subprocess +import sys +import sysconfig + +import pytest +import setuptools + + +@pytest.mark.parametrize( + "python_platform, host_machine, expected_arch", + [ + ("win-arm64", "ARM64", "ARM64"), + ("win-amd64", "ARM64", "x64"), + ("win-amd64", "AMD64", "x64"), + ], +) +def test_windows_extension_matches_python_architecture( + monkeypatch, tmp_path, python_platform, host_machine, expected_arch +): + root = pathlib.Path(__file__).resolve().parents[3] + monkeypatch.chdir(root) + monkeypatch.setattr(sys, "argv", ["setup.py"]) + monkeypatch.setattr(setuptools, "setup", lambda **kwargs: None) + setup = runpy.run_path(str(root / "setup.py")) + + monkeypatch.setattr(platform, "system", lambda: "Windows") + monkeypatch.setattr(platform, "machine", lambda: host_machine) + monkeypatch.setattr(sysconfig, "get_platform", lambda: python_platform) + monkeypatch.setattr(sys, "maxsize", 2**63 - 1) + + commands = [] + monkeypatch.setattr( + subprocess, "check_call", lambda args, **kwargs: commands.append(args) + ) + build = setup["CMakeBuild"](setuptools.Distribution()) + build.initialize_options() + build.build_temp = str(tmp_path / "build") + build.build_lib = str(tmp_path / "lib") + build.build_extension(setup["CMakeExtension"]("_dlib_pybind11", "tools/python")) + + configure = commands[0] + assert configure[configure.index("-A") + 1] == expected_arch + assert "-DPython_EXECUTABLE=" + sys.executable in configure