Files
RetroArch/libretro-common/audio/conversion/float_to_s16.c
T
LibretroAdmin cabaa7b04d x86 SIMD: build only baseline ISA tiers under PlayStation clang
Clang's immintrin.h for PlayStation targets (and clang-cl) declares
only the intrinsics of ISAs the whole build enables, so the AVX2 and
AVX-512 kernels that sinc_resampler, pixconv, float_to_s16 and
state_manager compile under per-function targets do not exist there.
On those compilers a tier is built only when the baseline carries it:
the PS4 keeps sinc's AVX kernel, the PS5 keeps the AVX2 tiers, and the
run-time choice never picks a tier that was left out.
2026-10-27 20:39:56 +01:00

442 lines
17 KiB
C

/* Copyright (C) 2010-2021 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (float_to_s16.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* retro_environment.h spells MSVC's SSE/SSE2 targets as __SSE__/__SSE2__,
* which the SIMD paths below test. */
#include <retro_environment.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#if defined(__SSE2__)
#include <emmintrin.h>
#elif defined(__ALTIVEC__)
#include <altivec.h>
#endif
#include <features/features_cpu.h>
#include <audio/conversion/float_to_s16.h>
#if (defined(__ARM_NEON) || defined(__ARM_NEON__) || defined(HAVE_NEON))
static bool float_to_s16_neon_enabled = false;
#ifdef HAVE_ARM_NEON_ASM_OPTIMIZATIONS
void convert_float_s16_asm(int16_t *s, const float *in, size_t len);
#else
#include <arm_neon.h>
#endif
void convert_float_to_s16(int16_t *s, const float *in, size_t len)
{
size_t i = 0;
if (float_to_s16_neon_enabled)
{
#ifndef HAVE_ARM_NEON_ASM_OPTIMIZATIONS
/* arm_neon.h is only included when the intrinsic path is built,
* so these NEON-typed locals must be scoped to it as well - the
* asm path calls out to float_to_s16_neon.S and needs none of
* them. s16_to_float.c already keeps its equivalents inside the
* #else for the same reason. */
float gf = (1<<15);
float32x4_t vgf = {gf, gf, gf, gf};
float32x4_t vhalf = vdupq_n_f32(0.5f);
uint32x4_t vsign = vdupq_n_u32(0x80000000u);
#endif
while (len >= 8)
{
#ifdef HAVE_ARM_NEON_ASM_OPTIMIZATIONS
size_t aligned_samples = len & ~7;
if (aligned_samples)
convert_float_s16_asm(s, in, aligned_samples);
s += aligned_samples;
in += aligned_samples;
len -= aligned_samples;
i = 0;
#else
int16x4x2_t oreg;
int32x4x2_t creg;
float32x4x2_t inreg = vld2q_f32(in);
float32x4_t sc0 = vmulq_f32(inreg.val[0], vgf);
float32x4_t sc1 = vmulq_f32(inreg.val[1], vgf);
float32x4_t b0 = vreinterpretq_f32_u32(vorrq_u32(
vandq_u32(vreinterpretq_u32_f32(sc0), vsign),
vreinterpretq_u32_f32(vhalf)));
float32x4_t b1 = vreinterpretq_f32_u32(vorrq_u32(
vandq_u32(vreinterpretq_u32_f32(sc1), vsign),
vreinterpretq_u32_f32(vhalf)));
creg.val[0] = vcvtq_s32_f32(vaddq_f32(sc0, b0));
creg.val[1] = vcvtq_s32_f32(vaddq_f32(sc1, b1));
oreg.val[0] = vqmovn_s32(creg.val[0]);
oreg.val[1] = vqmovn_s32(creg.val[1]);
vst2_s16(s, oreg);
in += 8;
s += 8;
len -= 8;
#endif
}
}
for (; i < len; i++)
{
float scaled = in[i] * 0x8000;
uint32_t bits;
/* Casting a non-finite or out-of-int32-range float is undefined,
* and on x86 it yields the "integer indefinite" INT32_MIN, which
* then saturates to -32768 - so a NaN or a large positive sample
* came out as full-scale negative. The NEON and AltiVec paths
* already give NaN -> 0 with correct saturation; match them. The
* NaN test is on the bit pattern rather than (scaled != scaled)
* because -ffast-math is entitled to fold the latter to false. */
memcpy(&bits, &scaled, sizeof(bits));
if ((bits & 0x7FFFFFFFu) > 0x7F800000u)
s[i] = 0;
else
{
scaled += (scaled >= 0.0f ? 0.5f : -0.5f);
if (scaled > 32767.0f)
s[i] = 0x7FFF;
else if (scaled < -32768.0f)
s[i] = -0x8000;
else
s[i] = (int16_t)(int32_t)scaled;
}
}
}
void convert_float_to_s16_init_simd(void)
{
uint64_t cpu = cpu_features_get();
if (cpu & RETRO_SIMD_NEON)
float_to_s16_neon_enabled = true;
}
#else
/* AVX2 and AVX-512 forms of the SSE2 loop below, built per function and
* chosen by convert_float_to_s16_init_simd(), so older CPUs keep SSE2
* and compilers without per-function targets leave them out. They run
* the SSE2 arithmetic step for step, so every target still yields the
* same s16 for the same float. */
#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 FLOAT_TO_S16_HAVE_WIDE 1
#define FLOAT_TO_S16_HAVE_AVX512 1
#define FLOAT_TO_S16_TARGET_AVX2
#define FLOAT_TO_S16_TARGET_AVX512
#endif
#elif defined(__clang__) && (defined(__SCE__) || defined(_MSC_VER))
/* Clang's immintrin.h for PlayStation and clang-cl only declares the
* intrinsics of ISAs enabled for the whole build, so per-function
* targets cannot reach them; build a tier only when the baseline has
* it (PS5's Zen 2 keeps AVX2, PS4's Jaguar has neither). */
#ifdef __AVX2__
#define FLOAT_TO_S16_HAVE_WIDE 1
#define FLOAT_TO_S16_TARGET_AVX2
#ifdef __AVX512F__
#define FLOAT_TO_S16_HAVE_AVX512 1
#define FLOAT_TO_S16_TARGET_AVX512
#endif
#endif
#elif (defined(__clang__) && __clang_major__ >= 4) \
|| (defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 5)
#define FLOAT_TO_S16_HAVE_WIDE 1
#define FLOAT_TO_S16_HAVE_AVX512 1
#define FLOAT_TO_S16_TARGET_AVX2 __attribute__((target("avx2")))
#define FLOAT_TO_S16_TARGET_AVX512 __attribute__((target("avx512f")))
#endif
#endif
#ifdef FLOAT_TO_S16_HAVE_WIDE
#include <immintrin.h>
/* 0 = SSE2, 1 = AVX2, 2 = AVX-512F; set once by init, before audio runs. */
static int float_to_s16_tier = 0;
#ifdef FLOAT_TO_S16_TEST_HOOKS
/* -1 leaves the tier alone; otherwise caps it for a test. */
int float_to_s16_tier_cap = -1;
#endif
#if defined(_MSC_VER) && !defined(__clang__)
#define FLOAT_TO_S16_ZEROUPPER() _mm256_zeroupper()
#else
#define FLOAT_TO_S16_ZEROUPPER()
#endif
/* Sixteen samples a step; returns how many it converted, leaving the
* rest (under 16) to the SSE2 loop and the scalar tail. */
FLOAT_TO_S16_TARGET_AVX2
static size_t float_to_s16_avx2(int16_t *s, const float *in, size_t len)
{
size_t i;
const __m256 factor = _mm256_set1_ps((float)0x8000);
const __m256 half = _mm256_set1_ps(0.5f);
const __m256 signmask = _mm256_castsi256_ps(
_mm256_set1_epi32((int)0x80000000));
const __m256 vmax = _mm256_set1_ps( 32767.0f);
const __m256 vmin = _mm256_set1_ps(-32768.0f);
for (i = 0; i + 16 <= len; i += 16)
{
__m256 a = _mm256_mul_ps(_mm256_loadu_ps(in + i), factor);
__m256 b = _mm256_mul_ps(_mm256_loadu_ps(in + i + 8), factor);
__m256i packed;
a = _mm256_and_ps(a, _mm256_cmp_ps(a, a, _CMP_ORD_Q));
b = _mm256_and_ps(b, _mm256_cmp_ps(b, b, _CMP_ORD_Q));
a = _mm256_max_ps(_mm256_min_ps(_mm256_add_ps(a, _mm256_or_ps(
_mm256_and_ps(a, signmask), half)), vmax), vmin);
b = _mm256_max_ps(_mm256_min_ps(_mm256_add_ps(b, _mm256_or_ps(
_mm256_and_ps(b, signmask), half)), vmax), vmin);
/* packs works per 128-bit lane: a0-3 b0-3 a4-7 b4-7; the
* permute restores sample order. */
packed = _mm256_packs_epi32(_mm256_cvttps_epi32(a),
_mm256_cvttps_epi32(b));
_mm256_storeu_si256((__m256i*)(s + i),
_mm256_permute4x64_epi64(packed, 0xD8));
}
FLOAT_TO_S16_ZEROUPPER();
return i;
}
/* Sixteen samples a step, the last step masked: the loads and the
* saturating narrow-and-store skip lanes past the end instead of a
* scalar tail, so it converts the whole buffer. */
#ifdef FLOAT_TO_S16_HAVE_AVX512
FLOAT_TO_S16_TARGET_AVX512
static void float_to_s16_avx512(int16_t *s, const float *in, size_t len)
{
size_t i;
const __m512 factor = _mm512_set1_ps((float)0x8000);
const __m512i half = _mm512_castps_si512(_mm512_set1_ps(0.5f));
const __m512i signmask = _mm512_set1_epi32((int)0x80000000);
const __m512 vmax = _mm512_set1_ps( 32767.0f);
const __m512 vmin = _mm512_set1_ps(-32768.0f);
for (i = 0; i < len; i += 16)
{
size_t left = len - i;
__mmask16 m = (left >= 16) ? (__mmask16)0xffff
: (__mmask16)((1u << left) - 1u);
__m512 x = _mm512_mul_ps(_mm512_maskz_loadu_ps(m, in + i),
factor);
__m512 bias;
/* NaN to +0, as the SSE2 cmpord/and does. */
x = _mm512_maskz_mov_ps(_mm512_cmp_ps_mask(x, x, _CMP_ORD_Q), x);
bias = _mm512_castsi512_ps(_mm512_or_si512(
_mm512_and_si512(_mm512_castps_si512(x), signmask), half));
x = _mm512_max_ps(_mm512_min_ps(_mm512_add_ps(x, bias), vmax),
vmin);
_mm512_mask_cvtsepi32_storeu_epi16(s + i, m, _mm512_cvttps_epi32(x));
}
FLOAT_TO_S16_ZEROUPPER();
}
#endif
#endif
/* The SSE2 / AltiVec / Allegrex loop and the scalar tail. */
static void convert_float_to_s16_base(int16_t *s, const float *in, size_t len)
{
size_t i = 0;
#if defined(__SSE2__)
__m128 factor = _mm_set1_ps((float)0x8000);
/* Initialize a 4D vector with 32768.0 for its elements */
__m128 half = _mm_set1_ps(0.5f);
__m128 signmask = _mm_castsi128_ps(_mm_set1_epi32((int)0x80000000));
__m128 vmax = _mm_set1_ps( 32767.0f);
__m128 vmin = _mm_set1_ps(-32768.0f);
/* Round half away from zero: add copysign(0.5, x) then truncate.
* This matches the scalar fallback and every other SIMD variant
* bit-for-bit, so the same float buffer yields identical s16 output
* on all targets (netplay / rewind / regression fixtures).
*
* NaN is squashed to +0.0 and the biased value clamped into the s16
* range before the convert: _mm_cvttps_epi32 returns INT32_MIN for
* NaN and for anything outside int32, which _mm_packs_epi32 then
* saturates to -32768 - so a NaN or a large positive sample became
* full-scale negative. NEON and AltiVec already give NaN -> 0 with
* correct saturation. _mm_cmpord_ps survives -ffast-math (it lowers
* straight to cmpordps); a plain float compare would not. */
for (i = 0; i + 8 <= len; i += 8, in += 8, s += 8)
{ /* Skip forward 8 samples at a time... */
__m128 res_a = _mm_mul_ps(_mm_loadu_ps(in + 0), factor); /* next four samples * 32768 */
__m128 res_b = _mm_mul_ps(_mm_loadu_ps(in + 4), factor); /* the *next* next four */
__m128 bias_a, bias_b;
__m128i ints_a, ints_b, packed;
res_a = _mm_and_ps(res_a, _mm_cmpord_ps(res_a, res_a));
res_b = _mm_and_ps(res_b, _mm_cmpord_ps(res_b, res_b));
bias_a = _mm_or_ps(_mm_and_ps(res_a, signmask), half); /* copysign(0.5, res) */
bias_b = _mm_or_ps(_mm_and_ps(res_b, signmask), half);
res_a = _mm_max_ps(_mm_min_ps(_mm_add_ps(res_a, bias_a), vmax), vmin);
res_b = _mm_max_ps(_mm_min_ps(_mm_add_ps(res_b, bias_b), vmax), vmin);
ints_a = _mm_cvttps_epi32(res_a); /* rounded, truncating cvt */
ints_b = _mm_cvttps_epi32(res_b);
packed = _mm_packs_epi32(ints_a, ints_b); /* Then to 16-bit, clamping to [-32768, 32767] */
_mm_storeu_si128((__m128i *)s, packed); /* Then put the result in the output array */
}
len = len - i;
i = 0;
/* If there are any stray samples at the end, we need to convert them
* (maybe the original array didn't contain a multiple of 8 samples) */
#elif defined(__ALTIVEC__)
int samples_in = len;
/* Unaligned loads/store is a bit expensive,
* so we optimize for the good path (very likely). */
if (((uintptr_t)s & 15) + ((uintptr_t)in & 15) == 0)
{
size_t i;
const vector float vscale =
(vector float){32768.0f, 32768.0f, 32768.0f, 32768.0f};
const vector float vzero =
(vector float){0.0f, 0.0f, 0.0f, 0.0f};
const vector float vhalf =
(vector float){0.5f, 0.5f, 0.5f, 0.5f};
const vector unsigned int vsign =
(vector unsigned int){0x80000000u, 0x80000000u,
0x80000000u, 0x80000000u};
/* Round half away from zero, matching the scalar/SSE2/NEON paths. */
for (i = 0; i + 8 <= len; i += 8, in += 8, s += 8)
{
vector float input0 = vec_ld( 0, in);
vector float input1 = vec_ld(16, in);
vector float sc0 = vec_madd(input0, vscale, vzero);
vector float sc1 = vec_madd(input1, vscale, vzero);
vector float b0 = (vector float)vec_or(
vec_and((vector unsigned int)sc0, vsign),
(vector unsigned int)vhalf);
vector float b1 = (vector float)vec_or(
vec_and((vector unsigned int)sc1, vsign),
(vector unsigned int)vhalf);
vector signed int result0 = vec_cts(vec_add(sc0, b0), 0);
vector signed int result1 = vec_cts(vec_add(sc1, b1), 0);
vec_st(vec_packs(result0, result1), 0, s);
}
samples_in -= i;
}
len = samples_in;
i = 0;
#elif defined(_MIPS_ARCH_ALLEGREX)
#ifdef DEBUG
/* Make sure the buffers are 16 byte aligned, this should be
* the default behaviour of malloc in the PSPSDK.
* Assume alignment. */
retro_assert(((uintptr_t)in & 0xf) == 0);
retro_assert(((uintptr_t)s & 0xf) == 0);
#endif
for (i = 0; i + 8 <= len; i += 8)
{
__asm__ (
".set push \n"
".set noreorder \n"
"lv.q c100, 0(%0) \n"
"lv.q c110, 16(%0) \n"
"vf2in.q c100, c100, 31 \n"
"vf2in.q c110, c110, 31 \n"
"vi2s.q c100, c100 \n"
"vi2s.q c102, c110 \n"
"sv.q c100, 0(%1) \n"
".set pop \n"
:: "r"(in + i), "r"(s + i));
}
#endif
/* This loop converts stray samples to the right format,
* but it's also a fallback in case no SIMD instructions are available. */
for (; i < len; i++)
{
float scaled = in[i] * 0x8000;
uint32_t bits;
/* Casting a non-finite or out-of-int32-range float is undefined,
* and on x86 it yields the "integer indefinite" INT32_MIN, which
* then saturates to -32768 - so a NaN or a large positive sample
* came out as full-scale negative. The NEON and AltiVec paths
* already give NaN -> 0 with correct saturation; match them. The
* NaN test is on the bit pattern rather than (scaled != scaled)
* because -ffast-math is entitled to fold the latter to false. */
memcpy(&bits, &scaled, sizeof(bits));
if ((bits & 0x7FFFFFFFu) > 0x7F800000u)
s[i] = 0;
else
{
scaled += (scaled >= 0.0f ? 0.5f : -0.5f);
if (scaled > 32767.0f)
s[i] = 0x7FFF;
else if (scaled < -32768.0f)
s[i] = -0x8000;
else
s[i] = (int16_t)(int32_t)scaled;
}
}
}
void convert_float_to_s16(int16_t *s, const float *in, size_t len)
{
#ifdef FLOAT_TO_S16_HAVE_WIDE
int tier = float_to_s16_tier;
#ifdef FLOAT_TO_S16_TEST_HOOKS
if (float_to_s16_tier_cap >= 0 && tier > float_to_s16_tier_cap)
tier = float_to_s16_tier_cap;
#endif
#ifdef FLOAT_TO_S16_HAVE_AVX512
if (tier == 2)
{
float_to_s16_avx512(s, in, len);
return;
}
#endif
if (tier == 1)
{
size_t done = float_to_s16_avx2(s, in, len);
s += done;
in += done;
len -= done;
}
#endif
convert_float_to_s16_base(s, in, len);
}
void convert_float_to_s16_init_simd(void)
{
#ifdef FLOAT_TO_S16_HAVE_WIDE
uint64_t cpu = cpu_features_get();
float_to_s16_tier = 0;
if (cpu & RETRO_SIMD_AVX2)
float_to_s16_tier = 1;
#ifdef FLOAT_TO_S16_HAVE_AVX512
if (cpu & RETRO_SIMD_AVX512)
float_to_s16_tier = 2;
#endif
#endif
}
#endif