Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/util-tests/image_tests.cpp
4802 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "util/image.h"
5
6
#include "common/error.h"
7
8
#include "gtest/gtest.h"
9
10
#include <type_traits>
11
12
namespace {
13
14
class ImageTest : public ::testing::Test
15
{
16
protected:
17
void SetUp() override
18
{
19
// Default test image is a 4x4 RGBA8 image
20
m_test_image = Image(4, 4, ImageFormat::RGBA8);
21
// Fill with a simple pattern (red gradient)
22
for (u32 y = 0; y < m_test_image.GetHeight(); y++)
23
{
24
for (u32 x = 0; x < m_test_image.GetWidth(); x++)
25
{
26
u32* pixel = reinterpret_cast<u32*>(m_test_image.GetRowPixels(y) + x * sizeof(u32));
27
// Red gradient, full alpha
28
*pixel = (x * 64) | (0u << 8) | (0u << 16) | (0xFFu << 24);
29
}
30
}
31
}
32
33
Image m_test_image;
34
};
35
36
} // namespace
37
38
// Basic constructor tests
39
TEST_F(ImageTest, DefaultConstructor)
40
{
41
Image img;
42
EXPECT_FALSE(img.IsValid());
43
EXPECT_EQ(img.GetWidth(), 0u);
44
EXPECT_EQ(img.GetHeight(), 0u);
45
EXPECT_EQ(img.GetFormat(), ImageFormat::None);
46
}
47
48
TEST_F(ImageTest, SizeFormatConstructor)
49
{
50
const u32 width = 16;
51
const u32 height = 8;
52
Image img(width, height, ImageFormat::RGBA8);
53
54
EXPECT_TRUE(img.IsValid());
55
EXPECT_EQ(img.GetWidth(), width);
56
EXPECT_EQ(img.GetHeight(), height);
57
EXPECT_EQ(img.GetFormat(), ImageFormat::RGBA8);
58
EXPECT_NE(img.GetPixels(), nullptr);
59
}
60
61
TEST_F(ImageTest, CopyConstructor)
62
{
63
const u32 width = 16;
64
const u32 height = 8;
65
Image src(width, height, ImageFormat::RGBA8);
66
67
// Set a test pattern
68
std::memset(src.GetPixels(), 0xAA, src.GetStorageSize());
69
70
// Copy construct
71
Image copy(src);
72
73
EXPECT_TRUE(copy.IsValid());
74
EXPECT_EQ(copy.GetWidth(), width);
75
EXPECT_EQ(copy.GetHeight(), height);
76
EXPECT_EQ(copy.GetFormat(), ImageFormat::RGBA8);
77
78
// Contents should be the same
79
EXPECT_EQ(std::memcmp(copy.GetPixels(), src.GetPixels(), src.GetStorageSize()), 0);
80
// But the memory should be different
81
EXPECT_NE(copy.GetPixels(), src.GetPixels());
82
}
83
84
// Assignment operator tests
85
TEST_F(ImageTest, CopyAssignmentOperator)
86
{
87
const u32 width = 8;
88
const u32 height = 6;
89
Image src(width, height, ImageFormat::RGBA8);
90
91
// Set a test pattern
92
std::memset(src.GetPixels(), 0xCCu, src.GetStorageSize());
93
94
// Create a different image first
95
Image dest(4, 4, ImageFormat::BGRA8);
96
std::memset(dest.GetPixels(), 0x55u, dest.GetStorageSize());
97
98
// Assign using copy assignment
99
dest = src;
100
101
// Verify properties were copied
102
EXPECT_EQ(dest.GetWidth(), width);
103
EXPECT_EQ(dest.GetHeight(), height);
104
EXPECT_EQ(dest.GetFormat(), ImageFormat::RGBA8);
105
106
// Contents should be the same
107
EXPECT_EQ(std::memcmp(dest.GetPixels(), src.GetPixels(), src.GetStorageSize()), 0);
108
// But the memory should be different
109
EXPECT_NE(dest.GetPixels(), src.GetPixels());
110
}
111
112
TEST_F(ImageTest, MoveAssignmentOperator)
113
{
114
const u32 width = 8;
115
const u32 height = 6;
116
Image src(width, height, ImageFormat::RGBA8);
117
118
// Set a test pattern
119
std::memset(src.GetPixels(), 0xCC, src.GetStorageSize());
120
121
// Keep track of the original pointer
122
const u8* original_pixels = src.GetPixels();
123
124
// Create a different image first
125
Image dest(4, 4, ImageFormat::BGRA8);
126
127
// Assign using move assignment
128
dest = std::move(src);
129
130
// Verify properties were moved
131
EXPECT_EQ(dest.GetWidth(), width);
132
EXPECT_EQ(dest.GetHeight(), height);
133
EXPECT_EQ(dest.GetFormat(), ImageFormat::RGBA8);
134
EXPECT_EQ(dest.GetPixels(), original_pixels); // Should be the same pointer
135
136
// Source should be invalidated
137
EXPECT_FALSE(src.IsValid());
138
EXPECT_EQ(src.GetWidth(), 0u);
139
EXPECT_EQ(src.GetHeight(), 0u);
140
EXPECT_EQ(src.GetFormat(), ImageFormat::None);
141
EXPECT_EQ(src.GetPixels(), nullptr);
142
}
143
144
// Test format utility functions
145
TEST_F(ImageTest, FormatUtilities)
146
{
147
EXPECT_STREQ(Image::GetFormatName(ImageFormat::RGBA8), "RGBA8");
148
EXPECT_STREQ(Image::GetFormatName(ImageFormat::BC1), "BC1");
149
150
EXPECT_EQ(Image::GetPixelSize(ImageFormat::RGBA8), 4u);
151
EXPECT_EQ(Image::GetPixelSize(ImageFormat::RGB565), 2u);
152
153
EXPECT_FALSE(Image::IsCompressedFormat(ImageFormat::RGBA8));
154
EXPECT_TRUE(Image::IsCompressedFormat(ImageFormat::BC1));
155
}
156
157
// Test pixel manipulation
158
TEST_F(ImageTest, PixelManipulation)
159
{
160
// Check initial pattern
161
const u32* first_pixel = reinterpret_cast<const u32*>(m_test_image.GetPixels());
162
EXPECT_EQ(*first_pixel, 0xFF000000u); // First pixel should be black with full alpha
163
164
// Test Clear()
165
m_test_image.Clear();
166
EXPECT_EQ(*first_pixel, 0u);
167
168
// Test SetAllPixelsOpaque()
169
std::memset(m_test_image.GetPixels(), 0x0u, m_test_image.GetStorageSize()); // Clear alpha
170
m_test_image.SetAllPixelsOpaque();
171
172
// Check all pixels now have alpha set to 0xFF
173
for (u32 y = 0; y < m_test_image.GetHeight(); y++)
174
{
175
for (u32 x = 0; x < m_test_image.GetWidth(); x++)
176
{
177
const u32* pixel = reinterpret_cast<const u32*>(m_test_image.GetRowPixels(y) + x * sizeof(u32));
178
EXPECT_EQ((*pixel & 0xFF000000), 0xFF000000u);
179
}
180
}
181
}
182
183
// Test resize functionality
184
TEST_F(ImageTest, Resize)
185
{
186
const u32 new_width = 8;
187
const u32 new_height = 10;
188
189
// Test resize without preservation
190
m_test_image.Resize(new_width, new_height, false);
191
EXPECT_EQ(m_test_image.GetWidth(), new_width);
192
EXPECT_EQ(m_test_image.GetHeight(), new_height);
193
194
// Test resize with format change
195
m_test_image.Resize(new_width, new_height, ImageFormat::BGRA8, false);
196
EXPECT_EQ(m_test_image.GetFormat(), ImageFormat::BGRA8);
197
198
// Fill with a known pattern
199
std::memset(m_test_image.GetPixels(), 0xBBu, m_test_image.GetStorageSize());
200
201
// Test resize with preservation
202
const u32 final_width = 6;
203
const u32 final_height = 7;
204
m_test_image.Resize(final_width, final_height, true);
205
206
// First bytes should still be 0xBB
207
EXPECT_EQ(m_test_image.GetPixels()[0], 0xBBu);
208
EXPECT_EQ(m_test_image.GetWidth(), final_width);
209
EXPECT_EQ(m_test_image.GetHeight(), final_height);
210
}
211
212
// Test format conversion - additional formats
213
TEST_F(ImageTest, RGB565ToRGBA8)
214
{
215
constexpr u32 width = 4;
216
constexpr u32 height = 4;
217
Image rgb565_image(width, height, ImageFormat::RGB565);
218
219
// Fill with a test pattern - pure red in RGB565 format (0xF800)
220
for (u32 y = 0; y < height; y++)
221
{
222
u16* row = reinterpret_cast<u16*>(rgb565_image.GetRowPixels(y));
223
for (u32 x = 0; x < width; x++)
224
{
225
row[x] = 0xF800; // Red in RGB565
226
}
227
}
228
229
// Convert to RGBA8
230
Error err;
231
std::optional<Image> rgba8_image = rgb565_image.ConvertToRGBA8(&err);
232
ASSERT_TRUE(rgba8_image.has_value());
233
EXPECT_EQ(rgba8_image->GetFormat(), ImageFormat::RGBA8);
234
235
// Check the first pixel - should be red with full alpha
236
const u32* first_pixel = reinterpret_cast<const u32*>(rgba8_image->GetPixels());
237
// Red component should be close to 0xFF (might be 0xF8 due to precision)
238
EXPECT_GE((*first_pixel & 0xFFu), 0xF8u);
239
// Green and blue should be 0
240
EXPECT_EQ((*first_pixel & 0xFF00u), 0u);
241
EXPECT_EQ((*first_pixel & 0xFF0000u), 0u);
242
// Alpha should be 0xFF
243
EXPECT_EQ((*first_pixel & 0xFF000000u), 0xFF000000u);
244
}
245
246
TEST_F(ImageTest, RGB5A1ToRGBA8)
247
{
248
constexpr u32 width = 4;
249
constexpr u32 height = 4;
250
Image rgb5a1_image(width, height, ImageFormat::RGB5A1);
251
252
// Fill with a test pattern - green with alpha in RGB5A1 format (0x07C0)
253
for (u32 y = 0; y < height; y++)
254
{
255
u16* row = reinterpret_cast<u16*>(rgb5a1_image.GetRowPixels(y));
256
for (u32 x = 0; x < width; x++)
257
{
258
row[x] = 0x87C0; // Green with alpha bit set
259
}
260
}
261
262
// Convert to RGBA8
263
Error err;
264
std::optional<Image> rgba8_image = rgb5a1_image.ConvertToRGBA8(&err);
265
ASSERT_TRUE(rgba8_image.has_value());
266
EXPECT_EQ(rgba8_image->GetFormat(), ImageFormat::RGBA8);
267
268
// Check the first pixel - should be green with full alpha
269
const u32* first_pixel = reinterpret_cast<const u32*>(rgba8_image->GetPixels());
270
// Green component should be set, red and blue should be 0
271
EXPECT_GE((*first_pixel & 0xFFu), 8u);
272
EXPECT_LT((*first_pixel & 0xFFu), 16u);
273
EXPECT_GE(((*first_pixel >> 8) & 0xFFu), 0xF0u);
274
EXPECT_LT(((*first_pixel >> 8) & 0xFFu), 0xF8u);
275
EXPECT_EQ((*first_pixel & 0xFF0000u), 0u);
276
// Alpha should be 0xFF since the alpha bit was set
277
EXPECT_EQ((*first_pixel & 0xFF000000u), 0xFF000000u);
278
}
279
280
// Test block sizes for compressed formats
281
TEST_F(ImageTest, BlockSizes)
282
{
283
// Test with 16x16 image - evenly divisible by block size (4)
284
const u32 width = 16;
285
const u32 height = 16;
286
287
// BC1 format (4x4 blocks, 8 bytes per block)
288
Image bc1_image(width, height, ImageFormat::BC1);
289
EXPECT_EQ(bc1_image.GetBlocksWide(), width / 4);
290
EXPECT_EQ(bc1_image.GetBlocksHigh(), height / 4);
291
EXPECT_EQ(bc1_image.GetPitch(), (width / 4) * 8);
292
293
// Test with non-multiple dimensions
294
const u32 odd_width = 10;
295
const u32 odd_height = 6;
296
297
// BC1 format with non-multiple dimensions
298
Image bc1_odd_image(odd_width, odd_height, ImageFormat::BC1);
299
// Should round up to multiple of 4
300
EXPECT_EQ(bc1_odd_image.GetBlocksWide(), (odd_width + 3) / 4);
301
EXPECT_EQ(bc1_odd_image.GetBlocksHigh(), (odd_height + 3) / 4);
302
303
// BC3 format (4x4 blocks, 16 bytes per block)
304
Image bc3_image(width, height, ImageFormat::BC3);
305
EXPECT_EQ(bc3_image.GetBlocksWide(), width / 4);
306
EXPECT_EQ(bc3_image.GetBlocksHigh(), height / 4);
307
EXPECT_EQ(bc3_image.GetPitch(), (width / 4) * 16);
308
309
// Storage size test for BC1
310
const u32 bc1_storage = bc1_image.GetStorageSize();
311
EXPECT_EQ(bc1_storage, (width / 4) * (height / 4) * 8);
312
313
// Storage size test for BC3
314
const u32 bc3_storage = bc3_image.GetStorageSize();
315
EXPECT_EQ(bc3_storage, (width / 4) * (height / 4) * 16);
316
}
317
318
// Test GetPixelsSpan
319
TEST_F(ImageTest, PixelSpans)
320
{
321
// Test const span
322
std::span<const u8> const_span = m_test_image.GetPixelsSpan();
323
EXPECT_EQ(const_span.data(), m_test_image.GetPixels());
324
EXPECT_EQ(const_span.size(), m_test_image.GetStorageSize());
325
326
// Test non-const span
327
std::span<u8> mutable_span = m_test_image.GetPixelsSpan();
328
EXPECT_EQ(mutable_span.data(), m_test_image.GetPixels());
329
EXPECT_EQ(mutable_span.size(), m_test_image.GetStorageSize());
330
331
// Modify through the span and verify
332
if (!mutable_span.empty())
333
{
334
mutable_span[0] = 0xAA;
335
EXPECT_EQ(m_test_image.GetPixels()[0], 0xAAu);
336
}
337
}
338
339
// Test TakePixels
340
TEST_F(ImageTest, TakePixels)
341
{
342
const u32 width = 8;
343
const u32 height = 6;
344
Image src(width, height, ImageFormat::RGBA8);
345
346
// Set a test pattern
347
std::memset(src.GetPixels(), 0xCC, src.GetStorageSize());
348
349
// Keep track of the original pointer
350
const u8* original_pixels = src.GetPixels();
351
352
// Take pixels
353
Image::PixelStorage pixels = src.TakePixels();
354
355
// Original image should now be invalid
356
EXPECT_FALSE(src.IsValid());
357
EXPECT_EQ(src.GetWidth(), 0u);
358
EXPECT_EQ(src.GetHeight(), 0u);
359
EXPECT_EQ(src.GetFormat(), ImageFormat::None);
360
EXPECT_EQ(src.GetPixels(), nullptr);
361
362
// Pixels pointer should be the original pointer
363
EXPECT_EQ(pixels.get(), original_pixels);
364
}
365
366
// Test invalid operations
367
TEST_F(ImageTest, OperationsOnInvalidImage)
368
{
369
Image invalid_image;
370
371
// These operations should safely handle invalid images
372
invalid_image.Clear(); // No-op for invalid images
373
invalid_image.FlipY(); // No-op for invalid images
374
375
// GetStorageSize should return 0 for invalid image
376
EXPECT_EQ(invalid_image.GetStorageSize(), 0u);
377
378
// GetPixels should return nullptr for invalid image
379
EXPECT_EQ(invalid_image.GetPixels(), nullptr);
380
381
// Spans should be empty for invalid image
382
EXPECT_TRUE(invalid_image.GetPixelsSpan().empty());
383
}
384
385
// Test conversion of different formats to RGBA8
386
TEST_F(ImageTest, ConvertMultipleFormatsToRGBA8)
387
{
388
const u32 width = 4;
389
const u32 height = 4;
390
Error err;
391
392
// Test RGBA8 to RGBA8 (should be essentially a copy)
393
{
394
Image rgba8_image(width, height, ImageFormat::RGBA8);
395
std::memset(rgba8_image.GetPixels(), 0xAA, rgba8_image.GetStorageSize());
396
397
std::optional<Image> converted = rgba8_image.ConvertToRGBA8(&err);
398
ASSERT_TRUE(converted.has_value());
399
EXPECT_EQ(converted->GetFormat(), ImageFormat::RGBA8);
400
EXPECT_EQ(std::memcmp(converted->GetPixels(), rgba8_image.GetPixels(), rgba8_image.GetStorageSize()), 0);
401
}
402
403
// Test BGRA8 to RGBA8 (color channels should be swapped)
404
{
405
Image bgra8_image(width, height, ImageFormat::BGRA8);
406
// Set to blue in BGRA8 (0xFFRRGGBB = 0xFF0000FF)
407
std::fill_n(reinterpret_cast<u32*>(bgra8_image.GetPixels()), width * height, 0xFF0000FF);
408
409
std::optional<Image> converted = bgra8_image.ConvertToRGBA8(&err);
410
ASSERT_TRUE(converted.has_value());
411
412
// First pixel should now be red in RGBA8 (0xFFBBGGRR = 0xFFFF0000)
413
const u32* first_pixel = reinterpret_cast<const u32*>(converted->GetPixels());
414
EXPECT_EQ(*first_pixel, 0xFFFF0000u);
415
}
416
417
// Test A1BGR5 to RGBA8
418
{
419
Image a1bgr5_image(width, height, ImageFormat::A1BGR5);
420
// Set to blue with alpha in A1BGR5
421
std::fill_n(reinterpret_cast<u16*>(a1bgr5_image.GetPixels()), width * height, static_cast<u16>(0x837b));
422
423
std::optional<Image> converted = a1bgr5_image.ConvertToRGBA8(&err);
424
ASSERT_TRUE(converted.has_value());
425
426
// First pixel should now be blue with alpha
427
const u32* first_pixel = reinterpret_cast<const u32*>(converted->GetPixels());
428
// Red should be high, green/blue should be low
429
EXPECT_GE((*first_pixel & 0xFFu), 0x80u);
430
EXPECT_GE(((*first_pixel >> 8) & 0xFFu), 0x34u);
431
EXPECT_GE(((*first_pixel >> 16) & 0xFFu), 0xE8u);
432
// Alpha should be 0xFF
433
EXPECT_EQ((*first_pixel & 0xFF000000u), 0xFF000000u);
434
}
435
}
436
437
// Test calculation functions
438
TEST_F(ImageTest, PitchAndStorage)
439
{
440
const u32 width = 16;
441
const u32 height = 8;
442
443
// Test uncompressed format
444
const u32 rgba_pitch = Image::CalculatePitch(width, height, ImageFormat::RGBA8);
445
EXPECT_EQ(rgba_pitch, width * 4); // 4 bytes per pixel
446
447
const u32 rgba_storage = Image::CalculateStorageSize(width, height, ImageFormat::RGBA8);
448
EXPECT_EQ(rgba_storage, rgba_pitch * height);
449
450
// Test compressed format (BC1)
451
const u32 bc1_pitch = Image::CalculatePitch(width, height, ImageFormat::BC1);
452
// BC1 uses 8 bytes per 4x4 block
453
EXPECT_EQ(bc1_pitch, (width / 4) * 8);
454
455
const u32 bc1_storage = Image::CalculateStorageSize(width, height, ImageFormat::BC1);
456
// Storage should be pitch * number of blocks high
457
EXPECT_EQ(bc1_storage, bc1_pitch * (height / 4));
458
}
459
460
// Test flip Y operation
461
TEST_F(ImageTest, FlipY)
462
{
463
// Create a test image with different colors on top and bottom
464
Image test_image(2, 2, ImageFormat::RGBA8);
465
466
// Top row: Red
467
u32* top_left = reinterpret_cast<u32*>(test_image.GetRowPixels(0));
468
u32* top_right = top_left + 1;
469
*top_left = *top_right = 0xFF0000FFu; // Red in RGBA
470
471
// Bottom row: Blue
472
u32* bottom_left = reinterpret_cast<u32*>(test_image.GetRowPixels(1));
473
u32* bottom_right = bottom_left + 1;
474
*bottom_left = *bottom_right = 0xFFFF0000u; // Blue in RGBA
475
476
// Flip the image
477
test_image.FlipY();
478
479
// Now the top row should be blue and the bottom row should be red
480
top_left = reinterpret_cast<u32*>(test_image.GetRowPixels(0));
481
top_right = top_left + 1;
482
bottom_left = reinterpret_cast<u32*>(test_image.GetRowPixels(1));
483
bottom_right = bottom_left + 1;
484
485
EXPECT_EQ(*top_left, 0xFFFF0000u); // Blue
486
EXPECT_EQ(*top_right, 0xFFFF0000u); // Blue
487
EXPECT_EQ(*bottom_left, 0xFF0000FFu); // Red
488
EXPECT_EQ(*bottom_right, 0xFF0000FFu); // Red
489
}
490
491
// Test edge cases
492
TEST_F(ImageTest, ZeroDimensions)
493
{
494
// Create image with zero width/height
495
Image zero_width(0, 10, ImageFormat::RGBA8);
496
EXPECT_FALSE(zero_width.IsValid());
497
498
Image zero_height(10, 0, ImageFormat::RGBA8);
499
EXPECT_FALSE(zero_height.IsValid());
500
501
// Resize to zero dimensions
502
Image normal(8, 8, ImageFormat::RGBA8);
503
normal.Resize(0, 8, false);
504
EXPECT_FALSE(normal.IsValid());
505
}
506
507
// Test that Invalidate properly resets all properties
508
TEST_F(ImageTest, Invalidate)
509
{
510
Image img(16, 16, ImageFormat::RGBA8);
511
EXPECT_TRUE(img.IsValid());
512
EXPECT_NE(img.GetPixels(), nullptr);
513
514
img.Invalidate();
515
EXPECT_FALSE(img.IsValid());
516
EXPECT_EQ(img.GetWidth(), 0u);
517
EXPECT_EQ(img.GetHeight(), 0u);
518
EXPECT_EQ(img.GetFormat(), ImageFormat::None);
519
EXPECT_EQ(img.GetPixels(), nullptr);
520
}
521
522