From 94cc3360791bfe983b452063cf4dd9a5cbee59ac Mon Sep 17 00:00:00 2001 From: Aaron Krol Date: Thu, 15 Oct 2026 17:36:55 -0400 Subject: [PATCH 1/6] kernels: load NEON lane indices from memory in index_min/max The NEON index_min/index_max kernels initialized their lane-index vector with a brace initializer, { 0, 1, 2, 3 }. On big-endian AArch64, GCC fills a brace-initialized vector in the reverse of NEON lane order, so lane 0 held index 3 and the kernels returned indices off by up to three within a group of four. Load the indices from a static array with vld1q_u32 instead, which gives the same lanes on either byte order. Found on an aarch64_be target (GCC 11.2), where volk_profile flagged neon and neonv8 of 32f_index_min_16u, 32fc_index_min_16u and 32fc_index_max_16u, and neonv8 of 32f_index_min_32u, 32fc_index_min_32u and 32fc_index_max_32u. The float index_max kernels have the same bug but pass that test: its input starts with 16 tied maxima, and the tie-break hides the reversed lanes. Signed-off-by: Aaron Krol --- kernels/volk/volk_32f_index_max_16u.h | 10 ++++++++-- kernels/volk/volk_32f_index_max_32u.h | 5 ++++- kernels/volk/volk_32f_index_min_16u.h | 10 ++++++++-- kernels/volk/volk_32f_index_min_32u.h | 5 ++++- kernels/volk/volk_32fc_index_max_16u.h | 10 ++++++++-- kernels/volk/volk_32fc_index_max_32u.h | 5 ++++- kernels/volk/volk_32fc_index_min_16u.h | 10 ++++++++-- kernels/volk/volk_32fc_index_min_32u.h | 5 ++++- 8 files changed, 48 insertions(+), 12 deletions(-) diff --git a/kernels/volk/volk_32f_index_max_16u.h b/kernels/volk/volk_32f_index_max_16u.h index f788ded77..16fe6d673 100644 --- a/kernels/volk/volk_32f_index_max_16u.h +++ b/kernels/volk/volk_32f_index_max_16u.h @@ -277,7 +277,10 @@ volk_32f_index_max_16u_neon(uint16_t* target, const float* src0, uint32_t num_po const float* inputPtr = src0; // Use integer indices directly - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_max = vdupq_n_f32(-FLT_MAX); @@ -345,7 +348,10 @@ volk_32f_index_max_16u_neonv8(uint16_t* target, const float* src0, uint32_t num_ const float* inputPtr = src0; // Use integer indices directly (no float conversion overhead) - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_max = vdupq_n_f32(-FLT_MAX); diff --git a/kernels/volk/volk_32f_index_max_32u.h b/kernels/volk/volk_32f_index_max_32u.h index 2d7cd8057..784142dc5 100644 --- a/kernels/volk/volk_32f_index_max_32u.h +++ b/kernels/volk/volk_32f_index_max_32u.h @@ -333,7 +333,10 @@ volk_32f_index_max_32u_neonv8(uint32_t* target, const float* src0, uint32_t num_ const float* inputPtr = src0; // Use integer indices directly (no float conversion overhead) - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_max = vdupq_n_f32(-FLT_MAX); diff --git a/kernels/volk/volk_32f_index_min_16u.h b/kernels/volk/volk_32f_index_min_16u.h index 3911f51d0..1e426750a 100644 --- a/kernels/volk/volk_32f_index_min_16u.h +++ b/kernels/volk/volk_32f_index_min_16u.h @@ -269,7 +269,10 @@ volk_32f_index_min_16u_neon(uint16_t* target, const float* source, uint32_t num_ const float* inputPtr = source; // Use integer indices directly - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_min = vdupq_n_f32(FLT_MAX); @@ -337,7 +340,10 @@ volk_32f_index_min_16u_neonv8(uint16_t* target, const float* source, uint32_t nu const float* inputPtr = source; // Use integer indices directly (no float conversion overhead) - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_min = vdupq_n_f32(FLT_MAX); diff --git a/kernels/volk/volk_32f_index_min_32u.h b/kernels/volk/volk_32f_index_min_32u.h index 38644c1db..14fe76130 100644 --- a/kernels/volk/volk_32f_index_min_32u.h +++ b/kernels/volk/volk_32f_index_min_32u.h @@ -316,7 +316,10 @@ volk_32f_index_min_32u_neonv8(uint32_t* target, const float* source, uint32_t nu const float* inputPtr = source; // Use integer indices directly (no float conversion overhead) - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_min = vdupq_n_f32(FLT_MAX); diff --git a/kernels/volk/volk_32fc_index_max_16u.h b/kernels/volk/volk_32fc_index_max_16u.h index 514065fcf..7fa45b76f 100644 --- a/kernels/volk/volk_32fc_index_max_16u.h +++ b/kernels/volk/volk_32fc_index_max_16u.h @@ -327,7 +327,10 @@ volk_32fc_index_max_16u_neon(uint16_t* target, const lv_32fc_t* src0, uint32_t n const lv_32fc_t* inputPtr = src0; // Use integer indices directly - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_max = vdupq_n_f32(0.0f); @@ -404,7 +407,10 @@ static inline void volk_32fc_index_max_16u_neonv8(uint16_t* target, const lv_32fc_t* inputPtr = src0; // Use integer indices directly (no float conversion overhead) - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_max = vdupq_n_f32(0.0f); diff --git a/kernels/volk/volk_32fc_index_max_32u.h b/kernels/volk/volk_32fc_index_max_32u.h index b97fe6e32..fbb492c3d 100644 --- a/kernels/volk/volk_32fc_index_max_32u.h +++ b/kernels/volk/volk_32fc_index_max_32u.h @@ -609,7 +609,10 @@ static inline void volk_32fc_index_max_32u_neonv8(uint32_t* target, const lv_32fc_t* inputPtr = src0; // Use integer indices directly (no float conversion overhead) - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_max = vdupq_n_f32(0.0f); diff --git a/kernels/volk/volk_32fc_index_min_16u.h b/kernels/volk/volk_32fc_index_min_16u.h index 8aa12bde4..6afd72eb9 100644 --- a/kernels/volk/volk_32fc_index_min_16u.h +++ b/kernels/volk/volk_32fc_index_min_16u.h @@ -326,7 +326,10 @@ static inline void volk_32fc_index_min_16u_neon(uint16_t* target, const lv_32fc_t* inputPtr = source; // Use integer indices directly - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_min = vdupq_n_f32(FLT_MAX); @@ -403,7 +406,10 @@ static inline void volk_32fc_index_min_16u_neonv8(uint16_t* target, const lv_32fc_t* inputPtr = source; // Use integer indices directly (no float conversion overhead) - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_min = vdupq_n_f32(FLT_MAX); diff --git a/kernels/volk/volk_32fc_index_min_32u.h b/kernels/volk/volk_32fc_index_min_32u.h index b2fce7c54..f28c7ac1c 100644 --- a/kernels/volk/volk_32fc_index_min_32u.h +++ b/kernels/volk/volk_32fc_index_min_32u.h @@ -605,7 +605,10 @@ static inline void volk_32fc_index_min_32u_neonv8(uint32_t* target, const lv_32fc_t* inputPtr = source; // Use integer indices directly (no float conversion overhead) - uint32x4_t vec_indices = { 0, 1, 2, 3 }; + // Load from an array to avoid GCC filling a brace-initialized vector like + // { 0, 1, 2, 3 } in reverse lane order on big-endian targets + static const uint32_t init_indices[4] = { 0, 1, 2, 3 }; + uint32x4_t vec_indices = vld1q_u32(init_indices); const uint32x4_t vec_incr = vdupq_n_u32(4); float32x4_t vec_min = vdupq_n_f32(FLT_MAX); From b7f169fea9e5d287eb8933fd432bbbe6b595191c Mon Sep 17 00:00:00 2001 From: Aaron Krol Date: Thu, 15 Oct 2026 17:19:00 -0400 Subject: [PATCH 2/6] volk_32fc_conjugate_32fc: load the neonv8 sign mask from memory The neonv8 kernel built its [0, -0, 0, -0] sign mask from a brace-initialized float32x4_t. On big-endian AArch64 (seen with GCC 11.2), GCC lays that vector out in reverse NEON lane order, so the kernel negated the real parts instead of the imaginary parts. Load the mask with vld1q_f32 from a static array, which is correct on either byte order. Signed-off-by: Aaron Krol --- kernels/volk/volk_32fc_conjugate_32fc.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kernels/volk/volk_32fc_conjugate_32fc.h b/kernels/volk/volk_32fc_conjugate_32fc.h index 507adf2d6..6fe18cf8e 100644 --- a/kernels/volk/volk_32fc_conjugate_32fc.h +++ b/kernels/volk/volk_32fc_conjugate_32fc.h @@ -271,9 +271,10 @@ static inline void volk_32fc_conjugate_32fc_neonv8(lv_32fc_t* cVector, lv_32fc_t* c = cVector; const lv_32fc_t* a = aVector; - /* Sign mask to flip imaginary parts: [0, -0, 0, -0] */ - const uint32x4_t sign_mask = - vreinterpretq_u32_f32((float32x4_t){ 0.0f, -0.0f, 0.0f, -0.0f }); + /* Sign mask to flip imaginary parts: [0, -0, 0, -0], loaded from memory + * because a brace-initialized vector is lane-reversed on big-endian GCC */ + static const float sign_bits[4] = { 0.0f, -0.0f, 0.0f, -0.0f }; + const uint32x4_t sign_mask = vreinterpretq_u32_f32(vld1q_f32(sign_bits)); /* Process 4 complex numbers per iteration (2x unroll) */ while (n >= 4) { From 3dbb37073838e360fe0f3cc22dedb870e6083ec5 Mon Sep 17 00:00:00 2001 From: Aaron Krol Date: Thu, 15 Oct 2026 17:19:00 -0400 Subject: [PATCH 3/6] volk_32u_byteswap, volk_32u_reverse_32u: use vrev32q_u8 in neonv8 Both neonv8 kernels reversed the bytes of each 32-bit word with vqtbl1q_u8 and a brace-initialized index vector. On big-endian AArch64 (seen with GCC 11.2), GCC lays that vector out in reverse NEON lane order, so the lookup reversed the order of the four words in each vector instead of the bytes within each word. vrev32q_u8 does the per-word byte reversal without a table and does not depend on byte order. Signed-off-by: Aaron Krol --- kernels/volk/volk_32u_byteswap.h | 5 ++--- kernels/volk/volk_32u_reverse_32u.h | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/kernels/volk/volk_32u_byteswap.h b/kernels/volk/volk_32u_byteswap.h index d5d0613ec..6b6a19bd3 100644 --- a/kernels/volk/volk_32u_byteswap.h +++ b/kernels/volk/volk_32u_byteswap.h @@ -201,18 +201,17 @@ static inline void volk_32u_byteswap_neonv8(uint32_t* intsToSwap, unsigned int n uint32_t* inputPtr = (uint32_t*)intsToSwap; const unsigned int n8points = num_points / 8; uint8x16_t input; - uint8x16_t idx = { 3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12 }; unsigned int number = 0; for (number = 0; number < n8points; ++number) { __VOLK_PREFETCH(inputPtr + 8); input = vld1q_u8((uint8_t*)inputPtr); - input = vqtbl1q_u8(input, idx); + input = vrev32q_u8(input); vst1q_u8((uint8_t*)inputPtr, input); inputPtr += 4; input = vld1q_u8((uint8_t*)inputPtr); - input = vqtbl1q_u8(input, idx); + input = vrev32q_u8(input); vst1q_u8((uint8_t*)inputPtr, input); inputPtr += 4; } diff --git a/kernels/volk/volk_32u_reverse_32u.h b/kernels/volk/volk_32u_reverse_32u.h index 3be939165..6b10458b3 100644 --- a/kernels/volk/volk_32u_reverse_32u.h +++ b/kernels/volk/volk_32u_reverse_32u.h @@ -262,15 +262,13 @@ volk_32u_reverse_32u_neonv8(uint32_t* out, const uint32_t* in, unsigned int num_ const uint32_t* in_ptr = in; uint32_t* out_ptr = out; - const uint8x16_t idx = { 3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12 }; - const unsigned int quarterPoints = num_points / 4; unsigned int number = 0; for (; number < quarterPoints; ++number) { __VOLK_PREFETCH(in_ptr + 4); uint32x4_t x = vld1q_u32(in_ptr); uint32x4_t z = - vreinterpretq_u32_u8(vqtbl1q_u8(vrbitq_u8(vreinterpretq_u8_u32(x)), idx)); + vreinterpretq_u32_u8(vrev32q_u8(vrbitq_u8(vreinterpretq_u8_u32(x)))); vst1q_u32(out_ptr, z); in_ptr += 4; out_ptr += 4; From 37fadb0df48d2c0449dce6ca04ae235d5dd03707 Mon Sep 17 00:00:00 2001 From: Aaron Krol Date: Thu, 15 Oct 2026 17:19:00 -0400 Subject: [PATCH 4/6] volk_8u_x4_conv_k7_r2_8u: fix neonspiral loads on big-endian The neonspiral kernel loaded and stored its path metrics and branch table through uint8x16_t pointer casts. On big-endian AArch64 (seen with GCC 11.2), GCC moves such vectors in the reverse of NEON lane order, so the lane-position dependent steps (vget_low/vget_high, vzip, vpmin, vgetq_lane) worked on the wrong lanes and the decisions differed from generic. Route the loads and stores through two macros that use vld1q_u8 and vst1q_u8 when __ARM_BIG_ENDIAN is defined. On little-endian they keep the pointer casts: vld1 has no immediate-offset addressing on 32-bit ARM, and switching to it there added address arithmetic and register spills to the loop. Signed-off-by: Aaron Krol --- kernels/volk/volk_8u_x4_conv_k7_r2_8u.h | 54 ++++++++++++++++--------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/kernels/volk/volk_8u_x4_conv_k7_r2_8u.h b/kernels/volk/volk_8u_x4_conv_k7_r2_8u.h index cb2db11ac..2989aff06 100644 --- a/kernels/volk/volk_8u_x4_conv_k7_r2_8u.h +++ b/kernels/volk/volk_8u_x4_conv_k7_r2_8u.h @@ -307,6 +307,17 @@ static inline void volk_8u_x4_conv_k7_r2_8u_spiral(unsigned char* Y, #include +// Vector pointer casts move lanes in reverse NEON order on big-endian GCC, so +// use vld1q/vst1q there. On little-endian keep the casts: unlike vld1 they let +// 32-bit ARM use offset addressing for the loads and stores below. +#ifdef __ARM_BIG_ENDIAN +#define NEONSPIRAL_LOAD(p) vld1q_u8(p) +#define NEONSPIRAL_STORE(p, v) vst1q_u8((p), (v)) +#else +#define NEONSPIRAL_LOAD(p) (*(uint8x16_t*)(p)) +#define NEONSPIRAL_STORE(p, v) (*(uint8x16_t*)(p) = (v)) +#endif + static inline void volk_8u_x4_conv_k7_r2_8u_neonspiral(unsigned char* Y, unsigned char* X, unsigned char* syms, @@ -329,13 +340,13 @@ static inline void volk_8u_x4_conv_k7_r2_8u_neonspiral(unsigned char* Y, uint8x8x2_t both; // First half of butterfly - s18 = ((uint8x16_t*)X)[0]; - s19 = ((uint8x16_t*)X)[2]; + s18 = NEONSPIRAL_LOAD(X); + s19 = NEONSPIRAL_LOAD(X + 2 * 16); a76 = vdupq_n_u8(syms[2 * i]); - a78 = ((uint8x16_t*)Branchtab)[0]; + a78 = NEONSPIRAL_LOAD(Branchtab); a79 = veorq_u8(a76, a78); a82 = vdupq_n_u8(syms[2 * i + 1]); - a84 = ((uint8x16_t*)Branchtab)[2]; + a84 = NEONSPIRAL_LOAD(Branchtab + 2 * 16); a85 = veorq_u8(a82, a84); a86 = vrhaddq_u8(a79, a85); t14 = vshrq_n_u8(a86, 2); @@ -365,18 +376,18 @@ static inline void volk_8u_x4_conv_k7_r2_8u_neonspiral(unsigned char* Y, left = vget_low_u8(a89); right = vget_low_u8(a90); both = vzip_u8(left, right); - ((uint8x16_t*)Y)[0] = vcombine_u8(both.val[0], both.val[1]); + NEONSPIRAL_STORE(Y, vcombine_u8(both.val[0], both.val[1])); left = vget_high_u8(a89); right = vget_high_u8(a90); both = vzip_u8(left, right); - ((uint8x16_t*)Y)[1] = vcombine_u8(both.val[0], both.val[1]); + NEONSPIRAL_STORE(Y + 1 * 16, vcombine_u8(both.val[0], both.val[1])); // Second half of butterfly - s24 = ((uint8x16_t*)X)[1]; - s25 = ((uint8x16_t*)X)[3]; - a100 = ((uint8x16_t*)Branchtab)[1]; + s24 = NEONSPIRAL_LOAD(X + 1 * 16); + s25 = NEONSPIRAL_LOAD(X + 3 * 16); + a100 = NEONSPIRAL_LOAD(Branchtab + 1 * 16); a101 = veorq_u8(a76, a100); - a103 = ((uint8x16_t*)Branchtab)[3]; + a103 = NEONSPIRAL_LOAD(Branchtab + 3 * 16); a104 = veorq_u8(a82, a103); a105 = vrhaddq_u8(a101, a104); t17 = vshrq_n_u8(a105, 2); @@ -406,28 +417,28 @@ static inline void volk_8u_x4_conv_k7_r2_8u_neonspiral(unsigned char* Y, left = vget_low_u8(a108); right = vget_low_u8(a109); both = vzip_u8(left, right); - ((uint8x16_t*)Y)[2] = vcombine_u8(both.val[0], both.val[1]); + NEONSPIRAL_STORE(Y + 2 * 16, vcombine_u8(both.val[0], both.val[1])); left = vget_high_u8(a108); right = vget_high_u8(a109); both = vzip_u8(left, right); - ((uint8x16_t*)Y)[3] = vcombine_u8(both.val[0], both.val[1]); + NEONSPIRAL_STORE(Y + 3 * 16, vcombine_u8(both.val[0], both.val[1])); // Renormalize uint8x16_t m5, m6; - m5 = ((uint8x16_t*)Y)[0]; - m5 = vminq_u8(m5, ((uint8x16_t*)Y)[1]); - m5 = vminq_u8(m5, ((uint8x16_t*)Y)[2]); - m5 = vminq_u8(m5, ((uint8x16_t*)Y)[3]); + m5 = NEONSPIRAL_LOAD(Y); + m5 = vminq_u8(m5, NEONSPIRAL_LOAD(Y + 1 * 16)); + m5 = vminq_u8(m5, NEONSPIRAL_LOAD(Y + 2 * 16)); + m5 = vminq_u8(m5, NEONSPIRAL_LOAD(Y + 3 * 16)); uint8x8_t m7; m7 = vpmin_u8(vget_low_u8(m5), vget_high_u8(m5)); m7 = vpmin_u8(m7, m7); m7 = vpmin_u8(m7, m7); m7 = vpmin_u8(m7, m7); m6 = vcombine_u8(m7, m7); - ((uint8x16_t*)Y)[0] = vqsubq_u8(((uint8x16_t*)Y)[0], m6); - ((uint8x16_t*)Y)[1] = vqsubq_u8(((uint8x16_t*)Y)[1], m6); - ((uint8x16_t*)Y)[2] = vqsubq_u8(((uint8x16_t*)Y)[2], m6); - ((uint8x16_t*)Y)[3] = vqsubq_u8(((uint8x16_t*)Y)[3], m6); + NEONSPIRAL_STORE(Y, vqsubq_u8(NEONSPIRAL_LOAD(Y), m6)); + NEONSPIRAL_STORE(Y + 1 * 16, vqsubq_u8(NEONSPIRAL_LOAD(Y + 1 * 16), m6)); + NEONSPIRAL_STORE(Y + 2 * 16, vqsubq_u8(NEONSPIRAL_LOAD(Y + 2 * 16), m6)); + NEONSPIRAL_STORE(Y + 3 * 16, vqsubq_u8(NEONSPIRAL_LOAD(Y + 3 * 16), m6)); // Swap pointers to old and new metrics tmp = X; @@ -436,6 +447,9 @@ static inline void volk_8u_x4_conv_k7_r2_8u_neonspiral(unsigned char* Y, } } +#undef NEONSPIRAL_LOAD +#undef NEONSPIRAL_STORE + #endif /*LV_HAVE_NEON*/ #if LV_HAVE_GENERIC From 3a417d9ba76122d8d01f7052d0713f5d10eb2f34 Mon Sep 17 00:00:00 2001 From: Aaron Krol Date: Tue, 20 Oct 2026 16:52:28 -0400 Subject: [PATCH 5/6] CI: add a big-endian AArch64 test job No CI job built VOLK for a big-endian target with NEON, so lane-order bugs in the NEON kernels went unnoticed. Ubuntu has no big-endian AArch64 port or cross toolchain, so the job cross-compiles on an x86 runner with the Arm GNU Toolchain's aarch64_be-none-linux-gnu build, pinned by SHA-256 and cached, and runs ctest under qemu-aarch64_be, like the RVV job does for RISC-V. The new toolchain file points qemu at the toolchain's glibc sysroot and at its libstdc++, which sits outside the sysroot. Signed-off-by: Aaron Krol --- .github/workflows/run-tests-aarch64-be.yml | 80 +++++++++++++++++++ .../aarch64_be-none-linux-gnu.cmake | 45 +++++++++++ 2 files changed, 125 insertions(+) create mode 100644 .github/workflows/run-tests-aarch64-be.yml create mode 100644 cmake/Toolchains/aarch64_be-none-linux-gnu.cmake diff --git a/.github/workflows/run-tests-aarch64-be.yml b/.github/workflows/run-tests-aarch64-be.yml new file mode 100644 index 000000000..656030155 --- /dev/null +++ b/.github/workflows/run-tests-aarch64-be.yml @@ -0,0 +1,80 @@ +# +# Copyright 2026 Zetier Inc +# +# This file is part of VOLK +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# + +name: Run VOLK tests on big-endian AArch64 + +on: [push, pull_request] + +env: + # Ubuntu has no big-endian AArch64 cross toolchain, so use Arm's. + ARM_TOOLCHAIN: arm-gnu-toolchain-15.3.rel2-x86_64-aarch64_be-none-linux-gnu + ARM_TOOLCHAIN_URL: https://gitlab.arm.com/api/v4/projects/tooling%2Fgnu-toolchains-for-arm/packages/generic/gnu-toolchain/15.3.rel2 + ARM_TOOLCHAIN_SHA256: bdf8e9b4c62791c5c3aeeb8ba3791421d73c7d147dd04f0da502716885bcc1d2 + +jobs: + Tests: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + with: + submodules: "recursive" + - name: Install packages + run: | + sudo DEBIAN_FRONTEND=noninteractive apt-get update -q -y + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y python3-mako cmake qemu-user curl + - name: Install sccache + shell: bash + run: sudo scripts/ci/install_sccache.bash "aarch64_be tests" + continue-on-error: true + - name: Cache toolchain + id: cache_toolchain + uses: actions/cache@v5 + with: + path: ~/${{ env.ARM_TOOLCHAIN }} + key: ${{ env.ARM_TOOLCHAIN }} + - name: Download toolchain + if: steps.cache_toolchain.outputs.cache-hit != 'true' + run: | + cd ~ + curl -fsSL -o "$ARM_TOOLCHAIN.tar.xz" "$ARM_TOOLCHAIN_URL/$ARM_TOOLCHAIN.tar.xz" + echo "$ARM_TOOLCHAIN_SHA256 $ARM_TOOLCHAIN.tar.xz" | sha256sum -c - + tar -xJf "$ARM_TOOLCHAIN.tar.xz" + rm "$ARM_TOOLCHAIN.tar.xz" + - name: Add toolchain to PATH + run: echo "$HOME/$ARM_TOOLCHAIN/bin" >> "$GITHUB_PATH" + - name: Prepare sccache usage + id: enable_sccache + run: scripts/ci/setup_cmake_cache_flags.bash "aarch64_be tests" + - name: Configure + run: > + cmake + -S . + -B build + -DCMAKE_TOOLCHAIN_FILE=cmake/Toolchains/aarch64_be-none-linux-gnu.cmake + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_C_FLAGS="-Werror" + -DCMAKE_CXX_FLAGS="-Werror" + -DBUILD_EXECUTABLE=ON + ${{ steps.enable_sccache.outputs.CXX_LAUNCHER }} + ${{ steps.enable_sccache.outputs.C_LAUNCHER }} + - name: Build + run: cmake --build build -j$(nproc) + - name: Print info + run: | + sysroot=$(aarch64_be-none-linux-gnu-gcc -print-sysroot) + for opt in --alignment --avail-machines --all-machines --malloc --cc; do + qemu-aarch64_be -L "$sysroot" -E LD_LIBRARY_PATH="$(dirname "$sysroot")/lib64" \ + build/apps/volk-config-info $opt + done + - name: Test + run: cd build && ctest -V + - name: Show final sccache and build directory stats + if: always() + continue-on-error: true + shell: bash + run: scripts/ci/build-summary.sh "aarch64_be tests" "./build/" diff --git a/cmake/Toolchains/aarch64_be-none-linux-gnu.cmake b/cmake/Toolchains/aarch64_be-none-linux-gnu.cmake new file mode 100644 index 000000000..a79fee9ff --- /dev/null +++ b/cmake/Toolchains/aarch64_be-none-linux-gnu.cmake @@ -0,0 +1,45 @@ +# +# Copyright 2026 Zetier Inc +# +# This file is part of VOLK +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# + +# Big-endian AArch64 with the Arm GNU Toolchain (aarch64_be-none-linux-gnu), +# whose bin directory must be on the PATH. Tests run under qemu-aarch64_be. + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR aarch64_be) + +set(TOOLCHAIN_PREFIX aarch64_be-none-linux-gnu-) + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) +set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++) + +# The toolchain ships its own glibc sysroot; libstdc++ and libgcc_s sit +# outside it, next to the sysroot in the toolchain's target directory. +execute_process( + COMMAND ${CMAKE_C_COMPILER} -print-sysroot + RESULT_VARIABLE TOOLCHAIN_SYSROOT_RESULT + OUTPUT_VARIABLE TOOLCHAIN_SYSROOT + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(NOT TOOLCHAIN_SYSROOT_RESULT EQUAL 0 OR TOOLCHAIN_SYSROOT STREQUAL "") + message(FATAL_ERROR "Could not run ${CMAKE_C_COMPILER} -print-sysroot; " + "put the toolchain's bin directory on the PATH") +endif() +get_filename_component(TOOLCHAIN_SYSROOT "${TOOLCHAIN_SYSROOT}" REALPATH) +get_filename_component(TOOLCHAIN_TARGET_DIR "${TOOLCHAIN_SYSROOT}" DIRECTORY) + +set(CMAKE_FIND_ROOT_PATH ${TOOLCHAIN_SYSROOT}) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +# One string, as VolkAddTest pastes it into each test's shell script. The -E +# replaces the scripts' LD_LIBRARY_PATH; the build tree's RPATH still finds +# libvolk. +set(CMAKE_CROSSCOMPILING_EMULATOR + "qemu-aarch64_be -L ${TOOLCHAIN_SYSROOT} -E LD_LIBRARY_PATH=${TOOLCHAIN_TARGET_DIR}/lib64") From 2370d8fcf72ea2dfa12a63e443ead3a10a05f661 Mon Sep 17 00:00:00 2001 From: Aaron Krol Date: Tue, 20 Oct 2026 16:54:32 -0400 Subject: [PATCH 6/6] qa: start the index kernels' float input below the tied maxima The float index kernels' QA input began with 16 tied maxima of 1.0, so every lane held the maximum and every implementation returned index 0. A kernel that pairs values with the wrong lane indices gives that same answer, so the big-endian lane-order bugs in volk_32f_index_max_16u (neon, neonv8) and volk_32f_index_max_32u (neonv8) passed QA. Put a 0.0 first. The ties still cover every SIMD width, but the first maximum is now at index 1, partway into the first vector, where a lane-index mixup gives a different answer. Signed-off-by: Aaron Krol --- lib/kernel_tests.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/kernel_tests.h b/lib/kernel_tests.h index 4711ced8b..a0ee446ca 100644 --- a/lib/kernel_tests.h +++ b/lib/kernel_tests.h @@ -87,9 +87,12 @@ std::vector init_test_list(volk_test_params_t test_params) QA(VOLK_INIT_TEST(volk_32f_accumulator_s32f, test_params.make_absolute(2e-2))) QA(VOLK_INIT_TEST(volk_32f_x2_add_32f, test_params)) - // Index kernels need identical values to test tie-breaking (first index wins) + // Index kernels need identical values to test tie-breaking (first index wins). + // The leading 0.0f puts the first maximum at index 1 rather than 0, so a + // kernel that tracks indices in the wrong lanes reports the wrong one. volk_test_params_t test_params_index(test_params.make_tol(0)); test_params_index.add_float_edge_cases({ + 0.0f, 1.0f, 1.0f, 1.0f,