From 9d14a4a57f21385c2634b31d40718333317918fd Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 24 Sep 2025 22:25:14 +1000 Subject: [PATCH] Tests: Add tests for Image class --- duckstation.sln | 26 ++ src/CMakeLists.txt | 1 + src/util-tests/CMakeLists.txt | 5 + src/util-tests/image_tests.cpp | 520 ++++++++++++++++++++++ src/util-tests/util-tests.vcxproj | 30 ++ src/util-tests/util-tests.vcxproj.filters | 7 + 6 files changed, 589 insertions(+) create mode 100644 src/util-tests/CMakeLists.txt create mode 100644 src/util-tests/image_tests.cpp create mode 100644 src/util-tests/util-tests.vcxproj create mode 100644 src/util-tests/util-tests.vcxproj.filters diff --git a/duckstation.sln b/duckstation.sln index 813399d7f..828d53754 100644 --- a/duckstation.sln +++ b/duckstation.sln @@ -55,6 +55,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rapidyaml", "dep\rapidyaml\ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "duckstation-mini", "src\duckstation-mini\duckstation-mini.vcxproj", "{FA259BC0-1007-4FD9-8A47-87CC0ECB8445}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "util-tests", "src\util-tests\util-tests.vcxproj", "{15538AD7-2201-45C2-B088-BBB7F37BD7F5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -951,6 +953,30 @@ Global {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG-Clang|x64.ActiveCfg = ReleaseLTCG-Clang|x64 {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG-Clang-SSE2|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 {FA259BC0-1007-4FD9-8A47-87CC0ECB8445}.ReleaseLTCG-Clang-SSE2|x64.ActiveCfg = ReleaseLTCG-Clang-SSE2|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Debug|ARM64.ActiveCfg = Debug-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Debug|x64.ActiveCfg = Debug|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Debug-Clang|ARM64.ActiveCfg = Debug-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Debug-Clang|ARM64.Build.0 = Debug-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Debug-Clang|x64.ActiveCfg = Debug-Clang|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Debug-Clang-SSE2|ARM64.ActiveCfg = Debug-Clang|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Debug-Clang-SSE2|x64.ActiveCfg = Debug-Clang-SSE2|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.DebugFast|ARM64.ActiveCfg = DebugFast-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.DebugFast|x64.ActiveCfg = DebugFast|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.DebugFast-Clang|ARM64.ActiveCfg = DebugFast-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.DebugFast-Clang|x64.ActiveCfg = DebugFast-Clang|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Devel-Clang|ARM64.ActiveCfg = Devel-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Devel-Clang|x64.ActiveCfg = Devel-Clang|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Release|ARM64.ActiveCfg = Release-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Release|ARM64.Build.0 = Release-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Release|x64.ActiveCfg = Release|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Release-Clang|ARM64.ActiveCfg = Release-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.Release-Clang|x64.ActiveCfg = Release-Clang|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.ReleaseLTCG|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.ReleaseLTCG-Clang|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.ReleaseLTCG-Clang|x64.ActiveCfg = ReleaseLTCG-Clang|x64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.ReleaseLTCG-Clang-SSE2|ARM64.ActiveCfg = ReleaseLTCG-Clang|ARM64 + {15538AD7-2201-45C2-B088-BBB7F37BD7F5}.ReleaseLTCG-Clang-SSE2|x64.ActiveCfg = ReleaseLTCG-Clang-SSE2|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 61d4d7e97..3a38bc501 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,4 +21,5 @@ endif() if(BUILD_TESTS) add_subdirectory(common-tests EXCLUDE_FROM_ALL) + add_subdirectory(util-tests EXCLUDE_FROM_ALL) endif() diff --git a/src/util-tests/CMakeLists.txt b/src/util-tests/CMakeLists.txt new file mode 100644 index 000000000..5c81983de --- /dev/null +++ b/src/util-tests/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(util-tests + image_tests.cpp +) + +target_link_libraries(util-tests PRIVATE util gtest gtest_main) diff --git a/src/util-tests/image_tests.cpp b/src/util-tests/image_tests.cpp new file mode 100644 index 000000000..4849539e1 --- /dev/null +++ b/src/util-tests/image_tests.cpp @@ -0,0 +1,520 @@ +// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "util/image.h" + +#include "common/error.h" + +#include "gtest/gtest.h" + +#include + +namespace { + +class ImageTest : public ::testing::Test +{ +protected: + void SetUp() override + { + // Default test image is a 4x4 RGBA8 image + m_test_image = Image(4, 4, ImageFormat::RGBA8); + // Fill with a simple pattern (red gradient) + for (u32 y = 0; y < m_test_image.GetHeight(); y++) + { + for (u32 x = 0; x < m_test_image.GetWidth(); x++) + { + u32* pixel = reinterpret_cast(m_test_image.GetRowPixels(y) + x * sizeof(u32)); + // Red gradient, full alpha + *pixel = (x * 64) | (0u << 8) | (0u << 16) | (0xFFu << 24); + } + } + } + + Image m_test_image; +}; + +} // namespace + +// Basic constructor tests +TEST_F(ImageTest, DefaultConstructor) +{ + Image img; + EXPECT_FALSE(img.IsValid()); + EXPECT_EQ(img.GetWidth(), 0u); + EXPECT_EQ(img.GetHeight(), 0u); + EXPECT_EQ(img.GetFormat(), ImageFormat::None); +} + +TEST_F(ImageTest, SizeFormatConstructor) +{ + const u32 width = 16; + const u32 height = 8; + Image img(width, height, ImageFormat::RGBA8); + + EXPECT_TRUE(img.IsValid()); + EXPECT_EQ(img.GetWidth(), width); + EXPECT_EQ(img.GetHeight(), height); + EXPECT_EQ(img.GetFormat(), ImageFormat::RGBA8); + EXPECT_NE(img.GetPixels(), nullptr); +} + +TEST_F(ImageTest, CopyConstructor) +{ + const u32 width = 16; + const u32 height = 8; + Image src(width, height, ImageFormat::RGBA8); + + // Set a test pattern + std::memset(src.GetPixels(), 0xAA, src.GetStorageSize()); + + // Copy construct + Image copy(src); + + EXPECT_TRUE(copy.IsValid()); + EXPECT_EQ(copy.GetWidth(), width); + EXPECT_EQ(copy.GetHeight(), height); + EXPECT_EQ(copy.GetFormat(), ImageFormat::RGBA8); + + // Contents should be the same + EXPECT_EQ(std::memcmp(copy.GetPixels(), src.GetPixels(), src.GetStorageSize()), 0); + // But the memory should be different + EXPECT_NE(copy.GetPixels(), src.GetPixels()); +} + +// Assignment operator tests +TEST_F(ImageTest, CopyAssignmentOperator) +{ + const u32 width = 8; + const u32 height = 6; + Image src(width, height, ImageFormat::RGBA8); + + // Set a test pattern + std::memset(src.GetPixels(), 0xCCu, src.GetStorageSize()); + + // Create a different image first + Image dest(4, 4, ImageFormat::BGRA8); + std::memset(dest.GetPixels(), 0x55u, dest.GetStorageSize()); + + // Assign using copy assignment + dest = src; + + // Verify properties were copied + EXPECT_EQ(dest.GetWidth(), width); + EXPECT_EQ(dest.GetHeight(), height); + EXPECT_EQ(dest.GetFormat(), ImageFormat::RGBA8); + + // Contents should be the same + EXPECT_EQ(std::memcmp(dest.GetPixels(), src.GetPixels(), src.GetStorageSize()), 0); + // But the memory should be different + EXPECT_NE(dest.GetPixels(), src.GetPixels()); +} + +TEST_F(ImageTest, MoveAssignmentOperator) +{ + const u32 width = 8; + const u32 height = 6; + Image src(width, height, ImageFormat::RGBA8); + + // Set a test pattern + std::memset(src.GetPixels(), 0xCC, src.GetStorageSize()); + + // Keep track of the original pointer + const u8* original_pixels = src.GetPixels(); + + // Create a different image first + Image dest(4, 4, ImageFormat::BGRA8); + + // Assign using move assignment + dest = std::move(src); + + // Verify properties were moved + EXPECT_EQ(dest.GetWidth(), width); + EXPECT_EQ(dest.GetHeight(), height); + EXPECT_EQ(dest.GetFormat(), ImageFormat::RGBA8); + EXPECT_EQ(dest.GetPixels(), original_pixels); // Should be the same pointer + + // Source should be invalidated + EXPECT_FALSE(src.IsValid()); + EXPECT_EQ(src.GetWidth(), 0u); + EXPECT_EQ(src.GetHeight(), 0u); + EXPECT_EQ(src.GetFormat(), ImageFormat::None); + EXPECT_EQ(src.GetPixels(), nullptr); +} + +// Test format utility functions +TEST_F(ImageTest, FormatUtilities) +{ + EXPECT_STREQ(Image::GetFormatName(ImageFormat::RGBA8), "RGBA8"); + EXPECT_STREQ(Image::GetFormatName(ImageFormat::BC1), "BC1"); + + EXPECT_EQ(Image::GetPixelSize(ImageFormat::RGBA8), 4u); + EXPECT_EQ(Image::GetPixelSize(ImageFormat::RGB565), 2u); + + EXPECT_FALSE(Image::IsCompressedFormat(ImageFormat::RGBA8)); + EXPECT_TRUE(Image::IsCompressedFormat(ImageFormat::BC1)); +} + +// Test pixel manipulation +TEST_F(ImageTest, PixelManipulation) +{ + // Check initial pattern + const u32* first_pixel = reinterpret_cast(m_test_image.GetPixels()); + EXPECT_EQ(*first_pixel, 0xFF000000u); // First pixel should be black with full alpha + + // Test Clear() + m_test_image.Clear(); + EXPECT_EQ(*first_pixel, 0u); + + // Test SetAllPixelsOpaque() + std::memset(m_test_image.GetPixels(), 0x0u, m_test_image.GetStorageSize()); // Clear alpha + m_test_image.SetAllPixelsOpaque(); + + // Check all pixels now have alpha set to 0xFF + for (u32 y = 0; y < m_test_image.GetHeight(); y++) + { + for (u32 x = 0; x < m_test_image.GetWidth(); x++) + { + const u32* pixel = reinterpret_cast(m_test_image.GetRowPixels(y) + x * sizeof(u32)); + EXPECT_EQ((*pixel & 0xFF000000), 0xFF000000u); + } + } +} + +// Test resize functionality +TEST_F(ImageTest, Resize) +{ + const u32 new_width = 8; + const u32 new_height = 10; + + // Test resize without preservation + m_test_image.Resize(new_width, new_height, false); + EXPECT_EQ(m_test_image.GetWidth(), new_width); + EXPECT_EQ(m_test_image.GetHeight(), new_height); + + // Test resize with format change + m_test_image.Resize(new_width, new_height, ImageFormat::BGRA8, false); + EXPECT_EQ(m_test_image.GetFormat(), ImageFormat::BGRA8); + + // Fill with a known pattern + std::memset(m_test_image.GetPixels(), 0xBBu, m_test_image.GetStorageSize()); + + // Test resize with preservation + const u32 final_width = 6; + const u32 final_height = 7; + m_test_image.Resize(final_width, final_height, true); + + // First bytes should still be 0xBB + EXPECT_EQ(m_test_image.GetPixels()[0], 0xBBu); + EXPECT_EQ(m_test_image.GetWidth(), final_width); + EXPECT_EQ(m_test_image.GetHeight(), final_height); +} + +// Test format conversion - additional formats +TEST_F(ImageTest, RGB565ToRGBA8) +{ + constexpr u32 width = 4; + constexpr u32 height = 4; + Image rgb565_image(width, height, ImageFormat::RGB565); + + // Fill with a test pattern - pure red in RGB565 format (0xF800) + for (u32 y = 0; y < height; y++) + { + u16* row = reinterpret_cast(rgb565_image.GetRowPixels(y)); + for (u32 x = 0; x < width; x++) + { + row[x] = 0xF800; // Red in RGB565 + } + } + + // Convert to RGBA8 + Error err; + std::optional rgba8_image = rgb565_image.ConvertToRGBA8(&err); + ASSERT_TRUE(rgba8_image.has_value()); + EXPECT_EQ(rgba8_image->GetFormat(), ImageFormat::RGBA8); + + // Check the first pixel - should be red with full alpha + const u32* first_pixel = reinterpret_cast(rgba8_image->GetPixels()); + // Red component should be close to 0xFF (might be 0xF8 due to precision) + EXPECT_GE((*first_pixel & 0xFFu), 0xF8u); + // Green and blue should be 0 + EXPECT_EQ((*first_pixel & 0xFF00u), 0u); + EXPECT_EQ((*first_pixel & 0xFF0000u), 0u); + // Alpha should be 0xFF + EXPECT_EQ((*first_pixel & 0xFF000000u), 0xFF000000u); +} + +TEST_F(ImageTest, RGB5A1ToRGBA8) +{ + constexpr u32 width = 4; + constexpr u32 height = 4; + Image rgb5a1_image(width, height, ImageFormat::RGB5A1); + + // Fill with a test pattern - green with alpha in RGB5A1 format (0x07C0) + for (u32 y = 0; y < height; y++) + { + u16* row = reinterpret_cast(rgb5a1_image.GetRowPixels(y)); + for (u32 x = 0; x < width; x++) + { + row[x] = 0x87C0; // Green with alpha bit set + } + } + + // Convert to RGBA8 + Error err; + std::optional rgba8_image = rgb5a1_image.ConvertToRGBA8(&err); + ASSERT_TRUE(rgba8_image.has_value()); + EXPECT_EQ(rgba8_image->GetFormat(), ImageFormat::RGBA8); + + // Check the first pixel - should be green with full alpha + const u32* first_pixel = reinterpret_cast(rgba8_image->GetPixels()); + // Green component should be set, red and blue should be 0 + EXPECT_GE((*first_pixel & 0xFFu), 8u); + EXPECT_LT((*first_pixel & 0xFFu), 16u); + EXPECT_GE(((*first_pixel >> 8) & 0xFFu), 0xF0u); + EXPECT_LT(((*first_pixel >> 8) & 0xFFu), 0xF8u); + EXPECT_EQ((*first_pixel & 0xFF0000u), 0u); + // Alpha should be 0xFF since the alpha bit was set + EXPECT_EQ((*first_pixel & 0xFF000000u), 0xFF000000u); +} + +// Test block sizes for compressed formats +TEST_F(ImageTest, BlockSizes) +{ + // Test with 16x16 image - evenly divisible by block size (4) + const u32 width = 16; + const u32 height = 16; + + // BC1 format (4x4 blocks, 8 bytes per block) + Image bc1_image(width, height, ImageFormat::BC1); + EXPECT_EQ(bc1_image.GetBlocksWide(), width / 4); + EXPECT_EQ(bc1_image.GetBlocksHigh(), height / 4); + EXPECT_EQ(bc1_image.GetPitch(), (width / 4) * 8); + + // Test with non-multiple dimensions + const u32 odd_width = 10; + const u32 odd_height = 6; + + // BC1 format with non-multiple dimensions + Image bc1_odd_image(odd_width, odd_height, ImageFormat::BC1); + // Should round up to multiple of 4 + EXPECT_EQ(bc1_odd_image.GetBlocksWide(), (odd_width + 3) / 4); + EXPECT_EQ(bc1_odd_image.GetBlocksHigh(), (odd_height + 3) / 4); + + // BC3 format (4x4 blocks, 16 bytes per block) + Image bc3_image(width, height, ImageFormat::BC3); + EXPECT_EQ(bc3_image.GetBlocksWide(), width / 4); + EXPECT_EQ(bc3_image.GetBlocksHigh(), height / 4); + EXPECT_EQ(bc3_image.GetPitch(), (width / 4) * 16); + + // Storage size test for BC1 + const u32 bc1_storage = bc1_image.GetStorageSize(); + EXPECT_EQ(bc1_storage, (width / 4) * (height / 4) * 8); + + // Storage size test for BC3 + const u32 bc3_storage = bc3_image.GetStorageSize(); + EXPECT_EQ(bc3_storage, (width / 4) * (height / 4) * 16); +} + +// Test GetPixelsSpan +TEST_F(ImageTest, PixelSpans) +{ + // Test const span + std::span const_span = m_test_image.GetPixelsSpan(); + EXPECT_EQ(const_span.data(), m_test_image.GetPixels()); + EXPECT_EQ(const_span.size(), m_test_image.GetStorageSize()); + + // Test non-const span + std::span mutable_span = m_test_image.GetPixelsSpan(); + EXPECT_EQ(mutable_span.data(), m_test_image.GetPixels()); + EXPECT_EQ(mutable_span.size(), m_test_image.GetStorageSize()); + + // Modify through the span and verify + if (!mutable_span.empty()) + { + mutable_span[0] = 0xAA; + EXPECT_EQ(m_test_image.GetPixels()[0], 0xAAu); + } +} + +// Test TakePixels +TEST_F(ImageTest, TakePixels) +{ + const u32 width = 8; + const u32 height = 6; + Image src(width, height, ImageFormat::RGBA8); + + // Set a test pattern + std::memset(src.GetPixels(), 0xCC, src.GetStorageSize()); + + // Keep track of the original pointer + const u8* original_pixels = src.GetPixels(); + + // Take pixels + Image::PixelStorage pixels = src.TakePixels(); + + // Original image should now be invalid + EXPECT_FALSE(src.IsValid()); + EXPECT_EQ(src.GetWidth(), 0u); + EXPECT_EQ(src.GetHeight(), 0u); + EXPECT_EQ(src.GetFormat(), ImageFormat::None); + EXPECT_EQ(src.GetPixels(), nullptr); + + // Pixels pointer should be the original pointer + EXPECT_EQ(pixels.get(), original_pixels); +} + +// Test invalid operations +TEST_F(ImageTest, OperationsOnInvalidImage) +{ + Image invalid_image; + + // These operations should safely handle invalid images + invalid_image.Clear(); // No-op for invalid images + invalid_image.FlipY(); // No-op for invalid images + + // GetStorageSize should return 0 for invalid image + EXPECT_EQ(invalid_image.GetStorageSize(), 0u); + + // GetPixels should return nullptr for invalid image + EXPECT_EQ(invalid_image.GetPixels(), nullptr); + + // Spans should be empty for invalid image + EXPECT_TRUE(invalid_image.GetPixelsSpan().empty()); +} + +// Test conversion of different formats to RGBA8 +TEST_F(ImageTest, ConvertMultipleFormatsToRGBA8) +{ + const u32 width = 4; + const u32 height = 4; + Error err; + + // Test RGBA8 to RGBA8 (should be essentially a copy) + { + Image rgba8_image(width, height, ImageFormat::RGBA8); + std::memset(rgba8_image.GetPixels(), 0xAA, rgba8_image.GetStorageSize()); + + std::optional converted = rgba8_image.ConvertToRGBA8(&err); + ASSERT_TRUE(converted.has_value()); + EXPECT_EQ(converted->GetFormat(), ImageFormat::RGBA8); + EXPECT_EQ(std::memcmp(converted->GetPixels(), rgba8_image.GetPixels(), rgba8_image.GetStorageSize()), 0); + } + + // Test BGRA8 to RGBA8 (color channels should be swapped) + { + Image bgra8_image(width, height, ImageFormat::BGRA8); + // Set to blue in BGRA8 (0xFFRRGGBB = 0xFF0000FF) + std::fill_n(reinterpret_cast(bgra8_image.GetPixels()), width * height, 0xFF0000FF); + + std::optional converted = bgra8_image.ConvertToRGBA8(&err); + ASSERT_TRUE(converted.has_value()); + + // First pixel should now be red in RGBA8 (0xFFBBGGRR = 0xFFFF0000) + const u32* first_pixel = reinterpret_cast(converted->GetPixels()); + EXPECT_EQ(*first_pixel, 0xFFFF0000u); + } + + // Test A1BGR5 to RGBA8 + { + Image a1bgr5_image(width, height, ImageFormat::A1BGR5); + // Set to blue with alpha in A1BGR5 + std::fill_n(reinterpret_cast(a1bgr5_image.GetPixels()), width * height, static_cast(0x837b)); + + std::optional converted = a1bgr5_image.ConvertToRGBA8(&err); + ASSERT_TRUE(converted.has_value()); + + // First pixel should now be blue with alpha + const u32* first_pixel = reinterpret_cast(converted->GetPixels()); + // Red should be high, green/blue should be low + EXPECT_GE((*first_pixel & 0xFFu), 0x80u); + EXPECT_GE(((*first_pixel >> 8) & 0xFFu), 0x34u); + EXPECT_GE(((*first_pixel >> 16) & 0xFFu), 0xE8u); + // Alpha should be 0xFF + EXPECT_EQ((*first_pixel & 0xFF000000u), 0xFF000000u); + } +} + +// Test calculation functions +TEST_F(ImageTest, PitchAndStorage) +{ + const u32 width = 16; + const u32 height = 8; + + // Test uncompressed format + const u32 rgba_pitch = Image::CalculatePitch(width, height, ImageFormat::RGBA8); + EXPECT_EQ(rgba_pitch, width * 4); // 4 bytes per pixel + + const u32 rgba_storage = Image::CalculateStorageSize(width, height, ImageFormat::RGBA8); + EXPECT_EQ(rgba_storage, rgba_pitch * height); + + // Test compressed format (BC1) + const u32 bc1_pitch = Image::CalculatePitch(width, height, ImageFormat::BC1); + // BC1 uses 8 bytes per 4x4 block + EXPECT_EQ(bc1_pitch, (width / 4) * 8); + + const u32 bc1_storage = Image::CalculateStorageSize(width, height, ImageFormat::BC1); + // Storage should be pitch * number of blocks high + EXPECT_EQ(bc1_storage, bc1_pitch * (height / 4)); +} + +// Test flip Y operation +TEST_F(ImageTest, FlipY) +{ + // Create a test image with different colors on top and bottom + Image test_image(2, 2, ImageFormat::RGBA8); + + // Top row: Red + u32* top_left = reinterpret_cast(test_image.GetRowPixels(0)); + u32* top_right = top_left + 1; + *top_left = *top_right = 0xFF0000FFu; // Red in RGBA + + // Bottom row: Blue + u32* bottom_left = reinterpret_cast(test_image.GetRowPixels(1)); + u32* bottom_right = bottom_left + 1; + *bottom_left = *bottom_right = 0xFFFF0000u; // Blue in RGBA + + // Flip the image + test_image.FlipY(); + + // Now the top row should be blue and the bottom row should be red + top_left = reinterpret_cast(test_image.GetRowPixels(0)); + top_right = top_left + 1; + bottom_left = reinterpret_cast(test_image.GetRowPixels(1)); + bottom_right = bottom_left + 1; + + EXPECT_EQ(*top_left, 0xFFFF0000u); // Blue + EXPECT_EQ(*top_right, 0xFFFF0000u); // Blue + EXPECT_EQ(*bottom_left, 0xFF0000FFu); // Red + EXPECT_EQ(*bottom_right, 0xFF0000FFu); // Red +} + +// Test edge cases +TEST_F(ImageTest, ZeroDimensions) +{ + // Create image with zero width/height + Image zero_width(0, 10, ImageFormat::RGBA8); + EXPECT_FALSE(zero_width.IsValid()); + + Image zero_height(10, 0, ImageFormat::RGBA8); + EXPECT_FALSE(zero_height.IsValid()); + + // Resize to zero dimensions + Image normal(8, 8, ImageFormat::RGBA8); + normal.Resize(0, 8, false); + EXPECT_FALSE(normal.IsValid()); +} + +// Test that Invalidate properly resets all properties +TEST_F(ImageTest, Invalidate) +{ + Image img(16, 16, ImageFormat::RGBA8); + EXPECT_TRUE(img.IsValid()); + EXPECT_NE(img.GetPixels(), nullptr); + + img.Invalidate(); + EXPECT_FALSE(img.IsValid()); + EXPECT_EQ(img.GetWidth(), 0u); + EXPECT_EQ(img.GetHeight(), 0u); + EXPECT_EQ(img.GetFormat(), ImageFormat::None); + EXPECT_EQ(img.GetPixels(), nullptr); +} diff --git a/src/util-tests/util-tests.vcxproj b/src/util-tests/util-tests.vcxproj new file mode 100644 index 000000000..7d2e34e7a --- /dev/null +++ b/src/util-tests/util-tests.vcxproj @@ -0,0 +1,30 @@ + + + + + + + + + + {49953e1b-2ef7-46a4-b88b-1bf9e099093b} + + + {57f6206d-f264-4b07-baf8-11b9bbe1f455} + + + + {15538AD7-2201-45C2-B088-BBB7F37BD7F5} + + + + + + %(AdditionalIncludeDirectories);$(SolutionDir)dep\googletest\include + + + Console + + + + \ No newline at end of file diff --git a/src/util-tests/util-tests.vcxproj.filters b/src/util-tests/util-tests.vcxproj.filters new file mode 100644 index 000000000..25a1583e9 --- /dev/null +++ b/src/util-tests/util-tests.vcxproj.filters @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file