#include "mocktail/graphics/etc2_decoder.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace mocktail::graphics { namespace { using Block8 = std::array; struct Rgba { int r; int g; int b; int a; }; std::vector Decode(EtcFormat format, const std::vector& source, std::uint32_t width, std::uint32_t height) { std::vector output( static_cast(width) * height * EtcDecodedTexelBytes(format), 0xAB); EXPECT_TRUE(DecodeEtcImage(format, source.data(), source.size(), width, height, output.data(), output.size())); return output; } std::vector Bytes(const Block8& block) { return {block.begin(), block.end()}; } void ExpectRgba(const std::vector& texels, std::uint32_t width, std::uint32_t x, std::uint32_t y, Rgba expected) { const std::size_t offset = (static_cast(y) * width + x) * 4; ASSERT_LT(offset + 3, texels.size()); EXPECT_EQ(texels[offset], expected.r) << "x=" << x << " y=" << y; EXPECT_EQ(texels[offset + 1], expected.g) << "x=" << x << " y=" << y; EXPECT_EQ(texels[offset + 2], expected.b) << "x=" << x << " y=" << y; EXPECT_EQ(texels[offset + 3], expected.a) << "x=" << x << " y=" << y; } std::uint16_t Unsigned16(const std::vector& texels, std::size_t index) { return static_cast(texels[index * 2] | (texels[index * 2 + 1] << 8)); } std::int16_t Signed16(const std::vector& texels, std::size_t index) { return static_cast(Unsigned16(texels, index)); } TEST(Etc2DecoderTest, ReportsBlockAndTexelSizes) { EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgb8), 8U); EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgb8A1), 8U); EXPECT_EQ(EtcBlockBytes(EtcFormat::kEtc2Rgba8), 16U); EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacR11), 8U); EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacR11Signed), 8U); EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacRg11), 16U); EXPECT_EQ(EtcBlockBytes(EtcFormat::kEacRg11Signed), 16U); EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgb8), 4U); EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgb8A1), 4U); EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEtc2Rgba8), 4U); EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEacR11), 2U); EXPECT_EQ(EtcDecodedTexelBytes(EtcFormat::kEacRg11Signed), 4U); } TEST(Etc2DecoderTest, DecodesIndividualMode) { // Base colours 8 (left sub-block) and 0 (right), codeword 0 (2/8). // Pixel (0,0) index 1 (+8), pixels in column 1 index 2 (-2). const Block8 block = {0x80, 0x80, 0x80, 0x00, 0x00, 0xF0, 0x00, 0x01}; const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); ExpectRgba(texels, 4, 0, 0, {144, 144, 144, 255}); ExpectRgba(texels, 4, 0, 1, {138, 138, 138, 255}); ExpectRgba(texels, 4, 1, 0, {134, 134, 134, 255}); ExpectRgba(texels, 4, 2, 0, {2, 2, 2, 255}); } TEST(Etc2DecoderTest, DecodesDifferentialMode) { // R/G/B = 16 with delta +1: base colours 132 and 140, modifier +2. const Block8 block = {0x81, 0x81, 0x81, 0x02, 0x00, 0x00, 0x00, 0x00}; const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); ExpectRgba(texels, 4, 0, 0, {134, 134, 134, 255}); ExpectRgba(texels, 4, 2, 3, {142, 142, 142, 255}); } TEST(Etc2DecoderTest, DecodesTMode) { // Red overflows: colour 1 (0,255,0), colour 2 (0,255,136), distance 11. const Block8 block = {0x04, 0xF0, 0x0F, 0x86, 0x00, 0x12, 0x00, 0x03}; const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); ExpectRgba(texels, 4, 0, 0, {11, 255, 147, 255}); ExpectRgba(texels, 4, 0, 1, {0, 244, 125, 255}); ExpectRgba(texels, 4, 1, 0, {0, 255, 136, 255}); ExpectRgba(texels, 4, 1, 1, {0, 255, 0, 255}); } TEST(Etc2DecoderTest, DecodesHMode) { // Green overflows: colour 1 (0,0,0), colour 2 (255,17,255), distance 3. const Block8 block = {0x00, 0x04, 0x78, 0xFA, 0x80, 0x01, 0x80, 0x00}; const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); ExpectRgba(texels, 4, 0, 0, {255, 20, 255, 255}); ExpectRgba(texels, 4, 3, 3, {252, 14, 252, 255}); ExpectRgba(texels, 4, 1, 0, {3, 3, 3, 255}); } TEST(Etc2DecoderTest, DecodesPlanarMode) { // Blue overflows: O=(130,64,0), H=(0,255,0), V=(255,0,255). const Block8 block = {0x40, 0x40, 0x04, 0x02, 0xFE, 0x07, 0xE0, 0x3F}; const auto texels = Decode(EtcFormat::kEtc2Rgb8, Bytes(block), 4, 4); ExpectRgba(texels, 4, 0, 0, {130, 64, 0, 255}); ExpectRgba(texels, 4, 3, 0, {33, 207, 0, 255}); ExpectRgba(texels, 4, 0, 3, {224, 16, 191, 255}); ExpectRgba(texels, 4, 3, 3, {126, 159, 191, 255}); } TEST(Etc2DecoderTest, DecodesPunchthroughAlpha) { // Opaque bit clear: index 2 is transparent black, index 0 has no modifier. const Block8 block = {0x81, 0x81, 0x81, 0x00, 0x00, 0x01, 0x00, 0x02}; const auto texels = Decode(EtcFormat::kEtc2Rgb8A1, Bytes(block), 4, 4); ExpectRgba(texels, 4, 0, 0, {0, 0, 0, 0}); ExpectRgba(texels, 4, 0, 1, {140, 140, 140, 255}); ExpectRgba(texels, 4, 1, 0, {132, 132, 132, 255}); ExpectRgba(texels, 4, 2, 0, {140, 140, 140, 255}); } TEST(Etc2DecoderTest, DecodesRgba8WithAlphaBlockFirst) { // Alpha: base 100, multiplier 2, table 0; pixel (0,0) index 7 (+14). const std::vector block = { 100, 0x20, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, }; const auto texels = Decode(EtcFormat::kEtc2Rgba8, block, 4, 4); ExpectRgba(texels, 4, 0, 0, {138, 138, 138, 128}); ExpectRgba(texels, 4, 1, 0, {138, 138, 138, 94}); } TEST(Etc2DecoderTest, DecodesUnsignedR11) { const Block8 block = {128, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00}; const auto texels = Decode(EtcFormat::kEacR11, Bytes(block), 4, 4); EXPECT_EQ(Unsigned16(texels, 0), 36497); EXPECT_EQ(Unsigned16(texels, 1), 32143); const Block8 zero_multiplier = {0, 0x00, 0, 0, 0, 0, 0, 0}; const auto flat = Decode(EtcFormat::kEacR11, Bytes(zero_multiplier), 1, 1); EXPECT_EQ(Unsigned16(flat, 0), 32); } TEST(Etc2DecoderTest, DecodesSignedR11) { const Block8 block = {0x80, 0x10, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00}; const auto texels = Decode(EtcFormat::kEacR11Signed, Bytes(block), 4, 4); EXPECT_EQ(Signed16(texels, 0), -28956); EXPECT_EQ(Signed16(texels, 1), -32767); } TEST(Etc2DecoderTest, DecodesRg11AsTwoChannels) { const std::vector block = { 128, 0x10, 0xE0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0, 0, 0, 0, 0, }; const auto texels = Decode(EtcFormat::kEacRg11, block, 1, 1); EXPECT_EQ(Unsigned16(texels, 0), 36497); EXPECT_EQ(Unsigned16(texels, 1), 32); } TEST(Etc2DecoderTest, DecodesPartialAndMultipleBlocks) { const Block8 t_mode = {0x04, 0xF0, 0x0F, 0x86, 0x00, 0x12, 0x00, 0x03}; const Block8 differential = {0x81, 0x81, 0x81, 0x02, 0, 0, 0, 0}; std::vector source = Bytes(t_mode); source.insert(source.end(), differential.begin(), differential.end()); const auto texels = Decode(EtcFormat::kEtc2Rgb8, source, 5, 1); ExpectRgba(texels, 5, 0, 0, {11, 255, 147, 255}); ExpectRgba(texels, 5, 1, 0, {0, 255, 136, 255}); ExpectRgba(texels, 5, 4, 0, {134, 134, 134, 255}); } std::vector RandomBlocks(EtcFormat format, std::uint32_t width, std::uint32_t height, std::uint32_t seed) { std::mt19937 rng(seed); std::vector source(((width + 3) / 4) * ((height + 3) / 4) * EtcBlockBytes(format)); for (std::uint8_t& byte : source) { byte = static_cast(rng()); } return source; } TEST(Etc2DecoderTest, DecodesBlockRowBandsLikeWholeImage) { const std::uint32_t width = 13; const std::uint32_t height = 10; const auto source = RandomBlocks(EtcFormat::kEtc2Rgba8, width, height, 1); const auto whole = Decode(EtcFormat::kEtc2Rgba8, source, width, height); std::vector banded(whole.size(), 0xAB); ASSERT_TRUE(DecodeEtcImageBlockRows(EtcFormat::kEtc2Rgba8, source.data(), source.size(), width, height, 1, 2, banded.data(), banded.size())); ASSERT_TRUE(DecodeEtcImageBlockRows(EtcFormat::kEtc2Rgba8, source.data(), source.size(), width, height, 0, 1, banded.data(), banded.size())); EXPECT_EQ(banded, whole); EXPECT_FALSE(DecodeEtcImageBlockRows(EtcFormat::kEtc2Rgba8, source.data(), source.size(), width, height, 2, 2, banded.data(), banded.size())); } TEST(Etc2DecoderTest, DecodesJobsOnWorkersLikeSerialDecode) { struct Case { EtcFormat format; std::uint32_t width; std::uint32_t height; }; const Case cases[] = {{EtcFormat::kEtc2Rgb8, 1030, 770}, {EtcFormat::kEtc2Rgba8, 517, 1024}, {EtcFormat::kEacRg11, 300, 301}, {EtcFormat::kEtc2Rgb8A1, 3, 5}}; std::vector> sources; std::vector> expected; std::vector> outputs; std::vector jobs; for (std::size_t index = 0; index < std::size(cases); ++index) { const Case& c = cases[index]; sources.push_back(RandomBlocks(c.format, c.width, c.height, static_cast(index + 2))); expected.push_back(Decode(c.format, sources.back(), c.width, c.height)); outputs.emplace_back(expected.back().size(), 0xAB); } for (std::size_t index = 0; index < std::size(cases); ++index) { const Case& c = cases[index]; jobs.push_back({c.format, sources[index].data(), sources[index].size(), c.width, c.height, outputs[index].data(), outputs[index].size()}); } std::vector short_output(4 * 4 * 4 - 1, 0xAB); jobs.push_back({EtcFormat::kEtc2Rgb8, sources[0].data(), sources[0].size(), 4, 4, short_output.data(), short_output.size()}); DecodeEtcJobs(jobs.data(), jobs.size(), 4); for (std::size_t index = 0; index < std::size(cases); ++index) { EXPECT_TRUE(jobs[index].ok) << index; EXPECT_EQ(outputs[index], expected[index]) << index; } EXPECT_FALSE(jobs.back().ok); EXPECT_EQ(short_output, std::vector(4 * 4 * 4 - 1, 0xAB)); } // Batches of small textures stay under the old parallel floor; the pool has // to decode them exactly as the caller would. TEST(Etc2DecoderTest, DecodesManySmallJobsLikeSerialDecode) { constexpr std::size_t kJobs = 24; constexpr std::uint32_t kWidth = 64; constexpr std::uint32_t kHeight = 64; std::vector> sources; std::vector> expected; std::vector> outputs; for (std::size_t index = 0; index < kJobs; ++index) { sources.push_back(RandomBlocks(EtcFormat::kEtc2Rgb8, kWidth, kHeight, static_cast(index + 11))); expected.push_back( Decode(EtcFormat::kEtc2Rgb8, sources.back(), kWidth, kHeight)); outputs.emplace_back(expected.back().size(), 0xAB); } std::vector jobs; for (std::size_t index = 0; index < kJobs; ++index) { jobs.push_back({EtcFormat::kEtc2Rgb8, sources[index].data(), sources[index].size(), kWidth, kHeight, outputs[index].data(), outputs[index].size()}); } DecodeEtcJobs(jobs.data(), jobs.size(), 8); for (std::size_t index = 0; index < kJobs; ++index) { EXPECT_TRUE(jobs[index].ok) << index; EXPECT_EQ(outputs[index], expected[index]) << index; } } // The workers outlive each batch, so a later batch must find them idle. TEST(Etc2DecoderTest, DecodesCorrectlyAcrossRepeatedBatches) { constexpr std::uint32_t kWidth = 260; constexpr std::uint32_t kHeight = 132; const std::vector source = RandomBlocks(EtcFormat::kEtc2Rgba8, kWidth, kHeight, 7); const std::vector expected = Decode(EtcFormat::kEtc2Rgba8, source, kWidth, kHeight); for (int round = 0; round < 40; ++round) { std::vector output(expected.size(), 0xAB); EtcDecodeJob job{EtcFormat::kEtc2Rgba8, source.data(), source.size(), kWidth, kHeight, output.data(), output.size()}; DecodeEtcJobs(&job, 1, 8); ASSERT_TRUE(job.ok) << round; ASSERT_EQ(output, expected) << round; } } std::size_t ThreadCount() { return static_cast( std::distance(std::filesystem::directory_iterator("/proc/self/task"), std::filesystem::directory_iterator())); } // Other batch work (ETC2 emit) drains its own queue across the decode // workers. The function runs worker_count times, once of them on the caller, // and a second batch reuses the threads the first one started. TEST(Etc2DecoderTest, RunsAFunctionAcrossPooledWorkersWithoutNewThreads) { struct Probe { std::mutex mutex; std::set threads; std::atomic calls{0}; }; const auto run = [](void* context) { auto* probe = static_cast(context); probe->calls.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(probe->mutex); probe->threads.insert(std::this_thread::get_id()); }; Probe first; RunOnDecodeWorkers(run, &first, 3); EXPECT_EQ(first.calls.load(), 3); // An idle worker may claim two of the slots. EXPECT_GE(first.threads.size(), 2u); EXPECT_LE(first.threads.size(), 3u); EXPECT_EQ(first.threads.count(std::this_thread::get_id()), 1u); const std::size_t pooled = ThreadCount(); Probe second; RunOnDecodeWorkers(run, &second, 3); EXPECT_EQ(second.calls.load(), 3); EXPECT_EQ(ThreadCount(), pooled); } TEST(Etc2DecoderTest, RejectsShortBuffers) { const std::vector source(8, 0); std::vector output(4 * 4 * 4, 0); EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(), 7, 4, 4, output.data(), output.size())); EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(), source.size(), 4, 4, output.data(), output.size() - 1)); EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, source.data(), source.size(), 5, 4, output.data(), output.size())); EXPECT_FALSE(DecodeEtcImage(EtcFormat::kEtc2Rgb8, nullptr, 0, 0, 0, output.data(), output.size())); } namespace reference { constexpr std::array kDistance = {3, 6, 11, 16, 23, 32, 41, 64}; constexpr int kEtc1Modifiers[8][4] = { {2, 8, -2, -8}, {5, 17, -5, -17}, {9, 29, -9, -29}, {13, 42, -13, -42}, {18, 60, -18, -60}, {24, 80, -24, -80}, {33, 106, -33, -106}, {47, 183, -47, -183}, }; constexpr int kEacModifiers[16][8] = { {-3, -6, -9, -15, 2, 5, 8, 14}, {-3, -7, -10, -13, 2, 6, 9, 12}, {-2, -5, -8, -13, 1, 4, 7, 12}, {-2, -4, -6, -13, 1, 3, 5, 12}, {-3, -6, -8, -12, 2, 5, 7, 11}, {-3, -7, -9, -11, 2, 6, 8, 10}, {-4, -7, -8, -11, 3, 6, 7, 10}, {-3, -5, -8, -11, 2, 4, 7, 10}, {-2, -6, -8, -10, 1, 5, 7, 9}, {-2, -5, -8, -10, 1, 4, 7, 9}, {-2, -4, -8, -10, 1, 3, 7, 9}, {-2, -5, -7, -10, 1, 4, 6, 9}, {-3, -4, -7, -10, 2, 3, 6, 9}, {-1, -2, -3, -10, 0, 1, 2, 9}, {-4, -6, -8, -9, 3, 5, 7, 8}, {-3, -5, -7, -9, 2, 4, 6, 8}, }; inline std::uint8_t Clamp255(int value) { return static_cast(std::clamp(value, 0, 255)); } inline int Expand4(int value) { return (value << 4) | value; } inline int Expand5(int value) { return (value << 3) | (value >> 2); } inline int Expand6(int value) { return (value << 2) | (value >> 4); } inline int Expand7(int value) { return (value << 1) | (value >> 6); } inline void DecodeRgbBlock(const std::uint8_t* b, bool punchthrough, std::array* out) { static constexpr int kDelta[8] = {0, 1, 2, 3, -4, -3, -2, -1}; const bool diff_or_opaque = (b[3] & 2) != 0; const bool opaque = !punchthrough || diff_or_opaque; const int msb = (b[4] << 8) | b[5]; const int lsb = (b[6] << 8) | b[7]; auto index_of = [msb, lsb](int texel) { return (((msb >> texel) & 1) << 1) | ((lsb >> texel) & 1); }; auto write = [&](int texel, int r, int g, int bl, int a) { (*out)[texel * 4] = Clamp255(r); (*out)[texel * 4 + 1] = Clamp255(g); (*out)[texel * 4 + 2] = Clamp255(bl); (*out)[texel * 4 + 3] = static_cast(a); }; const int r_sum = (b[0] >> 3) + kDelta[b[0] & 7]; const int g_sum = (b[1] >> 3) + kDelta[b[1] & 7]; const int b_sum = (b[2] >> 3) + kDelta[b[2] & 7]; const bool individual = !punchthrough && !diff_or_opaque; if (!individual && (r_sum < 0 || r_sum > 31 || g_sum < 0 || g_sum > 31)) { std::array, 4> paint{}; if (r_sum < 0 || r_sum > 31) { const std::array c1 = { Expand4((((b[0] >> 3) & 3) << 2) | (b[0] & 3)), Expand4(b[1] >> 4), Expand4(b[1] & 15)}; const std::array c2 = {Expand4(b[2] >> 4), Expand4(b[2] & 15), Expand4(b[3] >> 4)}; const int d = kDistance[(((b[3] >> 2) & 3) << 1) | (b[3] & 1)]; for (int c = 0; c < 3; ++c) { paint[0][c] = c1[c]; paint[1][c] = c2[c] + d; paint[2][c] = c2[c]; paint[3][c] = c2[c] - d; } } else { const std::array c1 = { Expand4((b[0] >> 3) & 15), Expand4(((b[0] & 7) << 1) | ((b[1] >> 4) & 1)), Expand4((b[1] & 8) | ((b[1] & 3) << 1) | (b[2] >> 7))}; const std::array c2 = { Expand4((b[2] >> 3) & 15), Expand4(((b[2] & 7) << 1) | (b[3] >> 7)), Expand4((b[3] >> 3) & 15)}; const int v1 = (c1[0] << 16) | (c1[1] << 8) | c1[2]; const int v2 = (c2[0] << 16) | (c2[1] << 8) | c2[2]; const int d = kDistance[(b[3] & 4) | ((b[3] & 1) << 1) | (v1 >= v2 ? 1 : 0)]; for (int c = 0; c < 3; ++c) { paint[0][c] = c1[c] + d; paint[1][c] = c1[c] - d; paint[2][c] = c2[c] + d; paint[3][c] = c2[c] - d; } } for (int texel = 0; texel < 16; ++texel) { const int index = index_of(texel); if (!opaque && index == 2) { write(texel, 0, 0, 0, 0); } else { write(texel, paint[index][0], paint[index][1], paint[index][2], 255); } } return; } if (!individual && (b_sum < 0 || b_sum > 31)) { const int ro = Expand6((b[0] >> 1) & 63); const int go = Expand7(((b[0] & 1) << 6) | ((b[1] >> 1) & 63)); const int bo = Expand6(((b[1] & 1) << 5) | (b[2] & 0x18) | ((b[2] & 3) << 1) | (b[3] >> 7)); const int rh = Expand6(((b[3] & 0x7c) >> 1) | (b[3] & 1)); const int gh = Expand7(b[4] >> 1); const int bh = Expand6(((b[4] & 1) << 5) | (b[5] >> 3)); const int rv = Expand6(((b[5] & 7) << 3) | (b[6] >> 5)); const int gv = Expand7(((b[6] & 31) << 2) | (b[7] >> 6)); const int bv = Expand6(b[7] & 63); for (int x = 0; x < 4; ++x) { for (int y = 0; y < 4; ++y) { write(x * 4 + y, (x * (rh - ro) + y * (rv - ro) + 4 * ro + 2) >> 2, (x * (gh - go) + y * (gv - go) + 4 * go + 2) >> 2, (x * (bh - bo) + y * (bv - bo) + 4 * bo + 2) >> 2, 255); } } return; } std::array, 2> base{}; for (int c = 0; c < 3; ++c) { if (individual) { base[0][c] = Expand4(b[c] >> 4); base[1][c] = Expand4(b[c] & 15); } else { base[0][c] = Expand5(b[c] >> 3); base[1][c] = Expand5((b[c] >> 3) + kDelta[b[c] & 7]); } } const int tables[2] = {b[3] >> 5, (b[3] >> 2) & 7}; const bool flipped = (b[3] & 1) != 0; for (int x = 0; x < 4; ++x) { for (int y = 0; y < 4; ++y) { const int texel = x * 4 + y; const int index = index_of(texel); if (!opaque && index == 2) { write(texel, 0, 0, 0, 0); continue; } const int sub = flipped ? (y >= 2 ? 1 : 0) : (x >= 2 ? 1 : 0); const int modifier = (!opaque && index == 0) ? 0 : kEtc1Modifiers[tables[sub]][index]; write(texel, base[sub][0] + modifier, base[sub][1] + modifier, base[sub][2] + modifier, 255); } } } inline int EacIndex(const std::uint8_t* b, int texel) { std::uint64_t bits = 0; for (int i = 2; i < 8; ++i) { bits = (bits << 8) | b[i]; } return static_cast((bits >> (45 - texel * 3)) & 7); } inline int EacModifier(const std::uint8_t* b, int texel) { return kEacModifiers[b[1] & 15][EacIndex(b, texel)]; } inline std::uint16_t DecodeR11(const std::uint8_t* b, int texel, bool is_signed) { const int multiplier = b[1] >> 4; const int modifier = EacModifier(b, texel); const int scaled = multiplier != 0 ? modifier * multiplier * 8 : modifier; if (!is_signed) { const int value = std::clamp(b[0] * 8 + 4 + scaled, 0, 2047); return static_cast((value << 5) | (value >> 6)); } const int base = std::max(-127, static_cast(static_cast(b[0]))); const int value = std::clamp(base * 8 + scaled, -1023, 1023); const int magnitude = value < 0 ? -value : value; const int expanded = (magnitude << 5) | (magnitude >> 5); return static_cast( static_cast(value < 0 ? -expanded : expanded)); } inline void DecodeReference(EtcFormat format, const std::uint8_t* source, std::uint32_t width, std::uint32_t height, std::uint8_t* destination) { const std::size_t texel_bytes = EtcDecodedTexelBytes(format); const std::size_t block_bytes = EtcBlockBytes(format); const std::uint64_t blocks_wide = (static_cast(width) + 3) / 4; const std::uint64_t blocks_high = (static_cast(height) + 3) / 4; std::array rgba{}; for (std::uint64_t by = 0; by < blocks_high; ++by) { for (std::uint64_t bx = 0; bx < blocks_wide; ++bx) { const std::uint8_t* block = source + (by * blocks_wide + bx) * block_bytes; if (format == EtcFormat::kEtc2Rgb8 || format == EtcFormat::kEtc2Rgb8A1) { DecodeRgbBlock(block, format == EtcFormat::kEtc2Rgb8A1, &rgba); } else if (format == EtcFormat::kEtc2Rgba8) { DecodeRgbBlock(block + 8, false, &rgba); } for (std::uint32_t y = 0; y < 4 && by * 4 + y < height; ++y) { for (std::uint32_t x = 0; x < 4 && bx * 4 + x < width; ++x) { const int texel = static_cast(x * 4 + y); std::uint8_t* out = destination + ((by * 4 + y) * width + bx * 4 + x) * texel_bytes; switch (format) { case EtcFormat::kEtc2Rgb8: case EtcFormat::kEtc2Rgb8A1: case EtcFormat::kEtc2Rgba8: std::copy_n(rgba.data() + texel * 4, 4, out); if (format == EtcFormat::kEtc2Rgba8) { const int alpha = block[0] + EacModifier(block, texel) * (block[1] >> 4); out[3] = Clamp255(alpha); } break; case EtcFormat::kEacR11: case EtcFormat::kEacR11Signed: case EtcFormat::kEacRg11: case EtcFormat::kEacRg11Signed: { const bool is_signed = format == EtcFormat::kEacR11Signed || format == EtcFormat::kEacRg11Signed; const int channels = static_cast(texel_bytes / 2); for (int channel = 0; channel < channels; ++channel) { const std::uint16_t value = DecodeR11(block + channel * 8, texel, is_signed); out[channel * 2] = static_cast(value); out[channel * 2 + 1] = static_cast(value >> 8); } break; } } } } } } } } // namespace reference TEST(Etc2DecoderTest, ComparesBitExactAgainstScalarReferenceAcrossAllFormats) { const EtcFormat formats[] = { EtcFormat::kEtc2Rgb8, EtcFormat::kEtc2Rgb8A1, EtcFormat::kEtc2Rgba8, EtcFormat::kEacR11, EtcFormat::kEacR11Signed, EtcFormat::kEacRg11, EtcFormat::kEacRg11Signed, }; const struct { std::uint32_t width; std::uint32_t height; } sizes[] = { {1, 1}, {4, 4}, {7, 5}, {16, 16}, {67, 59}, {128, 128}, }; for (std::size_t f = 0; f < std::size(formats); ++f) { const EtcFormat format = formats[f]; for (std::size_t s = 0; s < std::size(sizes); ++s) { const std::uint32_t width = sizes[s].width; const std::uint32_t height = sizes[s].height; const auto source = RandomBlocks( format, width, height, static_cast(0xABCD1234 + f * 100 + s)); const auto actual = Decode(format, source, width, height); std::vector expected(actual.size(), 0); reference::DecodeReference(format, source.data(), width, height, expected.data()); EXPECT_EQ(actual, expected) << "Mismatch in format=" << static_cast(format) << " width=" << width << " height=" << height; } } } } // namespace } // namespace mocktail::graphics