Files
LibretroAdmin 49a18c90c1 GSdx: make the unsigned float conversions defined, and the same on both hosts
Three sites convert a game-controlled float to an unsigned integer with a
plain cast. That is undefined outside the destination range, and the two
architectures the core ships on disagree rather than trapping:

                  x86-64      aarch64
    2^32          0           UINT32_MAX
    +inf          0           UINT32_MAX
    NaN           0           0
    -1.0          UINT32_MAX  0

The unsigned conversion is the worse of the two. It disagrees at both
ends, and -1.0 is not an edge case in the sense the others are -- it is a
small negative number, in range for a float and out of range only because
the destination has no sign.

GSTextureCache::ConvertDepthToColor is the one that shows. It scales a
normalised depth by 2^32, so a depth of exactly 1.0 -- the usual far-plane
clear -- lands on 2^32, one past UINT32_MAX. The colour came out black on
x86 and white on aarch64, from ordinary content rather than a malformed
draw.

GSRendererHW takes the top byte of Z as an alpha. The vertex trace holds Z
as (float)(u32), the full unsigned 32-bit value, so the top half of the Z
range does not fit an int: x86 gave INT32_MIN and the alpha came out -128,
aarch64 saturated and it came out 127.

Interpolate_UV narrows to u16. Its alpha comes from alpha0/alpha1, which
divide by the sprite's width or height, so a degenerate sprite makes it
infinite or NaN; and a merely negative t diverges too, for the u16 reason
above.

So add f32_to_u32_sat beside the existing f32_to_s32_sat and convert
through them. Both are bit-identical to the plain cast for every input the
plain cast had an answer for, so no well-formed draw moves.

zoverflow is left alone. It reads like a portable "is Z >= 2^31" test and
is not one -- it asks whether the host's conversion saturated, and x86
sets it where aarch64 does not. That is correct: it selects a workaround
only x86 needs, because _mm_cvttpd_epi32 has no unsigned form, while
aarch64's f64toi32 goes through int64 and narrows by truncation and is
exact over the whole unsigned range. The two divergences cancel, and the
direct path aarch64 takes is the more accurate of the two. Both copies now
say so, since the expression invites exactly the wrong repair.

tests/gs/gs_float_convert.cpp checks the helpers are defined for every
input, agree between hosts, and match the plain cast where it was defined;
build.sh runs it on x86 and again under qemu-aarch64. Inputs go through a
volatile -- gcc folds these conversions at compile time with its own
saturating semantics, which are not the ISA's, and a probe that lets it do
so reports the opposite of what the CPU does.
2026-09-22 15:07:13 +02:00

121 lines
4.2 KiB
C

/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2014- PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
// Hopefully this file will be used for cross-source math utilities.
// Currently these are strewn across the code base. Please collect them all!
#include <string.h>
#include "Pcsx2Defs.h"
/* ------------------------------------------------------------------
* Degenerate floats crossing into integers.
*
* Converting a float to an integer type is undefined for a NaN and for
* anything outside the destination range, and the two architectures this
* core ships on do not agree about what they produce:
*
* (s32)+inf (s32)NaN
* x86-64 INT32_MIN INT32_MIN
* aarch64 INT32_MAX 0
*
* so a draw takes a different path depending on the host. That matters
* wherever a texel coordinate is computed as S/Q: Q comes straight from
* the GIF register, and the mixer of substitutions GSState applies does
* not cover every path -- the fused packed handlers keep the zero-Q
* FLT_MIN swap but not the NaN one, and FLT_MIN turns a zero Q into an
* infinite quotient rather than a finite one anyway.
*
* These say what happens instead. q_is_usable and f32_to_s32_sat are
* mirrored on the paraLLEl-GS side, in pgs_vertex_kernels.h, written in
* C89 for the kernels there; keep those two in step. f32_to_u32_sat has
* no counterpart there -- paraLLEl-GS converts nothing to u32 on the CPU.
* ------------------------------------------------------------------ */
/* Whether a texel coordinate can be computed from this Q at all: a zero
* of either sign gives an infinity, and an infinity or a NaN gives a
* NaN. None of the three is a coordinate. */
static PCSX2_INLINE bool q_is_usable(float q)
{
u32 bits;
memcpy(&bits, &q, 4);
return (bits & 0x7fffffffu) != 0u &&
(bits & 0x7f800000u) != 0x7f800000u;
}
/* float to s32, saturating, NaN to zero. Truncates toward zero inside
* the range, exactly as the plain conversion does. */
static PCSX2_INLINE s32 f32_to_s32_sat(float v)
{
if (v != v)
return 0;
if (v >= 2147483648.0f)
return 2147483647;
if (v < -2147483648.0f)
return -2147483647 - 1;
return (s32)v;
}
/* float to u32, saturating, NaN to zero. The unsigned conversion is the
* worse of the two: it disagrees at both ends, and at values a signed
* conversion handles without trouble.
*
* x86-64 aarch64
* 2^32 0 UINT32_MAX
* +inf 0 UINT32_MAX
* NaN 0 0
* -1.0 UINT32_MAX 0
*
* -1.0 is not an edge case in the sense the others are -- it is a small
* negative number, in range for a float and out of range only because the
* destination has no sign. 2^32 is what a normalised depth of exactly 1.0
* scales to, which is the usual far-plane clear, so it is reached by
* ordinary content rather than by a malformed draw. */
static PCSX2_INLINE u32 f32_to_u32_sat(float v)
{
if (v != v)
return 0u;
if (v >= 4294967296.0f)
return 4294967295u;
if (v <= 0.0f)
return 0u;
return (u32)v;
}
static PCSX2_INLINE u32 count_leading_zero(s32 n)
{
#ifdef _MSC_VER
unsigned long ret;
_BitScanReverse(&ret, n);
return 31 - (u32)ret;
#else
return __builtin_clz(n);
#endif
}
// On GCC >= 4.7, this is equivalent to __builtin_clrsb(n);
static PCSX2_INLINE u32 count_leading_sign_bits(s32 n)
{
// If the sign bit is 1, we invert the bits to 0 for count-leading-zero.
if (n < 0)
n = ~n;
// If BSR is used directly, it would have an undefined value for 0.
if (n == 0)
return 32;
// Perform our count leading zero.
return count_leading_zero(n);
}