s16_to_float: run-time AVX2 and AVX-512 conversion

convert_s16_to_float widens every s16 core's audio before the
resampler, on every flush. It gains AVX2 and AVX-512 forms of the SSE2
loop, picked by convert_s16_to_float_init_simd() from
cpu_features_get(), built per function so older CPUs keep SSE2 and
compilers without per-function targets (before GCC 5, Clang 4, MSVC
2017 15.3) leave them out.

Each sample is an exact sign-extend and convert, then one multiply by
gain / 32768: the same single rounding as the SSE2 and scalar paths,
so every target gives the same float for the same sample. AVX2 takes
16 samples a step; AVX-512 masks its last step, so it has no scalar
tail.

One 534-frame stereo batch (1068 samples) on an AVX-512 Xeon:
SSE2 0.14 us, AVX2 0.07-0.09 us, AVX-512 0.08-0.10 us.

New libretro-common/samples/audio/s16_to_float pins each tier the CPU
has and compares it bit for bit with the scalar rule: every s16 value
at six gains, then every length up to 100 at odd offsets, with a guard
word after each buffer.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-27 20:39:36 +01:00
1 parent 5811daa1e5
commit 7b23ab8aa4
4 files changed
+246 -2

No files matched your search

@@ -103,6 +103,7 @@ jobs:
# failure. These are built AND executed.
declare -a RUN_TARGETS=(
rslc_test
s16_to_float_test
float_to_s16_test
float_to_s32_test
compat_fnmatch_test
+119 -2
View File
@@ -94,7 +94,89 @@ void convert_s16_to_float_init_simd(void)
s16_to_float_neon_enabled = true;
}
#else
void convert_s16_to_float(float *s,
/* AVX2 and AVX-512 forms of the SSE2 loop below, built per function and
* chosen by convert_s16_to_float_init_simd(), so older CPUs keep SSE2
* and compilers without per-function targets leave them out. Each
* sample is one exact widen and one multiply by gain / 32768 - the same
* single rounding as the SSE2 and scalar paths, so every target gives
* the same float for the same s16. */
#if defined(__SSE2__) && (defined(__x86_64__) || defined(__i386__) \
|| defined(_M_X64) || defined(_M_IX86))
#if defined(_MSC_VER) && !defined(__clang__)
#if _MSC_VER >= 1911
#define S16_TO_FLOAT_HAVE_WIDE 1
#define S16_TO_FLOAT_TARGET_AVX2
#define S16_TO_FLOAT_TARGET_AVX512
#endif
#elif (defined(__clang__) && __clang_major__ >= 4) \
|| (defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 5)
#define S16_TO_FLOAT_HAVE_WIDE 1
#define S16_TO_FLOAT_TARGET_AVX2 __attribute__((target("avx2")))
#define S16_TO_FLOAT_TARGET_AVX512 \
__attribute__((target("avx512f,avx512bw,avx512vl")))
#endif
#endif
#ifdef S16_TO_FLOAT_HAVE_WIDE
#include <immintrin.h>
/* 0 = SSE2, 1 = AVX2, 2 = AVX-512; set once by init, before audio runs. */
static int s16_to_float_tier = 0;
#ifdef S16_TO_FLOAT_TEST_HOOKS
/* -1 leaves the tier alone; otherwise caps it for a test. */
int s16_to_float_tier_cap = -1;
#endif
#if defined(_MSC_VER) && !defined(__clang__)
#define S16_TO_FLOAT_ZEROUPPER() _mm256_zeroupper()
#else
#define S16_TO_FLOAT_ZEROUPPER()
#endif
/* Sixteen samples a step; returns how many it converted, leaving the
* rest (under 16) to the SSE2 loop and the scalar tail. */
S16_TO_FLOAT_TARGET_AVX2
static size_t s16_to_float_avx2(float *s, const int16_t *in, size_t len,
float gain)
{
size_t i;
const __m256 factor = _mm256_set1_ps(gain / 0x8000);
for (i = 0; i + 16 <= len; i += 16)
{
_mm256_storeu_ps(s + i, _mm256_mul_ps(_mm256_cvtepi32_ps(
_mm256_cvtepi16_epi32(_mm_loadu_si128(
(const __m128i*)(in + i)))), factor));
_mm256_storeu_ps(s + i + 8, _mm256_mul_ps(_mm256_cvtepi32_ps(
_mm256_cvtepi16_epi32(_mm_loadu_si128(
(const __m128i*)(in + i + 8)))), factor));
}
S16_TO_FLOAT_ZEROUPPER();
return i;
}
/* Sixteen samples a step, the last masked, so no scalar tail. */
S16_TO_FLOAT_TARGET_AVX512
static void s16_to_float_avx512(float *s, const int16_t *in, size_t len,
float gain)
{
size_t i;
const __m512 factor = _mm512_set1_ps(gain / 0x8000);
for (i = 0; i < len; i += 16)
{
size_t left = len - i;
__mmask16 m = (left >= 16) ? (__mmask16)0xffff
: (__mmask16)((1u << left) - 1u);
_mm512_mask_storeu_ps(s + i, m, _mm512_mul_ps(_mm512_cvtepi32_ps(
_mm512_cvtepi16_epi32(_mm256_maskz_loadu_epi16(m,
in + i))), factor));
}
S16_TO_FLOAT_ZEROUPPER();
}
#endif
/* The SSE2 / AltiVec / Allegrex loop and the scalar tail. */
static void convert_s16_to_float_base(float *s,
const int16_t *in, size_t len, float gain)
{
unsigned i = 0;
@@ -204,6 +286,41 @@ void convert_s16_to_float(float *s,
s[i] = (float)in[i] * gain;
}
void convert_s16_to_float_init_simd(void) { }
void convert_s16_to_float(float *s,
const int16_t *in, size_t len, float gain)
{
#ifdef S16_TO_FLOAT_HAVE_WIDE
int tier = s16_to_float_tier;
#ifdef S16_TO_FLOAT_TEST_HOOKS
if (s16_to_float_tier_cap >= 0 && tier > s16_to_float_tier_cap)
tier = s16_to_float_tier_cap;
#endif
if (tier == 2)
{
s16_to_float_avx512(s, in, len, gain);
return;
}
if (tier == 1)
{
size_t done = s16_to_float_avx2(s, in, len, gain);
s += done;
in += done;
len -= done;
}
#endif
convert_s16_to_float_base(s, in, len, gain);
}
void convert_s16_to_float_init_simd(void)
{
#ifdef S16_TO_FLOAT_HAVE_WIDE
uint64_t cpu = cpu_features_get();
s16_to_float_tier = 0;
if (cpu & RETRO_SIMD_AVX2)
s16_to_float_tier = 1;
if (cpu & RETRO_SIMD_AVX512)
s16_to_float_tier = 2;
#endif
}
#endif
@@ -0,0 +1,39 @@
include ../../sample_objdir.mk
TARGET := s16_to_float_test
LIBRETRO_COMM_DIR := ../../..
COMMON_SOURCES := \
$(LIBRETRO_COMM_DIR)/audio/conversion/s16_to_float.c \
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c
COMMON_OBJS := $(call sample_objs,$(COMMON_SOURCES:.c=.o))
OBJS := s16_to_float_test.o $(COMMON_OBJS)
CFLAGS += -DS16_TO_FLOAT_TEST_HOOKS -Wall -pedantic -std=gnu99 -g1 -O2 -I$(LIBRETRO_COMM_DIR)/include
LDFLAGS += -lm
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
all: $(TARGET)
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
$(SAMPLE_OBJDIR)/%.o: $$(call sample_src,$$*).c
@mkdir -p $(@D)
$(CC) -c -o $@ $< $(CFLAGS)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
clean:
rm -rf $(SAMPLE_OBJDIR)
rm -f $(TARGET) $(OBJS)
.PHONY: clean
@@ -0,0 +1,87 @@
/* convert_s16_to_float against the scalar rule, (float)x * (gain /
* 32768), bit for bit: every s16 value at several gains, then every
* length up to 100 at odd offsets so each vector body, the AVX-512
* masked tail and the scalar remainder run, with a guard word after
* each buffer. Every tier the CPU has (SSE2, AVX2, AVX-512) is pinned
* in turn; all must give the same float for the same sample. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <audio/conversion/s16_to_float.h>
#ifdef S16_TO_FLOAT_TEST_HOOKS
extern int s16_to_float_tier_cap;
#endif
static unsigned failures;
static void check(const int16_t *in, size_t n, float gain,
const char *what, int tier)
{
float *out = (float*)malloc((n + 1) * sizeof(*out));
float g = gain / 0x8000;
uint32_t guard = 0x5A5A5A5Au;
size_t i;
unsigned bad = 0;
memcpy(&out[n], &guard, sizeof(guard));
convert_s16_to_float(out, in, n, gain);
for (i = 0; i < n; i++)
{
float want = (float)in[i] * g;
if (memcmp(&out[i], &want, sizeof(want)) && bad++ < 4)
printf("FAIL tier %d %s gain %g n=%u [%u]: in %d got %.9g want %.9g\n",
tier, what, gain, (unsigned)n, (unsigned)i, in[i],
out[i], want);
}
if (memcmp(&out[n], &guard, sizeof(guard)))
{
printf("FAIL tier %d %s n=%u: wrote past the end\n", tier, what,
(unsigned)n);
bad++;
}
failures += bad ? 1 : 0;
free(out);
}
int main(void)
{
static const float gains[] = { 1.0f, 0.5f, 0.0f, 1.37f, 3.98107f,
0.000123f };
int16_t *in = (int16_t*)malloc(65536 * sizeof(int16_t));
size_t i, n, g;
int tier;
for (i = 0; i < 65536; i++)
in[i] = (int16_t)(uint16_t)(i * 40503u);
convert_s16_to_float_init_simd();
for (tier = 0; tier <= 2; tier++)
{
#ifdef S16_TO_FLOAT_TEST_HOOKS
s16_to_float_tier_cap = tier;
#else
if (tier)
break;
#endif
for (g = 0; g < sizeof(gains) / sizeof(gains[0]); g++)
check(in, 65536, gains[g], "every value", tier);
for (n = 0; n <= 100; n++)
{
check(in, n, 1.0f, "length", tier);
check(in + 1 + (n % 7), n, 0.75f, "offset", tier);
}
}
free(in);
if (failures)
{
printf("s16_to_float_test: %u failing cases\n", failures);
return 1;
}
printf("s16_to_float_test: every tier matches the scalar rule\n");
return 0;
}