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.