OXYGENHUB
◆ main openmsx / src / video / scalers / LineScalers.hh
16.6 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#ifndef LINESCALERS_HH
2
#define LINESCALERS_HH
3
4
#include "PixelOperations.hh"
5
6
#include "ranges.hh"
7
#include "xrange.hh"
8
9
#include <bit>
10
#include <cassert>
11
#include <cstddef>
12
#include <cstdint>
13
#include <ranges>
14
#include <span>
15
#ifdef __SSE2__
16
#include "emmintrin.h"
17
#endif
18
19
namespace openmsx {
20
21
using Pixel = uint32_t;
22
23
/**  Scale_XonY functions
24
 * Transforms an input line of pixel to an output line (possibly) with
25
 * a different width. X input pixels are mapped on Y output pixels.
26
 * @param in Input line
27
 * @param out Output line
28
 */
29
void scale_1on3(std::span<const Pixel> in, std::span<Pixel> out);
30
void scale_1on4(std::span<const Pixel> in, std::span<Pixel> out);
31
void scale_1on6(std::span<const Pixel> in, std::span<Pixel> out);
32
void scale_1on2(std::span<const Pixel> in, std::span<Pixel> out);
33
void scale_2on1(std::span<const Pixel> in, std::span<Pixel> out);
34
void scale_6on1(std::span<const Pixel> in, std::span<Pixel> out);
35
void scale_4on1(std::span<const Pixel> in, std::span<Pixel> out);
36
void scale_3on1(std::span<const Pixel> in, std::span<Pixel> out);
37
void scale_3on2(std::span<const Pixel> in, std::span<Pixel> out);
38
void scale_3on4(std::span<const Pixel> in, std::span<Pixel> out);
39
void scale_3on8(std::span<const Pixel> in, std::span<Pixel> out);
40
void scale_2on3(std::span<const Pixel> in, std::span<Pixel> out);
41
void scale_4on3(std::span<const Pixel> in, std::span<Pixel> out);
42
void scale_8on3(std::span<const Pixel> in, std::span<Pixel> out);
43
void scale_2on9(std::span<const Pixel> in, std::span<Pixel> out);
44
void scale_4on9(std::span<const Pixel> in, std::span<Pixel> out);
45
void scale_8on9(std::span<const Pixel> in, std::span<Pixel> out);
46
47
/**  BlendLines functor
48
 * Generate an output line that is an interpolation of two input lines.
49
 * @param in1 First input line
50
 * @param in2 Second input line
51
 * @param out Output line
52
 * @param width Width of the lines in pixels
53
 */
54
template<unsigned w1 = 1, unsigned w2 = 1>
55
void blendLines(std::span<const Pixel> in1, std::span<const Pixel> in2,
56
                std::span<Pixel> out);
57
58
/**  AlphaBlendLines functor
59
 * Generate an output line that is a per-pixel-alpha-blend of the two input
60
 * lines. The first input line contains the alpha-value per pixel.
61
 * @param in1 First input line
62
 * @param in2 Second input line
63
 * @param out Output line
64
 */
65
void alphaBlendLines(std::span<const Pixel> in1, std::span<const Pixel> in2,
66
                     std::span<Pixel> out);
67
void alphaBlendLines(Pixel in1, std::span<const Pixel> in2,
68
                     std::span<Pixel> out);
69
70
71
// implementation
72
73
template<unsigned N>
74
static inline void scale_1onN(
75
	std::span<const Pixel> in, std::span<Pixel> out)
76
{
77
	auto outWidth = out.size();
78
	assert(in.size() == (outWidth / N));
79
80
	size_t i = 0, j = 0;
81
	for (/* */; i < (outWidth - (N - 1)); i += N, j += 1) {
82
		Pixel pix = in[j];
83
		for (auto k : xrange(N)) {
84
			out[i + k] = pix;
85
		}
86
	}
87
	for (auto k : xrange(N - 1)) {
88
		if ((i + k) < outWidth) out[i + k] = 0;
89
	}
90
}
91
92
inline void scale_1on3(std::span<const Pixel> in, std::span<Pixel> out)
93
{
94
	scale_1onN<3>(in, out);
95
}
96
97
inline void scale_1on4(std::span<const Pixel> in, std::span<Pixel> out)
98
{
99
	scale_1onN<4>(in, out);
100
}
101
102
inline void scale_1on6(std::span<const Pixel> in, std::span<Pixel> out)
103
{
104
	scale_1onN<6>(in, out);
105
}
106
107
#ifdef __SSE2__
108
inline __m128i unpacklo(__m128i x, __m128i y)
109
{
110
	// 32bpp
111
	return _mm_unpacklo_epi32(x, y);
112
}
113
inline __m128i unpackhi(__m128i x, __m128i y)
114
{
115
	// 32bpp
116
	return _mm_unpackhi_epi32(x, y);
117
}
118
119
inline void scale_1on2_SSE(const Pixel* __restrict in_, Pixel* __restrict out_, size_t srcWidth)
120
{
121
	size_t bytes = srcWidth * sizeof(Pixel);
122
	assert((bytes % (4 * sizeof(__m128i))) == 0);
123
	assert(bytes != 0);
124
125
	const auto* in  = std::bit_cast<const char*>(in_)  +     bytes;
126
	      auto* out = std::bit_cast<      char*>(out_) + 2 * bytes;
127
128
	auto x = -ptrdiff_t(bytes);
129
	do {
130
		__m128i a0 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + x +  0));
131
		__m128i a1 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + x + 16));
132
		__m128i a2 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + x + 32));
133
		__m128i a3 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + x + 48));
134
		__m128i l0 = unpacklo(a0, a0);
135
		__m128i h0 = unpackhi(a0, a0);
136
		__m128i l1 = unpacklo(a1, a1);
137
		__m128i h1 = unpackhi(a1, a1);
138
		__m128i l2 = unpacklo(a2, a2);
139
		__m128i h2 = unpackhi(a2, a2);
140
		__m128i l3 = unpacklo(a3, a3);
141
		__m128i h3 = unpackhi(a3, a3);
142
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + 2*x +   0), l0);
143
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + 2*x +  16), h0);
144
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + 2*x +  32), l1);
145
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + 2*x +  48), h1);
146
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + 2*x +  64), l2);
147
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + 2*x +  80), h2);
148
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + 2*x +  96), l3);
149
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + 2*x + 112), h3);
150
		x += 4 * sizeof(__m128i);
151
	} while (x < 0);
152
}
153
#endif
154
155
inline void scale_1on2(std::span<const Pixel> in, std::span<Pixel> out)
156
{
157
	// This is a fairly simple algorithm (output each input pixel twice).
158
	// An ideal compiler should generate optimal (vector) code for it.
159
	// I checked the 2013-05-29 dev snapshots of gcc-4.9 and clang-3.4:
160
	// - Clang is not able to vectorize this loop. My best tuned C version
161
	//   of this routine is a little over 4x slower than the tuned
162
	//   SSE-intrinsics version.
163
	// - Gcc can auto-vectorize this routine. Though my best tuned version
164
	//   (I mean tuned to further improve the auto-vectorization, including
165
	//   using the new __builtin_assume_aligned() intrinsic) still runs
166
	//   approx 40% slower than the intrinsics version.
167
	// Hopefully in some years the compilers have improved further so that
168
	// the intrinsic version is no longer needed.
169
	auto srcWidth = in.size();
170
	assert((out.size() / 2) == srcWidth);
171
172
#ifdef __SSE2__
173
	size_t chunk = 4 * sizeof(__m128i) / sizeof(Pixel);
174
	size_t srcWidth2 = srcWidth & ~(chunk - 1);
175
	scale_1on2_SSE(in.data(), out.data(), srcWidth2);
176
	in  = in .subspan(    srcWidth2);
177
	out = out.subspan(2 * srcWidth2);
178
	srcWidth -= srcWidth2;
179
#endif
180
181
	// C++ version. Used both on non-x86 machines and (possibly) on x86 for
182
	// the last few pixels of the line.
183
	for (auto x : xrange(srcWidth)) {
184
		out[x * 2] = out[x * 2 + 1] = in[x];
185
	}
186
}
187
188
#ifdef __SSE2__
189
template<int IMM8> static inline __m128i shuffle(__m128i x, __m128i y)
190
{
191
	return _mm_castps_si128(_mm_shuffle_ps(
192
		_mm_castsi128_ps(x), _mm_castsi128_ps(y), IMM8));
193
}
194
195
inline __m128i blend(__m128i x, __m128i y)
196
{
197
	// 32bpp
198
	__m128i p = shuffle<0x88>(x, y);
199
	__m128i q = shuffle<0xDD>(x, y);
200
	return _mm_avg_epu8(p, q);
201
}
202
203
inline void scale_2on1_SSE(
204
	const Pixel* __restrict in_, Pixel* __restrict out_, size_t dstBytes)
205
{
206
	assert((dstBytes % (4 * sizeof(__m128i))) == 0);
207
	assert(dstBytes != 0);
208
209
	const auto* in  = std::bit_cast<const char*>(in_)  + 2 * dstBytes;
210
	      auto* out = std::bit_cast<      char*>(out_) +     dstBytes;
211
212
	auto x = -ptrdiff_t(dstBytes);
213
	do {
214
		__m128i a0 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + 2*x +   0));
215
		__m128i a1 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + 2*x +  16));
216
		__m128i a2 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + 2*x +  32));
217
		__m128i a3 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + 2*x +  48));
218
		__m128i a4 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + 2*x +  64));
219
		__m128i a5 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + 2*x +  80));
220
		__m128i a6 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + 2*x +  96));
221
		__m128i a7 = _mm_loadu_si128(std::bit_cast<const __m128i*>(in + 2*x + 112));
222
		__m128i b0 = blend(a0, a1);
223
		__m128i b1 = blend(a2, a3);
224
		__m128i b2 = blend(a4, a5);
225
		__m128i b3 = blend(a6, a7);
226
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + x +  0), b0);
227
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + x + 16), b1);
228
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + x + 32), b2);
229
		_mm_storeu_si128(std::bit_cast<__m128i*>(out + x + 48), b3);
230
		x += 4 * sizeof(__m128i);
231
	} while (x < 0);
232
}
233
#endif
234
235
inline void scale_2on1(std::span<const Pixel> in, std::span<Pixel> out)
236
{
237
	assert((in.size() / 2) == out.size());
238
	auto outWidth = out.size();
239
#ifdef __SSE2__
240
	auto n64 = (outWidth * sizeof(Pixel)) & ~63;
241
	scale_2on1_SSE(in.data(), out.data(), n64); // process 64 byte chunks
242
	outWidth &= ((64 / sizeof(Pixel)) - 1); // remaining pixels (if any)
243
	if (outWidth == 0) [[likely]] return;
244
	in  = in .subspan(2 * n64 / sizeof(Pixel));
245
	out = out.subspan(    n64 / sizeof(Pixel));
246
	// fallthrough to c++ version
247
#endif
248
	// pure C++ version
249
	PixelOperations pixelOps;
250
	for (auto i : xrange(outWidth)) {
251
		out[i] = pixelOps.template blend<1, 1>(
252
			in[2 * i + 0], in[2 * i + 1]);
253
	}
254
}
255
256
inline void scale_6on1(std::span<const Pixel> in, std::span<Pixel> out)
257
{
258
	assert((in.size() / 6) == out.size());
259
	PixelOperations pixelOps;
260
	for (auto i : xrange(out.size())) {
261
		out[i] = pixelOps.template blend<1, 1, 1, 1, 1, 1>(subspan<6>(in, 6 * i));
262
	}
263
}
264
265
inline void scale_4on1(std::span<const Pixel> in, std::span<Pixel> out)
266
{
267
	assert((in.size() / 4) == out.size());
268
	PixelOperations pixelOps;
269
	for (auto i : xrange(out.size())) {
270
		out[i] = pixelOps.template blend<1, 1, 1, 1>(subspan<4>(in, 4 * i));
271
	}
272
}
273
274
inline void scale_3on1(std::span<const Pixel> in, std::span<Pixel> out)
275
{
276
	assert((in.size() / 3) == out.size());
277
	PixelOperations pixelOps;
278
	for (auto i : xrange(out.size())) {
279
		out[i] = pixelOps.template blend<1, 1, 1>(subspan<3>(in, 3 * i));
280
	}
281
}
282
283
inline void scale_3on2(std::span<const Pixel> in, std::span<Pixel> out)
284
{
285
	assert((in.size() / 3) == (out.size() / 2));
286
	PixelOperations pixelOps;
287
	size_t n = out.size();
288
	size_t i = 0, j = 0;
289
	for (/* */; i < (n - 1); i += 2, j += 3) {
290
		out[i + 0] = pixelOps.template blend<2, 1>(subspan<2>(in, j + 0));
291
		out[i + 1] = pixelOps.template blend<1, 2>(subspan<2>(in, j + 1));
292
	}
293
	if (i < n) out[i] = 0;
294
}
295
296
inline void scale_3on4(std::span<const Pixel> in, std::span<Pixel> out)
297
{
298
	assert((in.size() / 3) == (out.size() / 4));
299
	PixelOperations pixelOps;
300
	size_t n = out.size();
301
	size_t i = 0, j = 0;
302
	for (/* */; i < (n - 3); i += 4, j += 3) {
303
		out[i + 0] = in[j + 0];
304
		out[i + 1] = pixelOps.template blend<1, 2>(subspan<2>(in, j + 0));
305
		out[i + 2] = pixelOps.template blend<2, 1>(subspan<2>(in, j + 1));
306
		out[i + 3] = in[j + 2];
307
	}
308
	for (auto k : xrange(4 - 1)) {
309
		if ((i + k) < n) out[i + k] = 0;
310
	}
311
}
312
313
inline void scale_3on8(std::span<const Pixel> in, std::span<Pixel> out)
314
{
315
	assert((in.size() / 3) == (out.size() / 8));
316
	PixelOperations pixelOps;
317
	size_t n = out.size();
318
	size_t i = 0, j = 0;
319
	for (/* */; i < (n - 7); i += 8, j += 3) {
320
		out[i + 0] = in[j + 0];
321
		out[i + 1] = in[j + 0];
322
		out[i + 2] = pixelOps.template blend<2, 1>(subspan<2>(in, j + 0));
323
		out[i + 3] = in[j + 1];
324
		out[i + 4] = in[j + 1];
325
		out[i + 5] = pixelOps.template blend<1, 2>(subspan<2>(in, j + 1));
326
		out[i + 6] = in[j + 2];
327
		out[i + 7] = in[j + 2];
328
	}
329
	for (auto k : xrange(8 - 1)) {
330
		if ((i + k) < n) out[i + k] = 0;
331
	}
332
}
333
334
inline void scale_2on3(std::span<const Pixel> in, std::span<Pixel> out)
335
{
336
	assert((in.size() / 2) == (out.size() / 3));
337
	PixelOperations pixelOps;
338
	size_t n = out.size();
339
	size_t i = 0, j = 0;
340
	for (/* */; i < (n - 2); i += 3, j += 2) {
341
		out[i + 0] = in[j + 0];
342
		out[i + 1] = pixelOps.template blend<1, 1>(subspan<2>(in, j));
343
		out[i + 2] = in[j + 1];
344
	}
345
	if ((i + 0) < n) out[i + 0] = 0;
346
	if ((i + 1) < n) out[i + 1] = 0;
347
}
348
349
inline void scale_4on3(std::span<const Pixel> in, std::span<Pixel> out)
350
{
351
	assert((in.size() / 4) == (out.size() / 3));
352
	PixelOperations pixelOps;
353
	size_t n = out.size();
354
	size_t i = 0, j = 0;
355
	for (/* */; i < (n - 2); i += 3, j += 4) {
356
		out[i + 0] = pixelOps.template blend<3, 1>(subspan<2>(in, j + 0));
357
		out[i + 1] = pixelOps.template blend<1, 1>(subspan<2>(in, j + 1));
358
		out[i + 2] = pixelOps.template blend<1, 3>(subspan<2>(in, j + 2));
359
	}
360
	if ((i + 0) < n) out[i + 0] = 0;
361
	if ((i + 1) < n) out[i + 1] = 0;
362
}
363
364
inline void scale_8on3(std::span<const Pixel> in, std::span<Pixel> out)
365
{
366
	assert((in.size() / 8) == (out.size() / 3));
367
	PixelOperations pixelOps;
368
	size_t n = out.size();
369
	size_t i = 0, j = 0;
370
	for (/* */; i < (n - 2); i += 3, j += 8) {
371
		out[i + 0] = pixelOps.template blend<3, 3, 2>   (subspan<3>(in, j + 0));
372
		out[i + 1] = pixelOps.template blend<1, 3, 3, 1>(subspan<4>(in, j + 2));
373
		out[i + 2] = pixelOps.template blend<2, 3, 3>   (subspan<3>(in, j + 5));
374
	}
375
	if ((i + 0) < n) out[i + 0] = 0;
376
	if ((i + 1) < n) out[i + 1] = 0;
377
}
378
379
inline void scale_2on9(std::span<const Pixel> in, std::span<Pixel> out)
380
{
381
	assert((in.size() / 2) == (out.size() / 9));
382
	PixelOperations pixelOps;
383
	size_t n = out.size();
384
	size_t i = 0, j = 0;
385
	for (/* */; i < (n - 8); i += 9, j += 2) {
386
		out[i + 0] = in[j + 0];
387
		out[i + 1] = in[j + 0];
388
		out[i + 2] = in[j + 0];
389
		out[i + 3] = in[j + 0];
390
		out[i + 4] = pixelOps.template blend<1, 1>(subspan<2>(in, j));
391
		out[i + 5] = in[j + 1];
392
		out[i + 6] = in[j + 1];
393
		out[i + 7] = in[j + 1];
394
		out[i + 8] = in[j + 1];
395
	}
396
	if ((i + 0) < n) out[i + 0] = 0;
397
	if ((i + 1) < n) out[i + 1] = 0;
398
	if ((i + 2) < n) out[i + 2] = 0;
399
	if ((i + 3) < n) out[i + 3] = 0;
400
	if ((i + 4) < n) out[i + 4] = 0;
401
	if ((i + 5) < n) out[i + 5] = 0;
402
	if ((i + 6) < n) out[i + 6] = 0;
403
	if ((i + 7) < n) out[i + 7] = 0;
404
}
405
406
inline void scale_4on9(std::span<const Pixel> in, std::span<Pixel> out)
407
{
408
	assert((in.size() / 4) == (out.size() / 9));
409
	PixelOperations pixelOps;
410
	size_t n = out.size();
411
	size_t i = 0, j = 0;
412
	for (/* */; i < (n - 8); i += 9, j += 4) {
413
		out[i + 0] = in[j + 0];
414
		out[i + 1] = in[j + 0];
415
		out[i + 2] = pixelOps.template blend<1, 3>(subspan<2>(in, j + 0));
416
		out[i + 3] = in[j + 1];
417
		out[i + 4] = pixelOps.template blend<1, 1>(subspan<2>(in, j + 1));
418
		out[i + 5] = in[j + 2];
419
		out[i + 6] = pixelOps.template blend<3, 1>(subspan<2>(in, j + 2));
420
		out[i + 7] = in[j + 3];
421
		out[i + 8] = in[j + 3];
422
	}
423
	if ((i + 0) < n) out[i + 0] = 0;
424
	if ((i + 1) < n) out[i + 1] = 0;
425
	if ((i + 2) < n) out[i + 2] = 0;
426
	if ((i + 3) < n) out[i + 3] = 0;
427
	if ((i + 4) < n) out[i + 4] = 0;
428
	if ((i + 5) < n) out[i + 5] = 0;
429
	if ((i + 6) < n) out[i + 6] = 0;
430
	if ((i + 7) < n) out[i + 7] = 0;
431
}
432
433
inline void scale_8on9(std::span<const Pixel> in, std::span<Pixel> out)
434
{
435
	assert((in.size() / 8) == (out.size() / 9));
436
	PixelOperations pixelOps;
437
	size_t n = out.size();
438
	size_t i = 0, j = 0;
439
	for (/* */; i < (n - 8); i += 9, j += 8) {
440
		out[i + 0] = in[j + 0];
441
		out[i + 1] = pixelOps.template blend<1, 7>(subspan<2>(in, j + 0));
442
		out[i + 2] = pixelOps.template blend<1, 3>(subspan<2>(in, j + 1));
443
		out[i + 3] = pixelOps.template blend<3, 5>(subspan<2>(in, j + 2));
444
		out[i + 4] = pixelOps.template blend<1, 1>(subspan<2>(in, j + 3));
445
		out[i + 5] = pixelOps.template blend<5, 3>(subspan<2>(in, j + 4));
446
		out[i + 6] = pixelOps.template blend<3, 1>(subspan<2>(in, j + 5));
447
		out[i + 7] = pixelOps.template blend<7, 1>(subspan<2>(in, j + 6));
448
		out[i + 8] = in[j + 7];
449
	}
450
	if ((i + 0) < n) out[i + 0] = 0;
451
	if ((i + 1) < n) out[i + 1] = 0;
452
	if ((i + 2) < n) out[i + 2] = 0;
453
	if ((i + 3) < n) out[i + 3] = 0;
454
	if ((i + 4) < n) out[i + 4] = 0;
455
	if ((i + 5) < n) out[i + 5] = 0;
456
	if ((i + 6) < n) out[i + 6] = 0;
457
	if ((i + 7) < n) out[i + 7] = 0;
458
}
459
460
template<unsigned w1, unsigned w2>
461
void blendLines(std::span<const Pixel> in1, std::span<const Pixel> in2, std::span<Pixel> out)
462
{
463
	// It _IS_ allowed that the output is the same as one of the inputs.
464
	// TODO SSE optimizations
465
	// pure C++ version
466
	assert(in1.size() == in2.size());
467
	assert(in1.size() == out.size());
468
	PixelOperations pixelOps;
469
	for (auto [i1, i2, o] : std::views::zip(in1, in2, out)) {
470
		o = pixelOps.template blend<w1, w2>(i1, i2);
471
	}
472
}
473
474
inline void alphaBlendLines(
475
	std::span<const Pixel> in1, std::span<const Pixel> in2, std::span<Pixel> out)
476
{
477
	// It _IS_ allowed that the output is the same as one of the inputs.
478
	assert(in1.size() == in2.size());
479
	assert(in1.size() == out.size());
480
	PixelOperations pixelOps;
481
	for (auto [i1, i2, o] : std::views::zip(in1, in2, out)) {
482
		o = pixelOps.alphaBlend(i1, i2);
483
	}
484
}
485
486
inline void alphaBlendLines(
487
	Pixel in1, std::span<const Pixel> in2, std::span<Pixel> out)
488
{
489
	// It _IS_ allowed that the output is the same as the input.
490
491
	// ATM this routine is only called when 'in1' is not fully opaque nor
492
	// fully transparent.
493
	assert(in2.size() == out.size());
494
495
	PixelOperations pixelOps;
496
	unsigned alpha = pixelOps.alpha(in1);
497
498
	// When one of the two colors is loop-invariant, using the
499
	// pre-multiplied-alpha-blending equation is a tiny bit more efficient
500
	// than using alphaBlend() or even lerp().
501
	//    for (auto i : xrange(width)) {
502
	//        out[i] = pixelOps.lerp(in1, in2[i], alpha);
503
	//    }
504
	Pixel in1M = pixelOps.multiply(in1, alpha);
505
	unsigned alpha2 = 256 - alpha;
506
	for (auto [i2, o] : std::views::zip(in2, out)) {
507
		o = in1M + pixelOps.multiply(i2, alpha2);
508
	}
509
}
510
511
} // namespace openmsx
512
513
#endif