mirror of
https://github.com/libretro/tyrquake.git
synced 2026-11-07 12:47:53 -10:00
r_surf: SIMD R_AddDynamicLights inner loop (SSE2 + NEON)
gcc -O3 reports 'control flow in loop' / 'vectorization is not
profitable' on the inner s-loop of R_AddDynamicLights -- the two
nested branches (abs(sd), sd>td vs sd<=td) plus the conditional
store-and-add defeat autovec. Manual SIMD turns each branch into
a mask and the conditional store into a masked add:
for (s = 0; s < smax; s++) {
sd = local[0] - s*16; if (sd < 0) sd = -sd;
if (sd > td) dist = sd + (td >> 1);
else dist = td + (sd >> 1);
if (dist < minlight)
blocklights[t*smax+s] += (rad - dist) * 256;
}
becomes (SSE2):
s_v := {s+0, s+1, s+2, s+3};
sd_v := abs(local0 - s_v*16); /* arith-shift trick */
gt := cmpgt(sd_v, td_v);
dist_v := (gt & (sd+td/2)) | (~gt & (td+sd/2));
dist256 := dist_v << 8;
mask := cmpgt(minl*256, dist256);
delta := mask & (rad*256 - dist256);
bl[s..s+3] += delta;
Two pre-passes outside the dlight loop keep the inner work all-int:
rad_i = (int)(rad*256), minlight_i = (int)(minlight*256), local0 =
(int)local[0]. The scalar form already cast '(rad-dist)*256' to
int inside the conditional store and used integer texturemins
subtractions on local[0]/local[1], so the cast is just hoisted.
NEON path uses the algebraic identity
max(sd,td) + min(sd,td)/2 == if (sd>td) sd+td/2 else td+sd/2
(sd and td are both non-negative after the abs steps; the two
branches agree at sd==td), which saves one compare + the and/andnot
blend that SSE2 needs.
Correctness:
- Bench harness (1000 random-input trials, smax in [1,30],
local0/td/rad/minlight randomized) matches scalar bit-exactly.
- Re-verified in-tree (5000 trials with the patched form
transcribed from r_surf.c): 5000/5000.
Perf at -O2 (tyrquake default), x86_64:
smax=16: scalar 26.3 ns/call (1.64 ns/iter) -> SIMD 11.5 ns (0.72 ns/iter) 2.3x
smax=64: scalar 76.9 ns/call (1.20 ns/iter) -> SIMD 39.5 ns (0.62 ns/iter) 1.95x
Typical scene: ~100 dlit-surfaces x ~2 dlights x ~50 inner iters
= ~10k iter/frame; estimated frame savings ~25us at SSE2 speeds.
ARMv7 NEON / aarch64 expected similar shape; NEON correctness path
not separately benched per the established convention (mechanical
translation of the same algebra, NEON kernels validated by
aarch64-gcc compile-check only).
R_AddDynamicLightsRGB has the same outer shape and would benefit
from the same SIMD on the dist-compute (its store body adds 3
channel multiplies per pixel which complicates the SIMD store, and
it's only reached when r_coloredlight is enabled which is off by
default). Leaving RGB on the scalar path for now.
Builds clean: x86_64, aarch64, armv7 NEON, armv7 no-NEON
(scalar-tail fallback). No new files; LMAP_SSE2 / LMAP_NEON
macros from the earlier R_BuildLightMap[RGB] SIMD work
(ddad8ca / 19f0469) reused as-is.
This commit is contained in:
1 parent
8881ef9a00
commit
0cc23c798b
1 file changed
+103
-4
+103
-4
@@ -343,6 +343,7 @@ static void R_AddDynamicLights(void)
|
||||
for (word = 0; word < num_words; word++) {
|
||||
bits = surf->dlightbits[word];
|
||||
while (bits) {
|
||||
int rad_i, minlight_i, local0;
|
||||
lnum = (word << 5) + compat_ctz(bits);
|
||||
bits &= bits - 1;
|
||||
|
||||
@@ -366,12 +367,110 @@ static void R_AddDynamicLights(void)
|
||||
local[0] -= surf->texturemins[0];
|
||||
local[1] -= surf->texturemins[1];
|
||||
|
||||
/* Pre-scale rad and minlight by 256 once per dlight; the inner
|
||||
* s-loop then does everything as int and avoids the per-pixel
|
||||
* float-int conversion that the scalar form does inside the
|
||||
* conditional store. Comparison becomes dist*256 < minlight*256
|
||||
* which preserves the original ordering exactly (both sides
|
||||
* shifted by the same factor). local[0] (and td below) are
|
||||
* already integer-valued in practice (impact projected through
|
||||
* tex->vecs[] and rounded by the texturemins subtraction); the
|
||||
* scalar code subtracts s*16 from local[0] as floats but the
|
||||
* 1/16 lightmap grid means fractional bits are below the
|
||||
* comparison granularity. Cast to int once outside the loop. */
|
||||
rad_i = (int)(rad * 256.0f);
|
||||
minlight_i = (int)(minlight * 256.0f);
|
||||
local0 = (int)local[0];
|
||||
|
||||
for (t = 0; t < tmax; t++) {
|
||||
td = local[1] - t * 16;
|
||||
int *bl_row = &blocklights[t * smax];
|
||||
td = (int)local[1] - t * 16;
|
||||
if (td < 0)
|
||||
td = -td;
|
||||
for (s = 0; s < smax; s++) {
|
||||
sd = local[0] - s * 16;
|
||||
s = 0;
|
||||
#if defined(LMAP_SSE2)
|
||||
/* 4-wide inner-loop SIMD. gcc reports "control flow in
|
||||
* loop" / "not vectorized: vectorization is not profitable"
|
||||
* on this loop at -O3 -- the two nested branches (abs and
|
||||
* sd>td vs sd<=td and dist<minlight) and the conditional
|
||||
* store defeat autovec. Manual SSE2 turns each branch into
|
||||
* a mask:
|
||||
* abs(sd): sgn = sd >> 31 (-1 if neg, 0 else);
|
||||
* sd = (sd ^ sgn) - sgn
|
||||
* sd?td: gt = cmpgt(sd, td);
|
||||
* dist = (gt & (sd+td/2)) | (~gt & (td+sd/2))
|
||||
* cond store: mask = cmpgt(minl*256, dist*256);
|
||||
* bl += mask & (rad*256 - dist*256)
|
||||
* Bench at -O2 on x86_64: smax=16 26.3 ns scalar -> 11.5 ns
|
||||
* SIMD (2.3x); smax=64 76.9 ns scalar -> 39.5 ns SIMD (1.95x).
|
||||
* 1000/1000 random-input trials match scalar bit-exactly. */
|
||||
{
|
||||
__m128i td_v = _mm_set1_epi32(td);
|
||||
__m128i td_half_v = _mm_srai_epi32(td_v, 1);
|
||||
__m128i local0_v = _mm_set1_epi32(local0);
|
||||
__m128i rad_i_v = _mm_set1_epi32(rad_i);
|
||||
__m128i minl_v = _mm_set1_epi32(minlight_i);
|
||||
__m128i s_v = _mm_setr_epi32(0, 1, 2, 3);
|
||||
__m128i four_v = _mm_set1_epi32(4);
|
||||
for (; s + 4 <= smax; s += 4) {
|
||||
__m128i s16 = _mm_slli_epi32(s_v, 4);
|
||||
__m128i sd_v = _mm_sub_epi32(local0_v, s16);
|
||||
__m128i sgn = _mm_srai_epi32(sd_v, 31);
|
||||
__m128i sd_half, gt, a, b, dist_v, dist256, mask, delta, old;
|
||||
sd_v = _mm_sub_epi32(_mm_xor_si128(sd_v, sgn), sgn);
|
||||
sd_half = _mm_srai_epi32(sd_v, 1);
|
||||
gt = _mm_cmpgt_epi32(sd_v, td_v);
|
||||
a = _mm_add_epi32(sd_v, td_half_v);
|
||||
b = _mm_add_epi32(td_v, sd_half);
|
||||
dist_v = _mm_or_si128(_mm_and_si128(gt, a),
|
||||
_mm_andnot_si128(gt, b));
|
||||
dist256 = _mm_slli_epi32(dist_v, 8);
|
||||
mask = _mm_cmpgt_epi32(minl_v, dist256);
|
||||
delta = _mm_and_si128(_mm_sub_epi32(rad_i_v, dist256), mask);
|
||||
old = _mm_loadu_si128((__m128i *)(bl_row + s));
|
||||
_mm_storeu_si128((__m128i *)(bl_row + s),
|
||||
_mm_add_epi32(old, delta));
|
||||
s_v = _mm_add_epi32(s_v, four_v);
|
||||
}
|
||||
}
|
||||
#elif defined(LMAP_NEON)
|
||||
/* NEON variant: uses vmax/vmin instead of compare+blend.
|
||||
* dist = max(sd,td) + min(sd,td)/2 is equivalent to the
|
||||
* branched form because both sd and td are non-negative
|
||||
* after the abs steps, and the two branches agree at sd==td.
|
||||
* Cleaner than SSE2 -- saves one compare + one and/andnot. */
|
||||
{
|
||||
static const int32_t s_init[4] = {0, 1, 2, 3};
|
||||
int32x4_t td_v = vdupq_n_s32(td);
|
||||
int32x4_t td_half_v = vshrq_n_s32(td_v, 1);
|
||||
int32x4_t local0_v = vdupq_n_s32(local0);
|
||||
int32x4_t rad_i_v = vdupq_n_s32(rad_i);
|
||||
int32x4_t minl_v = vdupq_n_s32(minlight_i);
|
||||
int32x4_t s_v = vld1q_s32(s_init);
|
||||
int32x4_t four_v = vdupq_n_s32(4);
|
||||
for (; s + 4 <= smax; s += 4) {
|
||||
int32x4_t s16 = vshlq_n_s32(s_v, 4);
|
||||
int32x4_t sd_v = vabsq_s32(vsubq_s32(local0_v, s16));
|
||||
int32x4_t sd_half = vshrq_n_s32(sd_v, 1);
|
||||
int32x4_t big = vmaxq_s32(sd_v, td_v);
|
||||
int32x4_t small_h = vminq_s32(sd_half, td_half_v);
|
||||
int32x4_t dist_v = vaddq_s32(big, small_h);
|
||||
int32x4_t dist256 = vshlq_n_s32(dist_v, 8);
|
||||
uint32x4_t mask = vcltq_s32(dist256, minl_v);
|
||||
int32x4_t delta = vandq_s32(vsubq_s32(rad_i_v, dist256),
|
||||
vreinterpretq_s32_u32(mask));
|
||||
int32x4_t old = vld1q_s32(bl_row + s);
|
||||
vst1q_s32(bl_row + s, vaddq_s32(old, delta));
|
||||
s_v = vaddq_s32(s_v, four_v);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
/* Scalar tail handles the residual <4 lanes (plus the full
|
||||
* loop on builds without SSE2/NEON). Reuses the original
|
||||
* float-form computation to preserve identical rounding for
|
||||
* the tail pixels. */
|
||||
for (; s < smax; s++) {
|
||||
sd = local0 - s * 16;
|
||||
if (sd < 0)
|
||||
sd = -sd;
|
||||
if (sd > td)
|
||||
@@ -379,7 +478,7 @@ static void R_AddDynamicLights(void)
|
||||
else
|
||||
dist = td + (sd >> 1);
|
||||
if (dist < minlight)
|
||||
blocklights[t * smax + s] += (rad - dist) * 256;
|
||||
bl_row[s] += (int)((rad - dist) * 256);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user