The Battle for Wesnoth  1.19.25+dev
utils_simd.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2025
3  by Durzi/mentos987
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 // This file contains optimisations for functions in the sdl/utils.cpp file.
17 // Optimisations use SIMD (SSE2/NEON) processing 4 pixels at a time.
18 
19 #include "utils_simd.hpp"
20 
21 // ============================================================================
22 // PLATFORM DETECTION
23 // ============================================================================
24 
25 // x86/x64 SSE2
26 #if defined(__SSE2__) || \
27  defined(_M_X64) || \
28  (defined(_MSC_VER) && defined(_M_IX86_FP) && _M_IX86_FP >= 2)
29 
30 #include <emmintrin.h>
31 #define SIMD_SSE2 1
32 #define SIMD_NEON 0
33 
34 // ARM NEON
35 #elif defined(__ARM_NEON) || defined(__aarch64__)
36 
37 #include <arm_neon.h>
38 #define SIMD_NEON 1
39 #define SIMD_SSE2 0
40 
41 // No SIMD support
42 #else
43 #define SIMD_SSE2 0
44 #define SIMD_NEON 0
45 #endif
46 
47 namespace {
48 
49 // ============================================================================
50 // SSE2 IMPLEMENTATIONS
51 // ============================================================================
52 
53 #if (SIMD_SSE2)
54 
55 void mask_surface_simd_sse2(uint32_t* surf_ptr, const uint32_t* mask_ptr, const std::size_t pixel_count, bool& empty)
56 {
57  // 1. Setup bit-mask for 128-bit registers (4 pixels each)
58  // Each pixel is 0xAARRGGBB.
59  // ALPHA_MASK isolates the top byte of each 32-bit lane.
60  const __m128i ALPHA_MASK = _mm_set1_epi32(0xFF000000);
61 
62  // This accumulator tracks if we've seen any non-zero alpha.
63  // If it remains all zeros, the entire surface is transparent.
64  __m128i alpha_acc = _mm_setzero_si128();
65 
66  // 2. Main Vector Loop
67  for(std::size_t offset = 0; offset < pixel_count; offset += 4) { // Process in blocks of 4
68  // Load 4 pixels from surface and 4 pixels from mask (unaligned load)
69  const __m128i s = _mm_loadu_si128(reinterpret_cast<const __m128i*>(surf_ptr + offset));
70  const __m128i m = _mm_loadu_si128(reinterpret_cast<const __m128i*>(mask_ptr + offset));
71 
72  // Compute the minimum of every byte in the registers.
73  // This effectively finds min(s.Alpha, m.Alpha), min(s.Red, m.Red), etc.
74  const __m128i minv = _mm_min_epu8(s, m);
75 
76  // We only want the calculated Alpha. Mask out the RGB components of the 'min' result.
77  const __m128i newa = _mm_and_si128(minv, ALPHA_MASK);
78 
79  // Construct final pixel: (Original Surface RGB) OR (New Masked Alpha)
80  // We use _mm_andnot_si128(ALPHA_MASK, s) to flip the mask and isolate RGB
81  const __m128i res = _mm_or_si128(_mm_andnot_si128(ALPHA_MASK, s), newa);
82 
83  // Store the 4 resulting pixels back to memory
84  _mm_storeu_si128(reinterpret_cast<__m128i*>(surf_ptr + offset), res);
85 
86  // Keep track of the alpha values. OR-ing ensures that if any bit
87  // in any alpha channel is 1, alpha_acc will eventually be non-zero.
88  alpha_acc = _mm_or_si128(alpha_acc, newa);
89  }
90 
91  // 3. Is the surface empty?
92  // Compare alpha_acc to a zero register. If equal, it returns 0xFF per byte.
93  // _mm_movemask_epi8 collects the most significant bit of each byte into a 16-bit int.
94  if(_mm_movemask_epi8(_mm_cmpeq_epi8(alpha_acc, _mm_setzero_si128())) != 0xFFFF) {
95  empty = false;
96  }
97 }
98 
99 void apply_surface_opacity_simd_sse2(uint32_t* surf_ptr, const std::size_t pixel_count, const uint8_t alpha_mod_scalar)
100 {
101  const __m128i ALPHA_MASK = _mm_set1_epi32(0xFF000000);
102 
103  // Prepare constants for fixed-point math: (a * mod * 0x10100 + 0x800000) >> 24
104  const __m128i MOD_VEC = _mm_set1_epi32(0x10100 * alpha_mod_scalar);
105  const __m128i C128 = _mm_set1_epi32(0x800000);
106 
107  for(std::size_t offset = 0; offset < pixel_count; offset += 4) {
108  const __m128i p = _mm_loadu_si128(reinterpret_cast<const __m128i*>(surf_ptr + offset));
109 
110  // Quick skip: if all 4 pixels have Alpha == 0, skip calculations.
111  if (_mm_movemask_epi8(_mm_cmpeq_epi8(_mm_and_si128(p, ALPHA_MASK), _mm_setzero_si128())) == 0xFFFF) continue;
112 
113  // Shift right by 24 bits so Alpha is in the lowest 8 bits of each 32-bit lane.
114  const __m128i a = _mm_srli_epi32(p, 24);
115 
116  // 32-bit Multiplication: m0 handles pixels 0 & 2, m1 handles pixels 1 & 3.
117  const __m128i m0 = _mm_mul_epu32(a, MOD_VEC);
118  const __m128i m1 = _mm_mul_epu32(_mm_srli_si128(a, 4), MOD_VEC);
119 
120  // Recombine results back into 32-bit lanes to perform rounding once.
121  const __m128i merged = _mm_or_si128(m0, _mm_slli_si128(m1, 4));
122 
123  // Round and apply the new alpha values to the alpha channel.
124  const __m128i new_alpha = _mm_and_si128(_mm_add_epi32(merged, C128), ALPHA_MASK);
125 
126  // Merge results back into the original surface pixels (preserving RGB channels).
127  _mm_storeu_si128(reinterpret_cast<__m128i*>(surf_ptr + offset),
128  _mm_or_si128(_mm_andnot_si128(ALPHA_MASK, p), new_alpha));
129  }
130 }
131 
132 void adjust_surface_color_simd_sse2(uint32_t* surf_ptr, const std::size_t pixel_count, int r, int g, int b)
133 {
134  r = std::clamp(r, -255, 255);
135  g = std::clamp(g, -255, 255);
136  b = std::clamp(b, -255, 255);
137 
138  // Prepare Addition and Subtraction masks for unsigned saturating arithmetic.
139  uint32_t add_mask = 0, sub_mask = 0;
140 
141  if(r > 0) add_mask |= (static_cast<uint8_t>(r) << 16);
142  else sub_mask |= (static_cast<uint8_t>(std::abs(r)) << 16);
143  if(g > 0) add_mask |= (static_cast<uint8_t>(g) << 8);
144  else sub_mask |= (static_cast<uint8_t>(std::abs(g)) << 8);
145  if(b > 0) add_mask |= static_cast<uint8_t>(b);
146  else sub_mask |= static_cast<uint8_t>(std::abs(b));
147 
148  const __m128i ADD_VEC = _mm_set1_epi32(add_mask);
149  const __m128i SUB_VEC = _mm_set1_epi32(sub_mask);
150 
151  for(std::size_t offset = 0; offset < pixel_count; offset += 4) {
152  __m128i p = _mm_loadu_si128(reinterpret_cast<const __m128i*>(surf_ptr + offset));
153 
154  // Use _mm_subs_epu8 and _mm_adds_epu8 to automatically handle 0-255 clamping.
155  p = _mm_subs_epu8(p, SUB_VEC);
156  p = _mm_adds_epu8(p, ADD_VEC);
157 
158  _mm_storeu_si128(reinterpret_cast<__m128i*>(surf_ptr + offset), p);
159  }
160 }
161 
162 void flip_image_simd_sse2(uint32_t* pixels, const std::size_t width, const std::size_t height)
163 {
164  const std::size_t half_width = width / 2;
165 
166  // Reorder lanes [0, 1, 2, 3] to [3, 2, 1, 0] to mirror the 4-pixel block.
167  auto reverse_sse2 = [](__m128i v) -> __m128i {
168  return _mm_shuffle_epi32(v, _MM_SHUFFLE(0, 1, 2, 3));
169  };
170 
171  for(std::size_t y = 0; y < height; ++y) {
172  uint32_t* row = pixels + y * width;
173  std::size_t x = 0;
174 
175  for(; x + 4 <= half_width; x += 4) { // 4 pixels from the left and right. May leave 0-3 pixels unprocessed in the middle.
176  const std::size_t r_idx = width - x - 4;
177 
178  __m128i l = _mm_loadu_si128(reinterpret_cast<const __m128i*>(row + x));
179  __m128i r = _mm_loadu_si128(reinterpret_cast<const __m128i*>(row + r_idx));
180 
181  _mm_storeu_si128(reinterpret_cast<__m128i*>(row + x), reverse_sse2(r));
182  _mm_storeu_si128(reinterpret_cast<__m128i*>(row + r_idx), reverse_sse2(l));
183  }
184  }
185 }
186 
187 #endif // (SIMD_SSE2)
188 
189 // ============================================================================
190 // NEON IMPLEMENTATIONS
191 // ============================================================================
192 
193 #if (SIMD_NEON)
194 
195 void mask_surface_simd_neon(uint32_t* __restrict surf_ptr, const uint32_t* __restrict mask_ptr, const std::size_t pixel_count, bool& empty)
196 {
197  // NEON uses 128-bit registers (uint32x4_t) to process 4 pixels.
198  const uint32x4_t ALPHA_MASK = vdupq_n_u32(0xFF000000);
199  uint32x4_t alpha_acc = vdupq_n_u32(0);
200 
201  for(std::size_t offset = 0; offset < pixel_count; offset += 4) {
202  // vld1q_u32 loads 4 contiguous 32-bit values.
203  const uint32x4_t s = vld1q_u32(surf_ptr + offset);
204  const uint32x4_t m = vld1q_u32(mask_ptr + offset);
205 
206  // Find minimum values across all channels.
207  const uint32x4_t min_val = vminq_u32(s, m);
208 
209  // Accumulate alpha bits to check for non-empty surface later.
210  alpha_acc = vorrq_u32(alpha_acc, min_val);
211 
212  // vbslq_u32 (Bitwise Select) acts as a ternary: ALPHA_MASK ? min_val : s
213  // This keeps original RGB while applying the new masked Alpha.
214  const uint32x4_t res = vbslq_u32(ALPHA_MASK, min_val, s);
215  vst1q_u32(surf_ptr + offset, res);
216  }
217 
218  // Isolate alpha bits and reduce the 128-bit vector to a single scalar check
219  // by OR-ing the register halves and finding the maximum lane value.
220  alpha_acc = vandq_u32(alpha_acc, ALPHA_MASK);
221  uint32x2_t combined_halves = vorr_u32(vget_low_u32(alpha_acc), vget_high_u32(alpha_acc));
222  combined_halves = vpmax_u32(combined_halves, combined_halves);
223  if(vget_lane_u32(combined_halves, 0) > 0) {
224  empty = false;
225  }
226 }
227 
228 void apply_surface_opacity_simd_neon(uint32_t* __restrict surf_ptr, const std::size_t pixel_count, const uint8_t alpha_mod_scalar)
229 {
230  const uint32x4_t ALPHA_MASK = vdupq_n_u32(0xFF000000);
231  const uint32x4_t MOD_VEC = vdupq_n_u32(0x10100 * alpha_mod_scalar);
232  const uint32x4_t C128 = vdupq_n_u32(0x800000);
233 
234  for(std::size_t offset = 0; offset < pixel_count; offset += 4) {
235  uint32x4_t p = vld1q_u32(surf_ptr + offset);
236 
237  uint32x4_t alpha_check = vandq_u32(p, ALPHA_MASK);
238  if (vgetq_lane_u64(vreinterpretq_u64_u32(alpha_check), 0) == 0 &&
239  vgetq_lane_u64(vreinterpretq_u64_u32(alpha_check), 1) == 0) continue;
240 
241  const uint32x4_t alpha_32 = vshrq_n_u32(p, 24);
242  const uint32x4_t new_alpha_shifted = vaddq_u32(vmulq_u32(alpha_32, MOD_VEC), C128);
243  const uint32x4_t res = vbslq_u32(ALPHA_MASK, new_alpha_shifted, p);
244 
245  vst1q_u32(surf_ptr + offset, res);
246  }
247 }
248 
249 void adjust_surface_color_simd_neon(uint32_t* __restrict surf_ptr, const std::size_t pixel_count, int r, int g, int b)
250 {
251  r = std::clamp(r, -255, 255);
252  g = std::clamp(g, -255, 255);
253  b = std::clamp(b, -255, 255);
254 
255  uint32_t add_pkd = ((r > 0 ? r : 0) << 16) | ((g > 0 ? g : 0) << 8) | (b > 0 ? b : 0);
256  uint32_t sub_pkd = ((r < 0 ? -r : 0) << 16) | ((g < 0 ? -g : 0) << 8) | (b < 0 ? -b : 0);
257 
258  const uint8x16_t ADD_VEC = vreinterpretq_u8_u32(vdupq_n_u32(add_pkd));
259  const uint8x16_t SUB_VEC = vreinterpretq_u8_u32(vdupq_n_u32(sub_pkd));
260 
261  for(std::size_t offset = 0; offset < pixel_count; offset += 4) {
262  // vqsubq/vqaddq handle saturating byte arithmetic (clamping to 0-255).
263  uint8x16_t p = vld1q_u8(reinterpret_cast<uint8_t*>(surf_ptr + offset));
264  p = vqaddq_u8(vqsubq_u8(p, SUB_VEC), ADD_VEC);
265  vst1q_u8(reinterpret_cast<uint8_t*>(surf_ptr + offset), p);
266  }
267 }
268 
269 void flip_image_simd_neon(uint32_t* __restrict pixels, const std::size_t width, const std::size_t height)
270 {
271  const std::size_t half_width = width / 2;
272 
273  // vrev64q_u32 reverses elements within 64-bit halves; vcombine/vget swaps the halves.
274  auto reverse_vector_u32 = [](uint32x4_t v) -> uint32x4_t {
275  v = vrev64q_u32(v);
276  return vcombine_u32(vget_high_u32(v), vget_low_u32(v));
277  };
278 
279  for(std::size_t y = 0; y < height; ++y) {
280  uint32_t* row = pixels + y * width;
281  std::size_t x = 0;
282 
283  for(; x + 4 <= half_width; x += 4) {
284  const std::size_t r_idx = width - x - 4;
285  const uint32x4_t L = vld1q_u32(row + x);
286  const uint32x4_t R = vld1q_u32(row + r_idx);
287 
288  vst1q_u32(row + x, reverse_vector_u32(R));
289  vst1q_u32(row + r_idx, reverse_vector_u32(L));
290  }
291  }
292 }
293 
294 #endif // (SIMD_NEON)
295 
296 } // End anonymous namespace
297 
298 // ============================================================================
299 // PUBLIC DISPATCHERS
300 // ============================================================================
301 
302 std::size_t mask_surface_simd(surface& surf, const surface& mask, bool& empty)
303 {
304  surface_lock lock(surf);
305  const_surface_lock mlock(mask);
306  const std::size_t simd_count = (surf.area() / 4) * 4;
307  if(simd_count <= simd::SIMD_THRESHOLD) return 0;
308 
309 #if (SIMD_SSE2)
310  mask_surface_simd_sse2(lock.pixels(), mlock.pixels(), simd_count, empty);
311  return simd_count;
312 #elif (SIMD_NEON)
313  mask_surface_simd_neon(lock.pixels(), mlock.pixels(), simd_count, empty);
314  return simd_count;
315 #else
316  return 0;
317 #endif
318 }
319 
320 std::size_t apply_surface_opacity_simd(surface& surf, const uint8_t alpha_mod)
321 {
322  surface_lock lock(surf);
323  const std::size_t simd_count = (surf.area() / 4) * 4;
324  if(simd_count <= simd::SIMD_THRESHOLD) return 0;
325 
326 #if (SIMD_SSE2)
327  apply_surface_opacity_simd_sse2(lock.pixels(), simd_count, alpha_mod);
328  return simd_count;
329 #elif (SIMD_NEON)
330  apply_surface_opacity_simd_neon(lock.pixels(), simd_count, alpha_mod);
331  return simd_count;
332 #else
333  return 0;
334 #endif
335 }
336 
337 std::size_t adjust_surface_color_simd(surface& surf, const int r, const int g, const int b)
338 {
339  surface_lock lock(surf);
340  const std::size_t simd_count = (surf.area() / 4) * 4;
341  if(simd_count <= simd::SIMD_THRESHOLD) return 0;
342 
343 #if (SIMD_SSE2)
344  adjust_surface_color_simd_sse2(lock.pixels(), simd_count, r, g, b);
345  return simd_count;
346 #elif (SIMD_NEON)
347  adjust_surface_color_simd_neon(lock.pixels(), simd_count, r, g, b);
348  return simd_count;
349 #else
350  return 0;
351 #endif
352 }
353 
355 {
356  surface_lock lock(surf);
357  const std::size_t simd_cols = (surf->w / 2) / 4 * 4;
358  if (surf.area() <= simd::SIMD_THRESHOLD) return 0;
359 
360 #if (SIMD_SSE2)
361  flip_image_simd_sse2(lock.pixels(), surf->w, surf->h);
362  return simd_cols;
363 #elif (SIMD_NEON)
364  flip_image_simd_neon(lock.pixels(), surf->w, surf->h);
365  return simd_cols;
366 #else
367  return 0;
368 #endif
369 }
double g
Definition: astarsearch.cpp:63
Helper class for pinning SDL surfaces into memory.
Definition: surface.hpp:98
pixel_t * pixels() const
Definition: surface.hpp:117
std::size_t area() const
Total area of the surface in square pixels.
Definition: surface.cpp:115
constexpr std::size_t SIMD_THRESHOLD
Minimum number of pixels required to trigger SIMD optimizations.
Definition: utils_simd.hpp:35
surface surf
Image.
mock_party p
static map_location::direction s
std::size_t flip_image_simd(surface &surf)
Flips each row of pixels (horizontal mirror) using SIMD.
Definition: utils_simd.cpp:354
std::size_t adjust_surface_color_simd(surface &surf, const int r, const int g, const int b)
Adjusts the color channels of a surface using saturated SIMD arithmetic.
Definition: utils_simd.cpp:337
std::size_t apply_surface_opacity_simd(surface &surf, const uint8_t alpha_mod)
Applies an alpha modification to the whole surface using SIMD.
Definition: utils_simd.cpp:320
std::size_t mask_surface_simd(surface &surf, const surface &mask, bool &empty)
Modifies the alpha channel of the surface pixels based on the mask.
Definition: utils_simd.cpp:302
SIMD-accelerated helper functions for image manipulation.
#define b