The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
47namespace {
48
49// ============================================================================
50// SSE2 IMPLEMENTATIONS
51// ============================================================================
52
53#if (SIMD_SSE2)
54
55void 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
99void 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
132void 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
162void 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
195void 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
228void 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
249void 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
269void 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
302std::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
320std::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
337std::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}
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.
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.
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.
std::size_t apply_surface_opacity_simd(surface &surf, const uint8_t alpha_mod)
Applies an alpha modification to the whole surface using SIMD.
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.
SIMD-accelerated helper functions for image manipulation.
#define g
#define b