Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/build_cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 14 additions & 7 deletions .github/workflows/build_python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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 .
Expand All @@ -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
Expand All @@ -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
Expand Down
18 changes: 16 additions & 2 deletions dlib/cmake_utils/set_compiler_specific_options.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions dlib/cmake_utils/test_for_simd_options.cmake
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 14 additions & 3 deletions dlib/simd/simd4f.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -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 <typename T, typename std::enable_if<
std::is_same<T, int32x4_t>::value &&
!std::is_same<T, float32x4_t>::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); }
Expand All @@ -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
Expand Down
28 changes: 26 additions & 2 deletions dlib/simd/simd4i.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "simd_check.h"
#include "../uintn.h"
#include <type_traits>

namespace dlib
{
Expand Down Expand Up @@ -92,6 +93,8 @@ namespace dlib

#elif defined(DLIB_HAVE_NEON)

class simd4f;

class simd4i
{
public:
Expand All @@ -101,19 +104,40 @@ 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;
return *this;
}

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 <typename T, typename std::enable_if<
std::is_same<T, uint32x4_t>::value &&
!std::is_same<T, int32x4_t>::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); }
Expand Down
6 changes: 5 additions & 1 deletion dlib/simd/simd_check.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions dlib/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
129 changes: 129 additions & 0 deletions dlib/test/simd.cpp
Original file line number Diff line number Diff line change
@@ -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 <dlib/simd.h>
#include <dlib/array2d.h>
#include <dlib/image_transforms/interpolation.h>
#include <type_traits>

namespace
{
using namespace test;
using namespace dlib;

template <typename float_vector, typename int_vector>
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<int32>(input[offset+i]);
DLIB_TEST(converted[i] == expected);
DLIB_TEST(restored[i] == static_cast<float>(expected));
DLIB_TEST(fractions[i] == input[offset+i] - static_cast<float>(expected));
}
}
}

void test_resize()
{
// Bilinear interpolation must preserve a linear ramp. These dimensions
// exercise fractional coordinates, complete SIMD groups, and the scalar tail.
array2d<float> gray(17, 19), gray_out(12, 14);
array2d<rgb_pixel> 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<float>(8*r + c);
rgb[r][c] = rgb_pixel(
static_cast<unsigned char>(4*r + 2*c),
static_cast<unsigned char>(2*r + 4*c),
static_cast<unsigned char>(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<simd4f, simd4i>();
test_conversions<simd8f, simd8i>();
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<uint32>(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<float32x4_t, int32x4_t>::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;
}
Loading
Loading