diff options
Diffstat (limited to 'src/decoder/test')
-rw-r--r-- | src/decoder/test/astc_fuzzer.cc | 36 | ||||
-rw-r--r-- | src/decoder/test/codec_test.cc | 181 | ||||
-rw-r--r-- | src/decoder/test/endpoint_codec_test.cc | 464 | ||||
-rw-r--r-- | src/decoder/test/footprint_test.cc | 97 | ||||
-rw-r--r-- | src/decoder/test/image_utils.h | 217 | ||||
-rw-r--r-- | src/decoder/test/integer_sequence_codec_test.cc | 337 | ||||
-rw-r--r-- | src/decoder/test/intermediate_astc_block_test.cc | 453 | ||||
-rw-r--r-- | src/decoder/test/logical_astc_block_test.cc | 273 | ||||
-rw-r--r-- | src/decoder/test/partition_test.cc | 263 | ||||
-rw-r--r-- | src/decoder/test/physical_astc_block_test.cc | 361 | ||||
-rw-r--r-- | src/decoder/test/quantization_test.cc | 288 | ||||
-rw-r--r-- | src/decoder/test/weight_infill_test.cc | 69 |
12 files changed, 3039 insertions, 0 deletions
diff --git a/src/decoder/test/astc_fuzzer.cc b/src/decoder/test/astc_fuzzer.cc new file mode 100644 index 0000000..f152675 --- /dev/null +++ b/src/decoder/test/astc_fuzzer.cc @@ -0,0 +1,36 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ASTC fuzzing wrapper to help with fuzz testing. + +#include "src/decoder/codec.h" + +#include <benchmark/benchmark.h> + +#include <vector> + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + std::string error; + std::unique_ptr<astc_codec::ASTCFile> file = + astc_codec::ASTCFile::LoadFromMemory(reinterpret_cast<const char*>(data), + size, &error); + if (file) { + std::vector<uint8_t> out_buffer(file->GetWidth() * file->GetHeight() * 4); + bool result = astc_codec::DecompressToImage( + *file, out_buffer.data(), out_buffer.size(), file->GetWidth() * 4); + benchmark::DoNotOptimize(result); + } + + return 0; +} diff --git a/src/decoder/test/codec_test.cc b/src/decoder/test/codec_test.cc new file mode 100644 index 0000000..936eed3 --- /dev/null +++ b/src/decoder/test/codec_test.cc @@ -0,0 +1,181 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/codec.h" +#include "include/astc-codec/astc-codec.h" +#include "src/decoder/test/image_utils.h" + +#include <gtest/gtest.h> + +#include <string> + +namespace astc_codec { + +static void PrintTo(FootprintType footprint, std::ostream* os) { + switch (footprint) { + case FootprintType::k4x4: *os << "FootprintType::k4x4"; break; + case FootprintType::k5x4: *os << "FootprintType::k5x4"; break; + case FootprintType::k5x5: *os << "FootprintType::k5x5"; break; + case FootprintType::k6x5: *os << "FootprintType::k6x5"; break; + case FootprintType::k6x6: *os << "FootprintType::k6x6"; break; + case FootprintType::k8x5: *os << "FootprintType::k8x5"; break; + case FootprintType::k8x6: *os << "FootprintType::k8x6"; break; + case FootprintType::k10x5: *os << "FootprintType::k10x5"; break; + case FootprintType::k10x6: *os << "FootprintType::k10x6"; break; + case FootprintType::k8x8: *os << "FootprintType::k8x8"; break; + case FootprintType::k10x8: *os << "FootprintType::k10x8"; break; + case FootprintType::k10x10: *os << "FootprintType::k10x10"; break; + case FootprintType::k12x10: *os << "FootprintType::k12x10"; break; + case FootprintType::k12x12: *os << "FootprintType::k12x12"; break; + default: + *os << "<Unexpected FootprintType " + << static_cast<uint32_t>(footprint) << ">"; + } +} + +namespace { + +using ::testing::TestWithParam; +using ::testing::ValuesIn; + +ImageBuffer LoadGoldenImageWithAlpha(std::string basename) { + const std::string filename = + std::string("src/decoder/testdata/") + basename + ".bmp"; + ImageBuffer result; + LoadGoldenBmp(filename, &result); + EXPECT_EQ(result.BytesPerPixel(), 4); + return result; +} + +struct ImageTestParams { + std::string image_name; + FootprintType footprint; + size_t width; + size_t height; +}; + +static void PrintTo(const ImageTestParams& params, std::ostream* os) { + *os << "ImageTestParams(" << params.image_name << ", " << params.width << "x" + << params.height << ", "; + PrintTo(params.footprint, os); + *os << ")"; +} + +TEST(CodecTest, InvalidInput) { + const size_t valid_width = 16; + const size_t valid_height = 16; + const size_t valid_stride = valid_width * 4; + + const std::vector<uint8_t> data(256); + std::vector<uint8_t> output(valid_width * valid_height * 4); + + // Invalid footprint. + EXPECT_FALSE(ASTCDecompressToRGBA( + data.data(), data.size(), valid_width, valid_height, + FootprintType::kCount, output.data(), output.size(), valid_stride)); + + // Fail for 0 width or height. + EXPECT_FALSE(ASTCDecompressToRGBA(data.data(), data.size(), 0, valid_height, + FootprintType::k4x4, output.data(), + output.size(), valid_stride)); + EXPECT_FALSE(ASTCDecompressToRGBA(data.data(), data.size(), valid_width, 0, + FootprintType::k4x4, output.data(), + output.size(), valid_stride)); + + // Fail for data size that's not a multiple of block size. + EXPECT_FALSE(ASTCDecompressToRGBA( + data.data(), data.size() - 1, valid_width, valid_height, + FootprintType::k4x4, output.data(), output.size(), valid_stride)); + // Fail for data size that doesn't match the block count. + EXPECT_FALSE(ASTCDecompressToRGBA( + data.data(), data.size() - 16, valid_width, valid_height, + FootprintType::k4x4, output.data(), output.size(), valid_stride)); + + // Fail for invalid stride. + EXPECT_FALSE(ASTCDecompressToRGBA( + data.data(), data.size(), valid_width, valid_height, FootprintType::k4x4, + output.data(), output.size(), valid_stride - 1)); + + // Fail for invalid output size. + EXPECT_FALSE(ASTCDecompressToRGBA( + data.data(), data.size(), valid_width, valid_height, FootprintType::k4x4, + output.data(), output.size() - 1, valid_stride)); +} + +class CodecTest : public TestWithParam<ImageTestParams> {}; + +TEST_P(CodecTest, PublicAPI) { + const auto& params = GetParam(); + const std::string astc = LoadASTCFile(params.image_name); + + ImageBuffer our_decoded_image; + our_decoded_image.Allocate(params.width, params.height, 4); + ASSERT_TRUE(ASTCDecompressToRGBA( + reinterpret_cast<const uint8_t*>(astc.data()), astc.size(), params.width, + params.height, params.footprint, our_decoded_image.Data().data(), + our_decoded_image.DataSize(), our_decoded_image.Stride())); + + // Check that the decoded image is *very* similar to the library decoding + // of an ASTC texture. They may not be exact due to differences in how we + // convert a 16-bit float to an 8-bit integer. + ImageBuffer decoded_image = LoadGoldenImageWithAlpha(params.image_name); + CompareSumOfSquaredDifferences(decoded_image, our_decoded_image, 1.0); +} + +TEST_P(CodecTest, DecompressToImage) { + const auto& params = GetParam(); + + std::string error; + std::unique_ptr<ASTCFile> image_file = ASTCFile::LoadFile( + std::string("src/decoder/testdata/") + params.image_name + ".astc", + &error); + ASSERT_TRUE(image_file) << "Failed to load " << params.image_name << ": " + << error; + + ASSERT_TRUE(image_file->GetFootprint()); + EXPECT_EQ(params.footprint, image_file->GetFootprint().value().Type()); + + ImageBuffer our_decoded_image; + our_decoded_image.Allocate(image_file->GetWidth(), image_file->GetHeight(), + 4); + + ASSERT_TRUE(DecompressToImage(*image_file, our_decoded_image.Data().data(), + our_decoded_image.DataSize(), + our_decoded_image.Stride())); + + // Check that the decoded image is *very* similar to the library decoding + // of an ASTC texture. They may not be exact due to differences in how we + // convert a 16-bit float to an 8-bit integer. + ImageBuffer decoded_image = LoadGoldenImageWithAlpha(params.image_name); + CompareSumOfSquaredDifferences(decoded_image, our_decoded_image, 1.0); +} + +// Test to make sure that reading out color values from blocks in a real-world +// image isn't terribly wrong, either. +std::vector<ImageTestParams> GetTransparentImageTestParams() { + return { + // image_name astc footprint width height + { "atlas_small_4x4", FootprintType::k4x4, 256, 256 }, + { "atlas_small_5x5", FootprintType::k5x5, 256, 256 }, + { "atlas_small_6x6", FootprintType::k6x6, 256, 256 }, + { "atlas_small_8x8", FootprintType::k8x8, 256, 256 }, + }; +} + +INSTANTIATE_TEST_CASE_P(Transparent, CodecTest, + ValuesIn(GetTransparentImageTestParams())); + +} // namespace + +} // namespace astc_codec diff --git a/src/decoder/test/endpoint_codec_test.cc b/src/decoder/test/endpoint_codec_test.cc new file mode 100644 index 0000000..f2fef54 --- /dev/null +++ b/src/decoder/test/endpoint_codec_test.cc @@ -0,0 +1,464 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/endpoint_codec.h" +#include "src/decoder/intermediate_astc_block.h" +#include "src/decoder/test/image_utils.h" + +#include <random> +#include <string> +#include <utility> +#include <vector> + +#include <gtest/gtest.h> +#include <gmock/gmock.h> + +namespace astc_codec { + +namespace { + +using ::testing::AllOf; +using ::testing::AnyOf; +using ::testing::Each; +using ::testing::Eq; +using ::testing::Ge; +using ::testing::Le; +using ::testing::Ne; +using ::testing::Optional; +using ::testing::Pointwise; +using ::testing::SizeIs; +using ::testing::Pair; + +constexpr std::array<EndpointEncodingMode, 6> kEndpointEncodingModes = {{ + EndpointEncodingMode::kDirectLuma, + EndpointEncodingMode::kDirectLumaAlpha, + EndpointEncodingMode::kBaseScaleRGB, + EndpointEncodingMode::kBaseScaleRGBA, + EndpointEncodingMode::kDirectRGB, + EndpointEncodingMode::kDirectRGBA }}; + +const std::array<std::pair<RgbaColor, RgbaColor>, 3> kBlueContractPairs = {{ + std::make_pair(RgbaColor{{ 22, 18, 30, 59 }}, + RgbaColor{{ 162, 148, 155, 59 }}), + std::make_pair(RgbaColor{{ 22, 30, 27, 36 }}, + RgbaColor{{ 228, 221, 207, 36 }}), + std::make_pair(RgbaColor{{ 54, 60, 55, 255 }}, + RgbaColor{{ 23, 30, 27, 255 }}) + }}; + +// Used to directly initialize std::pairs of colors with initializer lists +// e.g. MakeColors({{ r, g, b, a }}, {{ r, g, b, a }}); +std::pair<RgbaColor, RgbaColor> MakeColors(RgbaColor&& a, RgbaColor&& b) { + return std::make_pair(a, b); +} + +// Returns |high| and |low| as they would be decoded using the quantization +// factor |quant| for the ColorEndpointMode |mode|. +std::pair<RgbaColor, RgbaColor> TestColors( + RgbaColor low, RgbaColor high, int quant, EndpointEncodingMode mode) { + ColorEndpointMode astc_mode; + std::vector<int> encoded; + const bool needs_swap = + EncodeColorsForMode(low, high, quant, mode, &astc_mode, &encoded); + + RgbaColor decoded_low, decoded_high; + DecodeColorsForMode(encoded, quant, astc_mode, &decoded_low, &decoded_high); + + if (needs_swap) { + return std::make_pair(decoded_high, decoded_low); + } else { + return std::make_pair(decoded_low, decoded_high); + } +} + +// Returns true if the argument tuple entries only differ by at most x. +MATCHER_P(IsCloseTo, x, "") { + const auto& a = ::testing::get<0>(arg); + const auto& b = ::testing::get<1>(arg); + return (a > b) ? ((a - b) <= x) : ((b - a) <= x); +} + +// Test to make sure that the range of values that we get as they are +// quantized remains within what we pass as |quant|. +TEST(EndpointCodecTest, QuantRanges) { + const RgbaColor low {{ 0, 0, 0, 0 }}; + const RgbaColor high {{ 255, 255, 255, 255 }}; + + std::vector<int> result; + for (const auto& mode : kEndpointEncodingModes) { + for (int i = 5; i < 256; ++i) { + ColorEndpointMode astc_mode; + const bool needs_swap = + EncodeColorsForMode(low, high, i, mode, &astc_mode, &result); + EXPECT_EQ(result.size(), NumValuesForEncodingMode(mode)) << i; + EXPECT_EQ(result.size(), NumColorValuesForEndpointMode(astc_mode)) << i; + + // ASTC mode shouldn't use base/offset when endpoints are so far apart. + EXPECT_THAT(astc_mode, Ne(ColorEndpointMode::kLDRRGBBaseOffset)); + EXPECT_THAT(astc_mode, Ne(ColorEndpointMode::kLDRRGBABaseOffset)); + + EXPECT_THAT(result, Each(AllOf(Ge(0), Le(i)))) + << "Mode: " << static_cast<int>(mode); + // We don't care if we need to swap the weights in this test + EXPECT_TRUE(needs_swap || !needs_swap); + } + } +} + +// Test to make sure that each mode that directly encodes colors can effectively +// encode both black and white +TEST(EndpointCodecTest, ExtremeDirectEncodings) { + const RgbaColor kWhite {{ 255, 255, 255, 255 }}; + const RgbaColor kBlack {{ 0, 0, 0, 255 }}; + + std::vector<int> encoded; + for (const auto& mode : kEndpointEncodingModes) { + for (int i = 5; i < 256; ++i) { + const auto expected = std::make_pair(kWhite, kBlack); + EXPECT_EQ(TestColors(kWhite, kBlack, i, mode), expected) + << "Range: " << i << ", Mode: " << static_cast<int>(mode); + } + } +} + +// According to the spec, this is used for colors close to gray. The values +// chosen here were according to the spec. +TEST(EndpointCodecTest, UsesBlueContract) { + std::vector<int> vals = { 132, 127, 116, 112, 183, 180, 31, 22 }; + EXPECT_TRUE(UsesBlueContract(255, ColorEndpointMode::kLDRRGBDirect, vals)); + EXPECT_TRUE(UsesBlueContract(255, ColorEndpointMode::kLDRRGBADirect, vals)); + + // For the offset modes the only way to trigger the blue contract mode is if + // we force the subtraction in the decoding procedure (See section C.2.14 of + // the spec), so we need to set the 7th bit to 1 for all of the odd-numbered + // values + vals[1] &= 0xBF; + vals[3] &= 0xBF; + vals[5] &= 0xBF; + vals[7] &= 0xBF; + + EXPECT_FALSE( + UsesBlueContract(255, ColorEndpointMode::kLDRRGBBaseOffset, vals)); + EXPECT_FALSE( + UsesBlueContract(255, ColorEndpointMode::kLDRRGBABaseOffset, vals)); + + vals[1] |= 0x40; + vals[3] |= 0x40; + vals[5] |= 0x40; + vals[7] |= 0x40; + + EXPECT_TRUE( + UsesBlueContract(255, ColorEndpointMode::kLDRRGBBaseOffset, vals)); + EXPECT_TRUE( + UsesBlueContract(255, ColorEndpointMode::kLDRRGBABaseOffset, vals)); + + // All other LDR endpoint modes should return no blue contract + for (int max_val : { 255, 127, 11 }) { + for (auto mode : { ColorEndpointMode::kLDRLumaDirect, + ColorEndpointMode::kLDRLumaBaseOffset, + ColorEndpointMode::kLDRLumaAlphaDirect, + ColorEndpointMode::kLDRLumaAlphaBaseOffset, + ColorEndpointMode::kLDRRGBBaseScale, + ColorEndpointMode::kLDRRGBBaseScaleTwoA }) { + EXPECT_FALSE(UsesBlueContract(max_val, mode, vals)); + } + } +} + +// Make sure that encoding and decoding for the direct luminance mode works. +TEST(EndpointCodecTest, LumaDirect) { + const auto mode = EndpointEncodingMode::kDirectLuma; + + // With a 255 quantizer, all greys should be exact. + for (int i = 0; i < 255; ++i) { + for (int j = 0; j < 255; ++j) { + EXPECT_EQ(TestColors({{ i, i, i, 255 }}, {{ j, j, j, 255 }}, 255, mode), + MakeColors({{ i, i, i, 255 }}, {{ j, j, j, 255 }})); + } + } + + // If we have almost grey, then they should encode to grey. + EXPECT_EQ(TestColors({{ 247, 248, 246, 255 }}, {{ 2, 3, 1, 255 }}, 255, mode), + MakeColors({{ 247, 247, 247, 255 }}, {{ 2, 2, 2, 255 }})); + + EXPECT_EQ(TestColors({{ 80, 80, 50, 255 }}, {{ 99, 255, 6, 255 }}, 255, mode), + MakeColors({{ 70, 70, 70, 255 }}, {{ 120, 120, 120, 255 }})); + + // If we have almost greys and a really small quantizer, it should be white + // and black (literally). + EXPECT_EQ(TestColors({{ 247, 248, 246, 255 }}, {{ 2, 3, 1, 255 }}, 15, mode), + MakeColors({{ 255, 255, 255, 255 }}, {{ 0, 0, 0, 255 }})); + + // The average of 64, 127, and 192 is 127.666..., so it should round to + // 130 instead of 125. + EXPECT_EQ(TestColors({{ 64, 127, 192, 255 }}, {{ 0, 0, 0, 255 }}, 63, mode), + MakeColors({{ 130, 130, 130, 255 }}, {{ 0, 0, 0, 255 }})); + + // If we have almost grey, then they should encode to grey -- similar to + // direct encoding since the encoded colors differ by < 63. + EXPECT_EQ(TestColors({{ 80, 80, 50, 255 }}, {{ 99, 255, 6, 255 }}, 255, mode), + MakeColors({{ 70, 70, 70, 255 }}, {{ 120, 120, 120, 255 }})); + + // Low precision colors should still encode pretty well with base/offset. + EXPECT_EQ(TestColors({{ 35, 36, 38, 255 }}, {{ 42, 43, 40, 255 }}, 47, mode), + MakeColors({{ 38, 38, 38, 255 }}, {{ 43, 43, 43, 255 }})); + + EXPECT_EQ(TestColors({{ 39, 42, 40, 255 }}, {{ 18, 20, 21, 255 }}, 39, mode), + MakeColors({{ 39, 39, 39, 255 }}, {{ 19, 19, 19, 255 }})); +} + +// Test encoding and decoding for the base-offset luminance mode. +TEST(EndpointCodecTest, LumaAlphaDirect) { + const auto mode = EndpointEncodingMode::kDirectLumaAlpha; + + // With a 255 quantizer, all greys should be exact. + for (int i = 0; i < 255; ++i) { + for (int j = 0; j < 255; ++j) { + EXPECT_EQ(TestColors({{ i, i, i, j }}, {{ j, j, j, i }}, 255, mode), + MakeColors({{ i, i, i, j }}, {{ j, j, j, i }})); + } + } + + // If we have almost grey, then they should encode to grey. + EXPECT_EQ(TestColors({{ 247, 248, 246, 250 }}, {{ 2, 3, 1, 172 }}, 255, mode), + MakeColors({{ 247, 247, 247, 250 }}, {{ 2, 2, 2, 172 }})); + + EXPECT_EQ(TestColors({{ 80, 80, 50, 0 }}, {{ 99, 255, 6, 255 }}, 255, mode), + MakeColors({{ 70, 70, 70, 0 }}, {{ 120, 120, 120, 255 }})); + + // If we have almost greys and a really small quantizer, it should be white + // and black (literally). + EXPECT_EQ(TestColors({{ 247, 248, 246, 253 }}, {{ 2, 3, 1, 3 }}, 15, mode), + MakeColors({{ 255, 255, 255, 255 }}, {{ 0, 0, 0, 0 }})); + + // The average of 64, 127, and 192 is 127.666..., so it should round to + // 130 instead of 125. The alpha in this case is independent. + EXPECT_EQ(TestColors({{ 64, 127, 192, 127 }}, {{ 0, 0, 0, 20 }}, 63, mode), + MakeColors({{ 130, 130, 130, 125 }}, {{ 0, 0, 0, 20 }})); +} + +// Test encoding for the direct RGB mode. +TEST(EndpointCodecTest, RGBDirect) { + const auto mode = EndpointEncodingMode::kDirectRGB; + + // Colors should be encoded exactly with a 255 quantizer. + std::mt19937 random(0xdeadbeef); + std::uniform_int_distribution<int> byte_distribution(0, 255); + + for (int i = 0; i < 100; ++i) { + RgbaColor low, high; + for (auto& x : high) { x = byte_distribution(random); } + for (auto& x : low) { x = byte_distribution(random); } + high[3] = low[3] = 255; // RGB Direct mode has opaque alpha. + + EXPECT_EQ(TestColors(low, high, 255, mode), std::make_pair(low, high)) + << "Random iter: " << i; + } + + // For each of the following tests, order of endpoints shouldn't have any + // bearing on the quantization properties, so we should be able to switch + // endpoints as we see fit and have them generate the same flipped encoded + // pairs. + + EXPECT_EQ(TestColors({{ 64, 127, 192, 255 }}, {{ 0, 0, 0, 255 }}, 63, mode), + MakeColors({{ 65, 125, 190, 255 }}, {{ 0, 0, 0, 255 }})); + + EXPECT_EQ(TestColors({{ 0, 0, 0, 255 }}, {{ 64, 127, 192, 255 }}, 63, mode), + MakeColors({{ 0, 0, 0, 255 }}, {{ 65, 125, 190, 255 }})); + + EXPECT_EQ(TestColors({{ 1, 2, 94, 255 }}, {{ 168, 255, 13, 255 }}, 7, mode), + MakeColors({{ 0, 0, 109, 255 }}, {{ 182, 255, 0, 255 }})); + + // Colors close to grey will likely need a blue contract. + EXPECT_EQ(TestColors(kBlueContractPairs[0].first, + kBlueContractPairs[0].second, 31, mode), + MakeColors({{ 24, 20, 33, 255 }}, {{ 160, 148, 156, 255 }})); + + EXPECT_EQ(TestColors(kBlueContractPairs[0].second, + kBlueContractPairs[0].first, 31, mode), + MakeColors({{ 160, 148, 156, 255 }}, {{ 24, 20, 33, 255 }})); + + EXPECT_EQ(TestColors(kBlueContractPairs[1].first, + kBlueContractPairs[1].second, 7, mode), + MakeColors({{ 18, 36, 36, 255 }}, {{ 237, 219, 219, 255 }})); + + EXPECT_EQ(TestColors(kBlueContractPairs[1].second, + kBlueContractPairs[1].first, 7, mode), + MakeColors({{ 237, 219, 219, 255 }}, {{ 18, 36, 36, 255 }})); + + // Colors close to grey (and each other) will likely need a blue contract AND + // use the offset mode for encoding + EXPECT_EQ(TestColors(kBlueContractPairs[2].first, + kBlueContractPairs[2].second, 31, mode), + MakeColors({{ 53, 59, 53, 255 }}, {{ 24, 30, 26, 255 }})); + + EXPECT_EQ(TestColors(kBlueContractPairs[2].second, + kBlueContractPairs[2].first, 31, mode), + MakeColors({{ 24, 30, 26, 255 }}, {{ 53, 59, 53, 255 }})); + + // Colors close to each other, but not to grey will likely only use the offset + // mode and not the blue-contract modes. + EXPECT_EQ(TestColors({{ 22, 148, 30, 59 }}, {{ 162, 18, 155, 59 }}, 31, mode), + MakeColors({{ 24, 148, 33, 255 }}, {{ 165, 16, 156, 255 }})); + + EXPECT_EQ(TestColors({{ 162, 18, 155, 59 }}, {{ 22, 148, 30, 59 }}, 31, mode), + MakeColors({{ 165, 16, 156, 255 }}, {{ 24, 148, 33, 255 }})); +} + +// Make sure that certain endpoint pairs result in the blue-contract path as +// we'd expect, such that we can make sure that we're hitting all of the encode +// paths. +TEST(EndpointCodecTest, RGBDirectMakesBlueContract) { + constexpr int kEndpointRange = 31; + for (const auto& endpoint_pair : kBlueContractPairs) { + ColorEndpointMode astc_mode; + std::vector<int> vals; + bool needs_swap = EncodeColorsForMode( + endpoint_pair.first, endpoint_pair.second, + kEndpointRange, EndpointEncodingMode::kDirectRGB, &astc_mode, &vals); + (void)(needs_swap); // Don't really care. + + EXPECT_TRUE(UsesBlueContract(kEndpointRange, astc_mode, vals)); + } +} + +// Make sure that encoding and decoding for the RGB base-scale mode works. +TEST(EndpointCodecTest, RGBBaseScale) { + const auto mode = EndpointEncodingMode::kBaseScaleRGB; + const auto close_to = [](RgbaColor c, int x) { + return Pointwise(IsCloseTo(x), c); + }; + + // Identical colors should be encoded with a 255 scale factor. Since ASTC + // decodes the scaled color by doing (x * s) >> 8, the decoded color will be + // multiplied by 255/256. This might cause rounding errors sometimes, so we + // check that every channel only deviates by 1. + std::mt19937 random(0xdeadbeef); + std::uniform_int_distribution<int> byte_distribution(0, 255); + + for (int i = 0; i < 100; ++i) { + RgbaColor color{{byte_distribution(random), byte_distribution(random), + byte_distribution(random), 255}}; + const auto test_result = TestColors(color, color, 255, mode); + EXPECT_THAT(test_result, Pair(close_to(color, 1), close_to(color, 1))); + } + + // Make sure that if we want to scale by e.g. 1/4 then we can do that exactly: + const RgbaColor low = {{ 20, 4, 40, 255 }}; + const RgbaColor high = {{ 80, 16, 160, 255 }}; + EXPECT_THAT(TestColors(low, high, 255, mode), + Pair(close_to(low, 0), close_to(high, 0))); + + // And if we quantize it, then we get roughly the same thing. The scale factor + // should be representable with most quantization levels. The problem is that + // if we're off on the 'high' color, then we will be off on the 'low' color. + EXPECT_THAT(TestColors(low, high, 127, mode), + Pair(close_to(low, 1), close_to(high, 1))); + + EXPECT_THAT(TestColors(low, high, 63, mode), + Pair(close_to(low, 1), close_to(high, 2))); + + EXPECT_THAT(TestColors(low, high, 31, mode), + Pair(close_to(low, 1), close_to(high, 4))); + + EXPECT_THAT(TestColors(low, high, 15, mode), + Pair(close_to(low, 2), close_to(high, 8))); +} + +// Make sure that encoding and decoding for the RGB base-offset mode works. +// Since we don't have a decoder, this is currently only a test that should work +// based on reasoning about what's written in the spec. +// TODO(krajcevski): Write an encoder. +TEST(EndpointCodecTest, RGBBaseOffset) { + const auto test_colors = [](const RgbaColor& low, const RgbaColor& high) { + const RgbaColor diff = {{ high[0] - low[0], high[1] - low[1], + high[2] - low[2], high[3] - low[3] }}; + + std::vector<int> vals; + for (int i = 0; i < 3; ++i) { + // If the base is "large", then it grabs it's most significant bit from + // the offset value. Hence, we need to save it here. + const bool is_large = low[i] >= 128; + vals.push_back((low[i] * 2) & 0xFF); + vals.push_back(diff[i] * 2); + + // Give the "large" bases their bits back. + if (is_large) { + vals.back() |= 0x80; + } + } + + RgbaColor dec_low, dec_high; + DecodeColorsForMode(vals, 255, ColorEndpointMode::kLDRRGBBaseOffset, + &dec_low, &dec_high); + + EXPECT_THAT(std::make_pair(dec_low, dec_high), Pair(Eq(low), Eq(high))); + }; + + // Test the "direct encoding" path. + test_colors({{ 80, 16, 112, 255 }}, {{ 87, 18, 132, 255 }}); + test_colors({{ 80, 74, 82, 255 }}, {{ 90, 92, 110, 255 }}); + test_colors({{ 0, 0, 0, 255 }}, {{ 2, 2, 2, 255 }}); + + // Identical endpoints should always encode exactly, provided they satisfy the + // requirements for the base encoding. + std::mt19937 random(0xdeadbeef); + std::uniform_int_distribution<int> byte_distribution(0, 255); + for (int i = 0; i < 100; ++i) { + RgbaColor color{{byte_distribution(random), byte_distribution(random), + byte_distribution(random), 255}}; + if ((color[0] | color[1] | color[2]) & 1) { + continue; + } + test_colors(color, color); + } + + // TODO(google): Test the "blue contract" path. +} + +// Make sure that we can decode colors that are given to us straight out of the +// ASTC codec. +TEST(EndpointCodecTest, DecodeCheckerboard) { + const RgbaColor kWhite {{ 255, 255, 255, 255 }}; + const RgbaColor kBlack {{ 0, 0, 0, 255 }}; + + const std::string astc = LoadASTCFile("checkerboard"); + for (int i = 0; i < astc.size(); i += 16) { + base::UInt128 block; + memcpy(&block, &astc[i], sizeof(block)); + + const auto intermediate = UnpackIntermediateBlock(PhysicalASTCBlock(block)); + ASSERT_TRUE(intermediate) << "Block is void extent???"; + + const auto block_data = &intermediate.value(); + ASSERT_THAT(block_data->endpoints, SizeIs(Eq(1))); + + const int color_range = EndpointRangeForBlock(*block_data); + const auto& endpoints = block_data->endpoints[0]; + + RgbaColor low, high; + DecodeColorsForMode(endpoints.colors, color_range, endpoints.mode, + &low, &high); + + // Expect that the endpoints are black and white, but either order. + EXPECT_THAT(std::make_pair(low, high), + AnyOf( + Pair(Eq(kWhite), Eq(kBlack)), + Pair(Eq(kBlack), Eq(kWhite)))); + } +} + +} // namespace + +} // namespace astc_codec diff --git a/src/decoder/test/footprint_test.cc b/src/decoder/test/footprint_test.cc new file mode 100644 index 0000000..6aef98a --- /dev/null +++ b/src/decoder/test/footprint_test.cc @@ -0,0 +1,97 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/footprint.h" + +#include <array> +#include <tuple> +#include <vector> + +#include <gtest/gtest.h> + +namespace astc_codec { + +namespace { + +TEST(FootprintTest, ParseAstcFootprintString) { + using ASTCTestPair = std::pair<std::string, Footprint>; + const std::array<ASTCTestPair, Footprint::NumValidFootprints()> + valid_footprints {{ + std::make_pair("4x4", Footprint::Get4x4()), + std::make_pair("5x4", Footprint::Get5x4()), + std::make_pair("5x5", Footprint::Get5x5()), + std::make_pair("6x5", Footprint::Get6x5()), + std::make_pair("6x6", Footprint::Get6x6()), + std::make_pair("8x5", Footprint::Get8x5()), + std::make_pair("8x6", Footprint::Get8x6()), + std::make_pair("8x8", Footprint::Get8x8()), + std::make_pair("10x5", Footprint::Get10x5()), + std::make_pair("10x6", Footprint::Get10x6()), + std::make_pair("10x8", Footprint::Get10x8()), + std::make_pair("10x10", Footprint::Get10x10()), + std::make_pair("12x10", Footprint::Get12x10()), + std::make_pair("12x12", Footprint::Get12x12()) + }}; + + for (const auto& test : valid_footprints) { + base::Optional<Footprint> footprint = Footprint::Parse(test.first.c_str()); + EXPECT_TRUE(footprint); + EXPECT_EQ(test.second, footprint.value()); + } + + EXPECT_DEBUG_DEATH(EXPECT_FALSE(Footprint::Parse("")), ""); + EXPECT_DEBUG_DEATH(EXPECT_FALSE(Footprint::Parse("3")), ""); + EXPECT_DEBUG_DEATH(EXPECT_FALSE(Footprint::Parse("x")), ""); + // Validly formed but out-of-bounds dimensions do not assert, otherwise + // malformed ASTC files could crash the decoder in debug builds. + EXPECT_FALSE(Footprint::Parse("9999999999x10")); + EXPECT_DEBUG_DEATH(EXPECT_FALSE(Footprint::Parse("ax8")), ""); + EXPECT_DEBUG_DEATH(EXPECT_FALSE(Footprint::Parse("2x3x4")), ""); + EXPECT_DEBUG_DEATH(EXPECT_FALSE(Footprint::Parse("-3x4")), ""); + EXPECT_FALSE(Footprint::Parse("10x4")); +} + +TEST(FootprintTest, Bitrates) { + EXPECT_NEAR(Footprint::Get4x4().Bitrate(), 8.f, 0.01f); + EXPECT_NEAR(Footprint::Get5x4().Bitrate(), 6.4f, 0.01f); + EXPECT_NEAR(Footprint::Get5x5().Bitrate(), 5.12f, 0.01f); + EXPECT_NEAR(Footprint::Get6x5().Bitrate(), 4.27f, 0.01f); + EXPECT_NEAR(Footprint::Get6x6().Bitrate(), 3.56f, 0.01f); + EXPECT_NEAR(Footprint::Get8x5().Bitrate(), 3.20f, 0.01f); + EXPECT_NEAR(Footprint::Get8x6().Bitrate(), 2.67f, 0.01f); + EXPECT_NEAR(Footprint::Get8x8().Bitrate(), 2.00f, 0.01f); + EXPECT_NEAR(Footprint::Get10x5().Bitrate(), 2.56f, 0.01f); + EXPECT_NEAR(Footprint::Get10x6().Bitrate(), 2.13f, 0.01f); + EXPECT_NEAR(Footprint::Get10x8().Bitrate(), 1.60f, 0.01f); + EXPECT_NEAR(Footprint::Get10x10().Bitrate(), 1.28f, 0.01f); + EXPECT_NEAR(Footprint::Get12x10().Bitrate(), 1.07f, 0.01f); + EXPECT_NEAR(Footprint::Get12x12().Bitrate(), 0.89f, 0.01f); +} + +TEST(FootprintTest, StorageRequirements) { + auto footprint = Footprint::Get10x8(); + EXPECT_EQ(footprint.Width(), 10); + EXPECT_EQ(footprint.Height(), 8); + + // If we have 8x8 blocks, then we have 64*16 = 1024 bytes. + EXPECT_EQ(footprint.StorageRequirements(80, 64), 1024); + + // If our block is a little smaller this still counts because we need to + // cover a partial block with a fully encoded one. + EXPECT_EQ(footprint.StorageRequirements(79, 63), 1024); +} + +} // namespace + +} // namespace astc_codec diff --git a/src/decoder/test/image_utils.h b/src/decoder/test/image_utils.h new file mode 100644 index 0000000..718696e --- /dev/null +++ b/src/decoder/test/image_utils.h @@ -0,0 +1,217 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <gtest/gtest.h> + +#include <fstream> +#include <vector> + +static constexpr size_t kMaxVectorOutput = 128; + +class ImageBuffer { + public: + static constexpr size_t Align = 4; + + void Allocate(size_t width, size_t height, size_t bytes_per_pixel) { + width_ = width; + height_ = height; + bytes_per_pixel_ = bytes_per_pixel; + stride_ = AlignBytes(width * bytes_per_pixel); + data_.resize(stride_ * height); + } + + uint8_t* operator()(size_t x, size_t y) { + assert(x < width_ && y < height_); + return &data_[y * Stride() + x * bytes_per_pixel_]; + } + + size_t Stride() const { return stride_; } + size_t BytesPerPixel() const { return bytes_per_pixel_; } + + std::vector<uint8_t>& Data() { return data_; } + const std::vector<uint8_t>& Data() const { return data_; } + size_t DataSize() const { return data_.size(); } + + private: + size_t AlignBytes(size_t bytes) const { + return (bytes + (Align - 1)) / Align * Align; + } + + size_t width_ = 0; + size_t height_ = 0; + size_t stride_ = 0; + size_t bytes_per_pixel_ = 0; + std::vector<uint8_t> data_; +}; + +namespace std { +static void PrintTo(const vector<uint8_t>& vec, ostream* os) { + ios::fmtflags origFlags(os->flags()); + + *os << '{'; + size_t count = 0; + for (vector<uint8_t>::const_iterator it = vec.begin(); it != vec.end(); + ++it, ++count) { + if (count > 0) { + *os << ", "; + } + + if (count == kMaxVectorOutput) { + *os << "... "; + break; + } + + if ((count % 16) == 0) { + *os << "\n"; + } + + if (*it == 0) { + *os << " "; + } else { + *os << "0x" << std::hex << std::uppercase << std::setw(2) + << std::setfill('0') << int(*it) << std::dec; + } + } + + *os << '}'; + + os->flags(origFlags); +} +} // namespace std + +static std::string LoadFile(const std::string& path) { + std::ifstream is(path, std::ios::binary); + EXPECT_TRUE(is) << "Failed to load file " << path; + if (!is) { + return ""; + } + + std::ostringstream ss; + ss << is.rdbuf(); + return ss.str(); +} + +static std::string LoadASTCFile(const std::string& basename) { + const std::string filename = + std::string("src/decoder/testdata/") + basename + ".astc"; + + std::string result = LoadFile(filename); + // Don't parse the header here, we already know what kind of astc encoding it + // is. + if (result.size() < 16) { + return ""; + } else { + return result.substr(16); + } +} + +void LoadGoldenBmp(const std::string& path, ImageBuffer* result) { + constexpr size_t kBmpHeaderSize = 54; + + SCOPED_TRACE(testing::Message() << "LoadGoldenBmp " << path); + + const std::string data = LoadFile(path); + ASSERT_FALSE(data.empty()) << "Failed to open golden image: " << path; + + ASSERT_GE(data.size(), kBmpHeaderSize); + ASSERT_EQ('B', data[0]); + ASSERT_EQ('M', data[1]); + + uint32_t dataPos = *reinterpret_cast<const uint32_t*>(&data[0x0A]); + uint32_t imageSize = *reinterpret_cast<const uint32_t*>(&data[0x22]); + const uint16_t bitsPerPixel = *reinterpret_cast<const uint16_t*>(&data[0x1C]); + int width = *reinterpret_cast<const int*>(&data[0x12]); + int height = *reinterpret_cast<const int*>(&data[0x16]); + + SCOPED_TRACE(testing::Message() + << "dataPos=" << dataPos << ", imageSize=" << imageSize + << ", bitsPerPixel=" << bitsPerPixel << ", width=" << width + << ", height=" << height); + + if (height < 0) { + height = -height; + } + + if (imageSize == 0) { + imageSize = width * height * 3; + } + + if (dataPos < kBmpHeaderSize) { + dataPos = kBmpHeaderSize; + } + + ASSERT_TRUE(bitsPerPixel == 24 || bitsPerPixel == 32) + << "BMP bits per pixel mismatch, expected 24 or 32"; + + result->Allocate(width, height, bitsPerPixel == 24 ? 3 : 4); + ASSERT_LE(imageSize, result->DataSize()); + + std::vector<uint8_t>& resultData = result->Data(); + const size_t stride = result->Stride(); + + // Copy the data row-by-row to make sure that stride is right. + for (size_t row = 0; row < static_cast<size_t>(height); ++row) { + memcpy(&resultData[row * stride], &data[dataPos + row * stride], + width * bitsPerPixel / 8); + } + + if (bitsPerPixel == 32) { + // Swizzle the data from ABGR to ARGB. + for (size_t row = 0; row < static_cast<size_t>(height); ++row) { + uint8_t* rowData = resultData.data() + row * stride; + + for (size_t i = 3; i < stride; i += 4) { + const uint8_t b = rowData[i - 3]; + rowData[i - 3] = rowData[i - 1]; + rowData[i - 1] = b; + } + } + } else { + // Swizzle the data from BGR to RGB. + for (size_t row = 0; row < static_cast<size_t>(height); ++row) { + uint8_t* rowData = resultData.data() + row * stride; + + for (size_t i = 2; i < stride; i += 3) { + const uint8_t tmp = rowData[i - 2]; + rowData[i - 2] = rowData[i]; + rowData[i] = tmp; + } + } + } +} + +static void CompareSumOfSquaredDifferences(const ImageBuffer& golden, + const ImageBuffer& image, + double threshold) { + ASSERT_EQ(golden.DataSize(), image.DataSize()); + ASSERT_EQ(golden.Stride(), image.Stride()); + ASSERT_EQ(golden.BytesPerPixel(), image.BytesPerPixel()); + + const std::vector<uint8_t>& image_data = image.Data(); + const std::vector<uint8_t>& golden_data = golden.Data(); + + double sum = 0.0; + for (size_t i = 0; i < image_data.size(); ++i) { + const double diff = static_cast<double>(image_data[i]) - golden_data[i]; + sum += diff * diff; + } + + EXPECT_LE(sum, threshold * image_data.size()) + << "Per pixel " << (sum / image_data.size()) + << ", expected <= " << threshold; + if (sum > threshold * image_data.size()) { + // Fall back to comparison which will dump first chunk of vector. + EXPECT_EQ(golden_data, image_data); + } +} diff --git a/src/decoder/test/integer_sequence_codec_test.cc b/src/decoder/test/integer_sequence_codec_test.cc new file mode 100644 index 0000000..b66ff2b --- /dev/null +++ b/src/decoder/test/integer_sequence_codec_test.cc @@ -0,0 +1,337 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/integer_sequence_codec.h" +#include "src/base/uint128.h" + +#include <random> +#include <string> +#include <vector> + +#include <gtest/gtest.h> + +using astc_codec::base::UInt128; +using astc_codec::base::BitStream; +using astc_codec::IntegerSequenceCodec; +using astc_codec::IntegerSequenceEncoder; +using astc_codec::IntegerSequenceDecoder; + +namespace { + +// Make sure that the counts returned for a specific range match what's +// expected. In particular, make sure that it fits with Table C.2.7 +TEST(ASTCIntegerSequenceCodecTest, TestGetCountsForRange) { + std::array<int, 3> kExpectedCounts[31] = { + {{ 0, 0, 1 }}, // 1 + {{ 1, 0, 0 }}, // 2 + {{ 0, 0, 2 }}, // 3 + {{ 0, 1, 0 }}, // 4 + {{ 1, 0, 1 }}, // 5 + {{ 0, 0, 3 }}, // 6 + {{ 0, 0, 3 }}, // 7 + {{ 0, 1, 1 }}, // 8 + {{ 0, 1, 1 }}, // 9 + {{ 1, 0, 2 }}, // 10 + {{ 1, 0, 2 }}, // 11 + {{ 0, 0, 4 }}, // 12 + {{ 0, 0, 4 }}, // 13 + {{ 0, 0, 4 }}, // 14 + {{ 0, 0, 4 }}, // 15 + {{ 0, 1, 2 }}, // 16 + {{ 0, 1, 2 }}, // 17 + {{ 0, 1, 2 }}, // 18 + {{ 0, 1, 2 }}, // 19 + {{ 1, 0, 3 }}, // 20 + {{ 1, 0, 3 }}, // 21 + {{ 1, 0, 3 }}, // 22 + {{ 1, 0, 3 }}, // 23 + {{ 0, 0, 5 }}, // 24 + {{ 0, 0, 5 }}, // 25 + {{ 0, 0, 5 }}, // 26 + {{ 0, 0, 5 }}, // 27 + {{ 0, 0, 5 }}, // 28 + {{ 0, 0, 5 }}, // 29 + {{ 0, 0, 5 }}, // 30 + {{ 0, 0, 5 }}, // 31 + }; + + int t, q, b; + for (int i = 1; i < 32; ++i) { + IntegerSequenceCodec::GetCountsForRange(i, &t, &q, &b); + EXPECT_EQ(t, kExpectedCounts[i - 1][0]); + EXPECT_EQ(q, kExpectedCounts[i - 1][1]); + EXPECT_EQ(b, kExpectedCounts[i - 1][2]); + } + + ASSERT_DEBUG_DEATH(IntegerSequenceCodec::GetCountsForRange(0, &t, &q, &b), ""); + ASSERT_DEBUG_DEATH( + IntegerSequenceCodec::GetCountsForRange(256, &t, &q, &b), ""); + + IntegerSequenceCodec::GetCountsForRange(1, &t, &q, &b); + EXPECT_EQ(t, 0); + EXPECT_EQ(q, 0); + EXPECT_EQ(b, 1); +} + +// Test to make sure that we're calculating the number of bits needed to +// encode a given number of values based on the range of the values. +TEST(ASTCIntegerSequenceCodecTest, TestNumBitsForCounts) { + int trits = 0; + int quints = 0; + int bits = 0; + + // A range of one should have single bits, so n 1-bit values should be n bits. + trits = 0; + quints = 0; + bits = 1; + for (int i = 0; i < 64; ++i) { + EXPECT_EQ(IntegerSequenceCodec::GetBitCount(i, trits, quints, bits), i); + EXPECT_EQ(IntegerSequenceCodec::GetBitCountForRange(i, 1), i); + } + + // Similarly, N two-bit values should be 2n bits... + trits = 0; + quints = 0; + bits = 2; + for (int i = 0; i < 64; ++i) { + int bit_counts = IntegerSequenceCodec::GetBitCount(i, trits, quints, bits); + EXPECT_EQ(bit_counts, 2 * i); + EXPECT_EQ(IntegerSequenceCodec::GetBitCountForRange(i, 3), 2 * i); + } + + // Trits are a bit more complicated -- there are five trits in a block, so + // if we encode 15 values with 3 bits each in trits, we'd get three blocks, + // each with eight bits of trits. + trits = 1; + quints = 0; + bits = 3; + EXPECT_EQ(IntegerSequenceCodec::GetBitCount(15, trits, quints, bits), + 8 * 3 + 15 * 3); + EXPECT_EQ(IntegerSequenceCodec::GetBitCountForRange(15, 23), + IntegerSequenceCodec::GetBitCount(15, trits, quints, bits)); + + // However, if instead we encode 13 values, we don't need to use the remaining + // two values, so we only need bits as they will be encoded. As it turns out, + // this means we can avoid three bits in the final block (one for the high + // order trit encoding and two for one of the values), resulting in 47 bits. + trits = 1; + quints = 0; + bits = 2; + EXPECT_EQ(IntegerSequenceCodec::GetBitCount(13, trits, quints, bits), 47); + EXPECT_EQ(IntegerSequenceCodec::GetBitCountForRange(13, 11), + IntegerSequenceCodec::GetBitCount(13, trits, quints, bits)); + + // Quints have a similar property -- if we encode six values using a quint and + // four bits, then we have two quint blocks each with three values and a seven + // bit encoded quint triplet... + trits = 0; + quints = 1; + bits = 4; + EXPECT_EQ(IntegerSequenceCodec::GetBitCount(6, trits, quints, bits), + 7 * 2 + 6 * 4); + EXPECT_EQ(IntegerSequenceCodec::GetBitCountForRange(6, 79), + IntegerSequenceCodec::GetBitCount(6, trits, quints, bits)); + + // If we have fewer values than blocks we can again avoid about 2 + nbits + // bits... + trits = 0; + quints = 1; + bits = 3; + EXPECT_EQ(IntegerSequenceCodec::GetBitCount(7, trits, quints, bits), + /* first two quint blocks */ 7 * 2 + + /* first two blocks of bits */ 6 * 3 + + /* last quint block without the high order four bits */ 3 + + /* last block with one set of three bits */ 3); +} + +// Tests that the encoder knows how to encode values of the form 5*2^k. +TEST(ASTCIntegerSequenceCodecTest, TestQuintCodec) { + // In this case, k = 4 + + // Setup bit src/sink + BitStream<UInt128> bit_sink; + + const int kValueRange = 79; + IntegerSequenceEncoder enc(kValueRange); + enc.AddValue(3); + enc.AddValue(79); + enc.AddValue(37); + enc.Encode(&bit_sink); + + // quint: 1000101 m0: 0011 m1: 1111 m2: 0101 + // 100 0100 0111 1101 0010 + // interleaved 10m200m1101m0 + // should be 100 1010 0111 1101 0011 = 0x4A7D3 + EXPECT_EQ(bit_sink.Bits(), 19); + + uint64_t encoded = 0; + bit_sink.GetBits(19, &encoded); + EXPECT_EQ(encoded, 0x4A7D3); + + // Now check that decoding it works as well + BitStream<UInt128> bit_src(encoded, 19); + + IntegerSequenceDecoder dec(kValueRange); + auto decoded_vals = dec.Decode(3, &bit_src); + ASSERT_EQ(decoded_vals.size(), 3); + EXPECT_EQ(decoded_vals[0], 3); + EXPECT_EQ(decoded_vals[1], 79); + EXPECT_EQ(decoded_vals[2], 37); +} + +// Tests that the encoder knows how to encode values of the form 3*2^k. +TEST(ASTCIntegerSequenceCodecTest, TestTritCodec) { + uint64_t encoded = 0; + + // Setup bit src/sink + BitStream<UInt128> bit_sink(encoded, 0); + + const int kValueRange = 11; + IntegerSequenceEncoder enc(kValueRange); + enc.AddValue(7); + enc.AddValue(5); + enc.AddValue(3); + enc.AddValue(6); + enc.AddValue(10); + enc.Encode(&bit_sink); + + EXPECT_EQ(bit_sink.Bits(), 18); + + bit_sink.GetBits(18, &encoded); + EXPECT_EQ(encoded, 0x37357); + + // Now check that decoding it works as well + BitStream<UInt128> bit_src(encoded, 19); + + IntegerSequenceDecoder dec(kValueRange); + auto decoded_vals = dec.Decode(5, &bit_src); + ASSERT_EQ(decoded_vals.size(), 5); + EXPECT_EQ(decoded_vals[0], 7); + EXPECT_EQ(decoded_vals[1], 5); + EXPECT_EQ(decoded_vals[2], 3); + EXPECT_EQ(decoded_vals[3], 6); + EXPECT_EQ(decoded_vals[4], 10); +} + +// Test a specific quint encoding/decoding. This test makes sure that the way we +// encode and decode integer sequences matches what we should expect out of the +// reference ASTC encoder. +TEST(ASTCIntegerSequenceCodecTest, TestDecodeThenEncode) { + std::vector<int> vals = {{ 16, 18, 17, 4, 7, 14, 10, 0 }}; + const uint64_t kValEncoding = 0x2b9c83dc; + + BitStream<UInt128> bit_src(kValEncoding, 64); + IntegerSequenceDecoder dec(19); + auto decoded_vals = dec.Decode(8, &bit_src); + ASSERT_EQ(decoded_vals.size(), vals.size()); + for (size_t i = 0; i < decoded_vals.size(); ++i) { + EXPECT_EQ(decoded_vals[i], vals[i]); + } + + // Setup bit src/sink + BitStream<UInt128> bit_sink; + IntegerSequenceEncoder enc(19); + for (const auto& v : vals) { + enc.AddValue(v); + } + enc.Encode(&bit_sink); + EXPECT_EQ(bit_sink.Bits(), 35); + + uint64_t encoded = 0; + EXPECT_TRUE(bit_sink.GetBits(35, &encoded)); + EXPECT_EQ(encoded, kValEncoding) + << std::hex << encoded << " -- " << kValEncoding; +} + +// Same as the previous test, except it uses a trit encoding rather than a +// quint encoding. +TEST(ASTCIntegerSequenceCodecTest, TestDecodeThenEncodeTrits) { + std::vector<int> vals = {{ 6, 0, 0, 2, 0, 0, 0, 0, 8, 0, 0, 0, 0, 8, 8, 0 }}; + const uint64_t kValEncoding = 0x0004c0100001006ULL; + + BitStream<UInt128> bit_src(kValEncoding, 64); + IntegerSequenceDecoder dec(11); + auto decoded_vals = dec.Decode(vals.size(), &bit_src); + ASSERT_EQ(decoded_vals.size(), vals.size()); + for (size_t i = 0; i < decoded_vals.size(); ++i) { + EXPECT_EQ(decoded_vals[i], vals[i]); + } + + // Setup bit src/sink + BitStream<UInt128> bit_sink; + IntegerSequenceEncoder enc(11); + for (const auto& v : vals) { + enc.AddValue(v); + } + enc.Encode(&bit_sink); + EXPECT_EQ(bit_sink.Bits(), 58); + + uint64_t encoded = 0; + EXPECT_TRUE(bit_sink.GetBits(58, &encoded)); + EXPECT_EQ(encoded, kValEncoding) + << std::hex << encoded << " -- " << kValEncoding; +} + +// Generate a random sequence of integer codings with different ranges to test +// the reciprocability of our codec (encoded sequences should be able to +// decoded) +TEST(ASTCIntegerSequenceCodecTest, TestRandomReciprocation) { + std::mt19937 mt(0xbad7357); + std::uniform_int_distribution<int> rand(0, 255); + + for (int test = 0; test < 1600; ++test) { + // Generate a random number of values and a random range + int num_vals = 4 + rand(mt) % 44; // Up to 48 weights in a grid + int range = 1 + rand(mt) % 63; + + // If this produces a bit pattern larger than our buffer, then ignore + // it... we already know what our bounds are for the integer sequences + int num_bits = IntegerSequenceCodec::GetBitCountForRange(num_vals, range); + if (num_bits >= 64) { + continue; + } + + std::vector<int> generated_vals(num_vals); + for (auto& val : generated_vals) { + val = rand(mt) % (range + 1); + } + + // Encode the values using the + BitStream<UInt128> bit_sink; + + // Add them to the encoder + IntegerSequenceEncoder enc(range); + for (int v : generated_vals) { + enc.AddValue(v); + } + enc.Encode(&bit_sink); + + uint64_t encoded = 0; + bit_sink.GetBits(bit_sink.Bits(), &encoded); + ASSERT_GE(encoded, 0); + EXPECT_LT(encoded, 1ULL << num_bits); + + BitStream<UInt128> bit_src(encoded, 64); + + IntegerSequenceDecoder dec(range); + auto decoded_vals = dec.Decode(num_vals, &bit_src); + + ASSERT_EQ(decoded_vals.size(), generated_vals.size()); + for (size_t i = 0; i < decoded_vals.size(); ++i) { + EXPECT_EQ(decoded_vals[i], generated_vals[i]); + } + } +} + +} // namespace diff --git a/src/decoder/test/intermediate_astc_block_test.cc b/src/decoder/test/intermediate_astc_block_test.cc new file mode 100644 index 0000000..69935ef --- /dev/null +++ b/src/decoder/test/intermediate_astc_block_test.cc @@ -0,0 +1,453 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/intermediate_astc_block.h" +#include "src/decoder/test/image_utils.h" + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include <string> + +namespace astc_codec { + +namespace { + +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::Optional; +using ::testing::SizeIs; +using ::testing::TestWithParam; +using ::testing::ValuesIn; + +// Test to make sure that unpacking an error block returns false. +TEST(IntermediateASTCBlockTest, TestUnpackError) { + const PhysicalASTCBlock kErrorBlock(base::UInt128(0)); + EXPECT_FALSE(UnpackVoidExtent(kErrorBlock)); + EXPECT_FALSE(UnpackIntermediateBlock(kErrorBlock)); +} + +// Test to make sure that if we don't populate our weight data in the +// intermediate block than the resulting color range should error due to the +// mismatch. +TEST(IntermediateASTCBlockTest, TestEndpointRangeErrorOnNotSettingWeights) { + IntermediateBlockData data; + data.weight_range = 15; + for (auto& ep : data.endpoints) { + ep.mode = ColorEndpointMode::kLDRRGBDirect; + } + data.weight_grid_dim_x = 6; + data.weight_grid_dim_y = 6; + EXPECT_EQ(-1, EndpointRangeForBlock(data)); + + base::UInt128 dummy; + auto err_str = Pack(data, &dummy); + EXPECT_TRUE(err_str.hasValue()); + EXPECT_THAT(err_str.value(), HasSubstr("Incorrect number of weights")); +} + +// Test to make sure that if we run out of bits, then we should say so. +TEST(IntermediateASTCBlockTest, TestEndpointRangeErrorOnNotEnoughBits) { + IntermediateBlockData data; + data.weight_range = 1; + data.partition_id = 0; + data.endpoints.resize(3); + for (auto& ep : data.endpoints) { + ep.mode = ColorEndpointMode::kLDRRGBDirect; + } + data.weight_grid_dim_x = 8; + data.weight_grid_dim_y = 8; + EXPECT_EQ(-2, EndpointRangeForBlock(data)); + + // Resize the weights to get past the error that they do not match the grid + // dimensions. + data.weights.resize(64); + + base::UInt128 dummy; + auto err_str = Pack(data, &dummy); + EXPECT_TRUE(err_str.hasValue()); + EXPECT_THAT(err_str.value(), HasSubstr("illegal color range")); +} + +// Test to make sure that as we increase the number of weights, we decrease the +// allowable range of colors +TEST(IntermediateASTCBlockTest, TestEndpointRangeForBlock) { + IntermediateBlockData data; + data.weight_range = 2; + data.endpoints.resize(2); + data.dual_plane_channel.clear(); + for (auto& ep : data.endpoints) { + ep.mode = ColorEndpointMode::kLDRRGBDirect; + } + + // Weight params control how many weights are present in a block + struct WeightParams { + int width; + int height; + + // We should sort based on number of weights for these params + int NumWeights() const { return width * height; } + bool operator<(const WeightParams& other) const { + return NumWeights() < other.NumWeights(); + } + }; + + std::vector<WeightParams> weight_params; + for (int y = 2; y < 8; ++y) { + for (int x = 2; x < 8; ++x) { + weight_params.emplace_back(WeightParams{x, y}); + } + } + + // Sort weights from fewest to largest such that the allowable color range + // should be monotonically decreasing + std::sort(weight_params.begin(), weight_params.end()); + + // Keep track of the largest available color range and measure that it + // decreases as we add more weights to our block + int last_color_range = 255; + for (const auto& params : weight_params) { + data.weight_grid_dim_x = params.width; + data.weight_grid_dim_y = params.height; + + const int color_range = EndpointRangeForBlock(data); + EXPECT_LE(color_range, last_color_range); + last_color_range = std::min(color_range, last_color_range); + } + + // Make sure that we actually changed it at some point. + EXPECT_LT(last_color_range, 255); +} + +// Test to make sure that unpacking an legitimate ASTC block returns the encoded +// values that we expect. +TEST(IntermediateASTCBlockTest, TestUnpackNonVoidExtentBlock) { + PhysicalASTCBlock blk(0x0000000001FE000173ULL); + auto b = UnpackIntermediateBlock(blk); + ASSERT_TRUE(b); + + const auto& data = b.value(); + EXPECT_EQ(data.weight_grid_dim_x, 6); + EXPECT_EQ(data.weight_grid_dim_y, 5); + EXPECT_EQ(data.weight_range, 7); + + EXPECT_FALSE(data.partition_id); + EXPECT_FALSE(data.dual_plane_channel); + + ASSERT_EQ(data.weights.size(), 30); + for (auto weight : data.weights) { + EXPECT_EQ(weight, 0); + } + + ASSERT_EQ(data.endpoints.size(), 1); + for (const auto& ep_data : data.endpoints) { + EXPECT_EQ(ep_data.mode, ColorEndpointMode::kLDRLumaDirect); + ASSERT_EQ(ep_data.colors.size(), 2); + EXPECT_EQ(ep_data.colors[0], 0); + EXPECT_EQ(ep_data.colors[1], 255); + } +} + +// Make sure that we can pack blocks that aren't void extent blocks. (In other +// words, can we actually deal with intermediate ASTC data). +TEST(IntermediateASTCBlockTest, TestPackNonVoidExtentBlock) { + IntermediateBlockData data; + + data.weight_grid_dim_x = 6; + data.weight_grid_dim_y = 5; + data.weight_range = 7; + + data.partition_id = {}; + data.dual_plane_channel = {}; + + data.weights.resize(30); + for (auto& weight : data.weights) { + weight = 0; + } + + data.endpoints.resize(1); + for (auto& ep_data : data.endpoints) { + ep_data.mode = ColorEndpointMode::kLDRLumaDirect; + ep_data.colors.resize(2); + ep_data.colors[0] = 0; + ep_data.colors[1] = 255; + } + + base::UInt128 packed; + auto error_str = Pack(data, &packed); + ASSERT_FALSE(error_str) << (error_str ? error_str.value() : std::string("")); + EXPECT_EQ(packed, 0x0000000001FE000173ULL); +} + +// Make sure that we can unpack void extent blocks +TEST(IntermediateASTCBlockTest, TestUnpackVoidExtentBlock) { + PhysicalASTCBlock void_extent_block(0xFFFFFFFFFFFFFDFCULL); + + auto b = UnpackVoidExtent(void_extent_block); + ASSERT_TRUE(b); + + const auto& data = b.value(); + EXPECT_EQ(data.r, 0); + EXPECT_EQ(data.g, 0); + EXPECT_EQ(data.b, 0); + EXPECT_EQ(data.a, 0); + for (const auto& coord : data.coords) { + EXPECT_EQ(coord, (1 << 13) - 1); + } + + base::UInt128 more_interesting(0xdeadbeefdeadbeefULL, 0xFFF8003FFE000DFCULL); + b = UnpackVoidExtent(PhysicalASTCBlock(more_interesting)); + ASSERT_TRUE(b); + + const auto& other_data = b.value(); + EXPECT_EQ(other_data.r, 0xbeef); + EXPECT_EQ(other_data.g, 0xdead); + EXPECT_EQ(other_data.b, 0xbeef); + EXPECT_EQ(other_data.a, 0xdead); + EXPECT_EQ(other_data.coords[0], 0); + EXPECT_EQ(other_data.coords[1], 8191); + EXPECT_EQ(other_data.coords[2], 0); + EXPECT_EQ(other_data.coords[3], 8191); +} + +// Make sure that we can pack void extent blocks and void extent data. +TEST(IntermediateASTCBlockTest, TestPackVoidExtentBlock) { + VoidExtentData data; + data.r = 0; + data.g = 0; + data.b = 0; + data.a = 0; + for (auto& coord : data.coords) { + coord = (1 << 13) - 1; + } + + base::UInt128 packed; + auto error_str = Pack(data, &packed); + ASSERT_FALSE(error_str) << (error_str ? error_str.value() : std::string("")); + EXPECT_EQ(packed, 0xFFFFFFFFFFFFFDFCULL); + + data.r = 0xbeef; + data.g = 0xdead; + data.b = 0xbeef; + data.a = 0xdead; + data.coords[0] = 0; + data.coords[1] = 8191; + data.coords[2] = 0; + data.coords[3] = 8191; + + error_str = Pack(data, &packed); + ASSERT_FALSE(error_str) << (error_str ? error_str.value() : std::string("")); + EXPECT_EQ(packed, + base::UInt128(0xdeadbeefdeadbeefULL, 0xFFF8003FFE000DFCULL)); +} + +// Make sure that the color endpoint mode is properly repacked. This test case +// was created as a bug during testing. +TEST(IntermediateASTCBlockTest, TestPackUnpackWithSameCEM) { + base::UInt128 orig(0xe8e8eaea20000980ULL, 0x20000200cb73f045ULL); + + auto b = UnpackIntermediateBlock(PhysicalASTCBlock(orig)); + ASSERT_TRUE(b); + + base::UInt128 repacked; + auto err_str = Pack(b.value(), &repacked); + ASSERT_FALSE(err_str) << (err_str ? err_str.value() : std::string("")); + + EXPECT_EQ(repacked, orig); + + // Test case #2 + orig = base::UInt128(0x3300c30700cb01c5ULL, 0x0573907b8c0f6879ULL); + b = UnpackIntermediateBlock(PhysicalASTCBlock(orig)); + ASSERT_TRUE(b); + + err_str = Pack(b.value(), &repacked); + ASSERT_FALSE(err_str) << (err_str ? err_str.value() : std::string("")); + EXPECT_EQ(repacked, orig); +} + +// Test that we can encode/decode a block that uses a very large gap +// between weight and endpoint data. +TEST(IntermediateASTCBlockTest, TestPackingWithLargeGap) { + // We can construct this block by doing the following: + // -- choose a block mode that only gives 24 weight bits + // -- choose the smallest endpoint mode: grayscale direct + // -- make sure there are no partitions + const base::UInt128 orig(0xBEDEAD0000000000ULL, 0x0000000001FE032EULL); + const auto b = UnpackIntermediateBlock(PhysicalASTCBlock(orig)); + ASSERT_TRUE(b); + + const auto& data = b.value(); + EXPECT_EQ(data.weight_grid_dim_x, 2); + EXPECT_EQ(data.weight_grid_dim_y, 3); + EXPECT_EQ(data.weight_range, 15); + + EXPECT_FALSE(data.partition_id); + EXPECT_FALSE(data.dual_plane_channel); + + ASSERT_EQ(data.endpoints.size(), 1); + EXPECT_EQ(data.endpoints.at(0).mode, ColorEndpointMode::kLDRLumaDirect); + + ASSERT_EQ(data.endpoints.at(0).colors.size(), 2); + EXPECT_EQ(data.endpoints.at(0).colors.at(0), 255); + EXPECT_EQ(data.endpoints.at(0).colors.at(1), 0); + + // Now encode it again + base::UInt128 repacked; + const auto err_str = Pack(b.value(), &repacked); + EXPECT_EQ(orig, repacked) << (err_str ? err_str.value() : std::string("")); +} + +// Take a block that is encoded using direct luma with full byte values and see +// if we properly set the endpoint range. +TEST(IntermediateASTCBlockTest, TestEndpointRange) { + PhysicalASTCBlock blk(0x0000000001FE000173ULL); + EXPECT_THAT(blk.ColorValuesRange(), Optional(Eq(255))); + + auto b = UnpackIntermediateBlock(blk); + ASSERT_TRUE(b); + + const auto& data = b.value(); + ASSERT_THAT(data.endpoints, SizeIs(1)); + EXPECT_THAT(data.endpoints[0].mode, Eq(ColorEndpointMode::kLDRLumaDirect)); + EXPECT_THAT(data.endpoints[0].colors, ElementsAre(0, 255)); + EXPECT_THAT(data.endpoint_range, Optional(Eq(255))); +} + +struct ImageTestParams { + std::string image_name; + int checkered_dim; +}; + +static void PrintTo(const ImageTestParams& params, std::ostream* os) { + *os << "ImageTestParams(" << params.image_name << ")"; +} + +class IntermediateASTCBlockTest : public TestWithParam<ImageTestParams> { }; + +// Test whether or not a real-world ASTC implementation can be unpacked and +// then repacked into the same implementation. In conjunction with the other +// tests, we make sure that we can recreate ASTC blocks that we have previously +// unpacked. +TEST_P(IntermediateASTCBlockTest, TestPackUnpack) { + const auto& params = GetParam(); + const int astc_dim = 8; + const int img_dim = params.checkered_dim * astc_dim; + const std::string astc = LoadASTCFile(params.image_name); + + // Make sure that unpacking and repacking all of the blocks works... + const int kNumASTCBlocks = (img_dim / astc_dim) * (img_dim / astc_dim); + for (int i = 0; i < kNumASTCBlocks; ++i) { + base::UInt128 block_bits; + memcpy(&block_bits, astc.data() + PhysicalASTCBlock::kSizeInBytes * i, + PhysicalASTCBlock::kSizeInBytes); + + const PhysicalASTCBlock block(block_bits); + + base::UInt128 repacked; + if (block.IsVoidExtent()) { + auto b = UnpackVoidExtent(block); + ASSERT_TRUE(b); + + auto err_str = Pack(b.value(), &repacked); + ASSERT_FALSE(err_str) << (err_str ? err_str.value() : std::string("")); + } else { + auto b = UnpackIntermediateBlock(block); + ASSERT_TRUE(b); + + // Check to see that we properly set the endpoint range when we decoded + // the block. + auto& block_data = b.value(); + EXPECT_EQ(block_data.endpoint_range, block.ColorValuesRange()); + + // Reset the endpoint range here to see if we correctly reconstruct it + // below + block_data.endpoint_range = {}; + + auto err_str = Pack(b.value(), &repacked); + ASSERT_FALSE(err_str) << (err_str ? err_str.value() : std::string("")); + } + + // You would expect the following line to be enough: + // EXPECT_EQ(repacked, block.GetBlockBits()) + // ... except that the ASTC encoder makes some interesting decisions + // about how to encode the same logical bits. One example is that + // sometimes if all partitions share an endpoint mode, the encoded + // block will not use the shared CEM mode, and rather list each + // partition's mode explicitly. For that reason, we just need to make as + // close of an approximation as possible that we decode to the same + // physical values. + + PhysicalASTCBlock pb(repacked); + ASSERT_FALSE(pb.IsIllegalEncoding()); + + base::UInt128 pb_color_mask = + (base::UInt128(1) << pb.NumColorBits().value()) - 1; + base::UInt128 pb_color_bits = + pb.GetBlockBits() >> pb.ColorStartBit().value(); + pb_color_bits &= pb_color_mask; + + base::UInt128 b_color_mask = + (base::UInt128(1) << pb.NumColorBits().value()) - 1; + base::UInt128 b_color_bits = + block.GetBlockBits() >> block.ColorStartBit().value(); + b_color_bits &= b_color_mask; + + EXPECT_EQ(pb_color_mask, b_color_mask); + EXPECT_EQ(pb_color_bits, b_color_bits); + + EXPECT_EQ(pb.IsVoidExtent(), block.IsVoidExtent()); + EXPECT_EQ(pb.VoidExtentCoords(), block.VoidExtentCoords()); + + EXPECT_EQ(pb.WeightGridDims(), block.WeightGridDims()); + EXPECT_EQ(pb.WeightRange(), block.WeightRange()); + EXPECT_EQ(pb.NumWeightBits(), block.NumWeightBits()); + EXPECT_EQ(pb.WeightStartBit(), block.WeightStartBit()); + + EXPECT_EQ(pb.IsDualPlane(), block.IsDualPlane()); + EXPECT_EQ(pb.DualPlaneChannel(), block.DualPlaneChannel()); + + EXPECT_EQ(pb.NumPartitions(), block.NumPartitions()); + EXPECT_EQ(pb.PartitionID(), block.PartitionID()); + + EXPECT_EQ(pb.NumColorValues(), block.NumColorValues()); + EXPECT_EQ(pb.ColorValuesRange(), block.ColorValuesRange()); + + for (int j = 0; j < pb.NumPartitions().valueOr(0); ++j) { + EXPECT_EQ(pb.GetEndpointMode(j), block.GetEndpointMode(j)); + } + } +} + +std::vector<ImageTestParams> GetImageTestParams() { + return { + // image_name checkered_dim + { "checkered_4", 4 }, + { "checkered_5", 5 }, + { "checkered_6", 6 }, + { "checkered_7", 7 }, + { "checkered_8", 8 }, + { "checkered_9", 9 }, + { "checkered_10", 10 }, + { "checkered_11", 11 }, + { "checkered_12", 12 }, + }; +} + +INSTANTIATE_TEST_CASE_P(Checkered, IntermediateASTCBlockTest, + ValuesIn(GetImageTestParams())); + +} // namespace + +} // namespace astc_codec diff --git a/src/decoder/test/logical_astc_block_test.cc b/src/decoder/test/logical_astc_block_test.cc new file mode 100644 index 0000000..ed85f3f --- /dev/null +++ b/src/decoder/test/logical_astc_block_test.cc @@ -0,0 +1,273 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/logical_astc_block.h" +#include "src/decoder/test/image_utils.h" + +#include <gtest/gtest.h> +#include <gmock/gmock.h> + +#include <fstream> +#include <string> + +namespace astc_codec { + +namespace { + +using ::testing::Eq; +using ::testing::ElementsAre; +using ::testing::TestWithParam; +using ::testing::ValuesIn; + +ImageBuffer LoadGoldenImageWithAlpha(std::string basename) { + const std::string filename = std::string("src/decoder/testdata/") + basename + ".bmp"; + ImageBuffer result; + LoadGoldenBmp(filename, &result); + EXPECT_EQ(result.BytesPerPixel(), 4); + return result; +} + +ImageBuffer LoadGoldenImage(std::string basename) { + const std::string filename = std::string("src/decoder/testdata/") + basename + ".bmp"; + ImageBuffer result; + LoadGoldenBmp(filename, &result); + EXPECT_EQ(result.BytesPerPixel(), 3); + return result; +} + +struct ImageTestParams { + std::string image_name; + bool has_alpha; + Footprint footprint; + int width; + int height; +}; + +static void PrintTo(const ImageTestParams& params, std::ostream* os) { + *os << "ImageTestParams(" << params.image_name << ", " + << params.width << "x" << params.height << ", " + << (params.has_alpha ? "RGBA" : "RGB") << ", " + << "footprint " << params.footprint.Width() << "x" + << params.footprint.Height() << ")"; +} + +class LogicalASTCBlockTest : public TestWithParam<ImageTestParams> { }; + +// Test to make sure that reading out color values from blocks is not +// terribly wrong. To do so, we compress an image and then decompress it +// using our logical blocks and the library. The difference between the +// decoded images should be minimal. +TEST_P(LogicalASTCBlockTest, ImageWithFootprint) { + const auto& params = GetParam(); + const std::string astc = LoadASTCFile(params.image_name); + + ImageBuffer our_decoded_image; + our_decoded_image.Allocate(params.width, params.height, params.has_alpha ? 4 : 3); + + const int block_width = params.footprint.Width(); + const int block_height = params.footprint.Height(); + + base::UInt128 block; + for (int i = 0; i < astc.size(); i += 16) { + const int block_index = i / 16; + const int blocks_wide = + (params.width + block_width - 1) / block_width; + const int block_x = block_index % blocks_wide; + const int block_y = block_index / blocks_wide; + memcpy(&block, astc.data() + i, sizeof(block)); + + PhysicalASTCBlock physical_block(block); + if (physical_block.IsVoidExtent()) { + auto ve = UnpackVoidExtent(physical_block); + ASSERT_TRUE(ve) << "ASTC encoder produced invalid block!"; + } else { + auto ib = UnpackIntermediateBlock(physical_block); + ASSERT_TRUE(ib) << "ASTC encoder produced invalid block!"; + } + + // Make sure that the library doesn't produce incorrect ASTC blocks. + // This is covered in more depth in other tests in + // intermediate_astc_block_test and physical_astc_block_test + auto lb = UnpackLogicalBlock(params.footprint, physical_block); + ASSERT_TRUE(lb) << "ASTC encoder produced invalid block!"; + + LogicalASTCBlock logical_block = lb.value(); + const size_t color_size = params.has_alpha ? 4 : 3; + + for (int y = 0; y < block_height; ++y) { + for (int x = 0; x < block_width; ++x) { + const int px = block_width * block_x + x; + const int py = block_height * block_y + y; + + // Skip out of bounds. + if (px >= params.width || py >= params.height) { + continue; + } + + uint8_t* pixel = our_decoded_image(px, py); + const RgbaColor decoded_color = logical_block.ColorAt(x, y); + ASSERT_LE(color_size, decoded_color.size()); + + for (int c = 0; c < color_size; ++c) { + // All of the pixels should also be 8-bit values. + ASSERT_GE(decoded_color[c], 0); + ASSERT_LT(decoded_color[c], 256); + pixel[c] = decoded_color[c]; + } + } + } + } + + // Check that the decoded image is *very* similar to the library decoding + // of an ASTC texture. They may not be exact due to differences in how we + // convert a 16-bit float to an 8-bit integer. + ImageBuffer decoded_image = params.has_alpha ? LoadGoldenImageWithAlpha(params.image_name) : LoadGoldenImage(params.image_name); + CompareSumOfSquaredDifferences(decoded_image, our_decoded_image, 1.0); +} + +// Test to make sure that a simple gradient image can be compressed and decoded +// by our logical block representation. This should work with every footprint. +std::vector<ImageTestParams> GetSyntheticImageTestParams() { + return { + // image_name alpha astc footprint width height + { "footprint_4x4", false, Footprint::Get4x4(), 32, 32 }, + { "footprint_5x4", false, Footprint::Get5x4(), 32, 32 }, + { "footprint_5x5", false, Footprint::Get5x5(), 32, 32 }, + { "footprint_6x5", false, Footprint::Get6x5(), 32, 32 }, + { "footprint_6x6", false, Footprint::Get6x6(), 32, 32 }, + { "footprint_8x5", false, Footprint::Get8x5(), 32, 32 }, + { "footprint_8x6", false, Footprint::Get8x6(), 32, 32 }, + { "footprint_10x5", false, Footprint::Get10x5(), 32, 32 }, + { "footprint_10x6", false, Footprint::Get10x6(), 32, 32 }, + { "footprint_8x8", false, Footprint::Get8x8(), 32, 32 }, + { "footprint_10x8", false, Footprint::Get10x8(), 32, 32 }, + { "footprint_10x10", false, Footprint::Get10x10(), 32, 32 }, + { "footprint_12x10", false, Footprint::Get12x10(), 32, 32 }, + { "footprint_12x12", false, Footprint::Get12x12(), 32, 32 }, + }; +} + +INSTANTIATE_TEST_CASE_P(Synthetic, LogicalASTCBlockTest, + ValuesIn(GetSyntheticImageTestParams())); + +// Test to make sure that reading out color values from blocks in a real-world +// image isn't terribly wrong, either. +std::vector<ImageTestParams> GetRealWorldImageTestParams() { + return { + // image_name alpha astc footprint width height + { "rgb_4x4", false, Footprint::Get4x4(), 224, 288 }, + { "rgb_6x6", false, Footprint::Get6x6(), 224, 288 }, + { "rgb_8x8", false, Footprint::Get8x8(), 224, 288 }, + { "rgb_12x12", false, Footprint::Get12x12(), 224, 288 }, + { "rgb_5x4", false, Footprint::Get5x4(), 224, 288 } + }; +} + +INSTANTIATE_TEST_CASE_P(RealWorld, LogicalASTCBlockTest, + ValuesIn(GetRealWorldImageTestParams())); + +// Test to make sure that reading out color values from blocks in a real-world +// image isn't terribly wrong, either. +std::vector<ImageTestParams> GetTransparentImageTestParams() { + return { + // image_name alpha astc footprint width height + { "atlas_small_4x4", true, Footprint::Get4x4(), 256, 256 }, + { "atlas_small_5x5", true, Footprint::Get5x5(), 256, 256 }, + { "atlas_small_6x6", true, Footprint::Get6x6(), 256, 256 }, + { "atlas_small_8x8", true, Footprint::Get8x8(), 256, 256 }, + }; +} + +INSTANTIATE_TEST_CASE_P(Transparent, LogicalASTCBlockTest, + ValuesIn(GetTransparentImageTestParams())); + +// Test to make sure that if we set our endpoints then it's reflected in our +// color selection +TEST(LogicalASTCBlockTest, SetEndpoints) { + LogicalASTCBlock logical_block(Footprint::Get8x8()); + + // Setup a weight checkerboard + for (int j = 0; j < 8; ++j) { + for (int i = 0; i < 8; ++i) { + if (((i ^ j) & 1) == 1) { + logical_block.SetWeightAt(i, j, 0); + } else { + logical_block.SetWeightAt(i, j, 64); + } + } + } + + // Now set the colors to something ridiculous + logical_block.SetEndpoints({{ 123, 45, 67, 89 }}, {{ 101, 121, 31, 41 }}, 0); + + // For each pixel, we expect it to mirror the endpoints in a checkerboard + // pattern + for (int j = 0; j < 8; ++j) { + for (int i = 0; i < 8; ++i) { + if (((i ^ j) & 1) == 1) { + EXPECT_THAT(logical_block.ColorAt(i, j), ElementsAre(123, 45, 67, 89)); + } else { + EXPECT_THAT(logical_block.ColorAt(i, j), ElementsAre(101, 121, 31, 41)); + } + } + } +} + +// Test whether or not setting weight values under different circumstances is +// supported and reflected in the query functions. +TEST(LogicalASTCBlockTest, SetWeightVals) { + LogicalASTCBlock logical_block(Footprint::Get4x4()); + + EXPECT_THAT(logical_block.GetFootprint(), Eq(Footprint::Get4x4())); + + // Not a dual plane by default + EXPECT_FALSE(logical_block.IsDualPlane()); + logical_block.SetWeightAt(2, 3, 2); + + // Set the dual plane + logical_block.SetDualPlaneChannel(0); + EXPECT_TRUE(logical_block.IsDualPlane()); + + // This shouldn't have reset our weight + const LogicalASTCBlock other_block = logical_block; + EXPECT_THAT(other_block.WeightAt(2, 3), Eq(2)); + EXPECT_THAT(other_block.DualPlaneWeightAt(0, 2, 3), Eq(2)); + + // If we set the dual plane weight, it shouldn't change the original weight + // value or the other channels + logical_block.SetDualPlaneWeightAt(0, 2, 3, 1); + EXPECT_THAT(logical_block.WeightAt(2, 3), Eq(2)); + EXPECT_THAT(logical_block.DualPlaneWeightAt(0, 2, 3), Eq(1)); + for (int i = 1; i < 4; ++i) { + EXPECT_THAT(logical_block.DualPlaneWeightAt(i, 2, 3), Eq(2)); + } + + // Remove the dual plane + logical_block.SetDualPlaneChannel(-1); + EXPECT_FALSE(logical_block.IsDualPlane()); + + // Now the original dual plane weight should be reset back to the others. Note + // that we have to call DualPlaneWeightAt from a const logical block since + // returning a reference to a weight that doesn't exist is illegal. + const LogicalASTCBlock other_block2 = logical_block; + EXPECT_THAT(logical_block.WeightAt(2, 3), Eq(2)); + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(logical_block.WeightAt(2, 3), + other_block2.DualPlaneWeightAt(i, 2, 3)); + } +} + +} // namespace + +} // namespace astc_codec diff --git a/src/decoder/test/partition_test.cc b/src/decoder/test/partition_test.cc new file mode 100644 index 0000000..63adfb5 --- /dev/null +++ b/src/decoder/test/partition_test.cc @@ -0,0 +1,263 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/partition.h" + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include <array> +#include <random> +#include <string> +#include <vector> + +namespace { + +using ::testing::ElementsAreArray; +using ::testing::Eq; +using ::testing::Le; +using ::testing::Not; + +using astc_codec::Footprint; +using astc_codec::Partition; +using astc_codec::PartitionMetric; +using astc_codec::GetASTCPartition; +using astc_codec::FindClosestASTCPartition; + +// Test to make sure that a simple difference between two partitions where +// most of the values are the same returns what we expect. +TEST(PartitionTest, TestSimplePartitionMetric) { + Partition a = {Footprint::Get6x6(), /* num_parts = */ 2, + /* partition_id = */ {}, /* assignment = */ {}}; + Partition b = a; + + a.assignment = { + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, + }; + + b.assignment = { + 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + }; + + const int dist = PartitionMetric(a, b); + EXPECT_EQ(dist, 2); +} + +// Test to make sure that if one partition is a subset of another that we still +// return the proper difference against the subset of the larger one. +TEST(PartitionDeathTest, TestPartitionMetric) { + Partition a = {Footprint::Get4x4(), /* num_parts = */ 2, + /* partition_id = */ {}, /* assignment = */ {}}; + Partition b = {Footprint::Get6x6(), /* num_parts = */ 2, + /* partition_id = */ {}, /* assignment = */ {}}; + + a.assignment = {{ + 1, 1, 1, 1, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 1, + }}; + + b.assignment = {{ + 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, + 0, 0, 1, 1, 0, 0, + }}; + + EXPECT_DEATH(PartitionMetric(a, b), ""); +} + +// Test to make sure that even if we have different numbers of subsets for each +// partition, that the returned value is what we'd expect. +TEST(PartitionTest, TestDiffPartsPartitionMetric) { + Partition a = {Footprint::Get4x4(), /* num_parts = */ 2, + /* partition_id = */ {}, /* assignment = */ {}}; + Partition b = {Footprint::Get4x4(), /* num_parts = */ 3, + /* partition_id = */ {}, /* assignment = */ {}}; + + a.assignment = {{ + 2, 2, 2, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 1, + }}; + + b.assignment = {{ + 1, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + }}; + + const int dist = PartitionMetric(a, b); + EXPECT_EQ(dist, 3); +} + +// An additional sanity check test that makes sure that we're not always mapping +// zero to zero in our tests. +TEST(PartitionTest, TestDiffMappingPartitionMetric) { + Partition a = {Footprint::Get4x4(), /* num_parts = */ 2, + /* partition_id = */ {}, /* assignment = */ {}}; + Partition b = {Footprint::Get4x4(), /* num_parts = */ 3, + /* partition_id = */ {}, /* assignment = */ {}}; + + a.assignment = {{ + 0, 1, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + }}; + + b.assignment = {{ + 1, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + }}; + + const int dist = PartitionMetric(a, b); + EXPECT_EQ(dist, 1); +} + +// Finally, if we grab an ASTC partition and modify it a tad, the closest +// partition should still be the same ASTC partition. +TEST(PartitionTest, TestFindingASTCPartition) { + const Partition astc = GetASTCPartition(Footprint::Get12x12(), 3, 0x3CB); + Partition almost_astc = astc; + almost_astc.assignment[0]++; + + const Partition& closest_astc = FindClosestASTCPartition(almost_astc); + EXPECT_EQ(astc, closest_astc); +} + +// Test a partition that was obtained from the reference ASTC encoder. We should +// be able to match it exactly +TEST(PartitionTest, TestSpecificPartition) { + const Partition astc = GetASTCPartition(Footprint::Get10x6(), 3, 557); + EXPECT_THAT(astc.assignment, ElementsAreArray(std::array<int, 60> {{ + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2 }})); +} + +// Make sure that when we match against this specific partition, it'll return a +// partition with the same number of subsets +TEST(PartitionTest, EstimatedPartitionSubsets) { + Partition partition = { + /* footprint = */ Footprint::Get6x6(), + /* num_parts = */ 2, + /* partition_id = */ {}, + /* assignment = */ { + 0, 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1 + }}; + + const Partition astc = FindClosestASTCPartition(partition); + EXPECT_THAT(astc.num_parts, Eq(partition.num_parts)); +} + +// Make sure that regardless of what partition we match against, it'll return a +// partition with at most a fewer number of subsets +TEST(PartitionTest, EstimatedPartitionFewerSubsets) { + std::mt19937 random(0xdeadbeef); + auto randUniform = [&random](int max) { + std::uniform_int_distribution<> dist(0, max - 1); + return dist(random); + }; + + constexpr int kNumFootprints = Footprint::NumValidFootprints(); + const auto kFootprints = std::array<Footprint, kNumFootprints> {{ + Footprint::Get4x4(), + Footprint::Get5x4(), + Footprint::Get5x5(), + Footprint::Get6x5(), + Footprint::Get6x6(), + Footprint::Get8x5(), + Footprint::Get8x6(), + Footprint::Get8x8(), + Footprint::Get10x5(), + Footprint::Get10x6(), + Footprint::Get10x8(), + Footprint::Get10x10(), + Footprint::Get12x10(), + Footprint::Get12x12() + }}; + + constexpr int kNumTests = 200; + for (int i = 0; i < kNumTests; ++i) { + const auto& footprint = kFootprints[randUniform(kNumFootprints)]; + const int num_parts = 2 + randUniform(3); + Partition partition = { + footprint, + num_parts, + /* partition_id = */ {}, + /* assignment = */ std::vector<int>(footprint.NumPixels(), 0)}; + + for (auto& p : partition.assignment) { + p = randUniform(num_parts); + } + + const Partition astc = FindClosestASTCPartition(partition); + EXPECT_THAT(astc.num_parts, Le(partition.num_parts)) + << "Test #" << i << ": " + << "Selected partition with ID " << astc.partition_id.value(); + } +} + +// Make sure that we generate unique partitions that are close to the +// candidates. +TEST(PartitionTest, UniquePartitionResults) { + Partition partition = { + /* footprint = */ Footprint::Get6x6(), + /* num_parts = */ 2, + /* partition_id = */ {}, + /* assignment = */ { + 0, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 1, 1 + }}; + + const auto parts = FindKClosestASTCPartitions(partition, 2); + EXPECT_THAT(*parts[0], Not(Eq(*parts[1]))); +} + +// TODO(google): Verify somehow that the assignment generated from +// GetASTCPartition actually matches what's in the spec. The selection +// function was more or less copy/pasted though so it's unclear how to +// measure that against e.g. the ASTC encoder. + +} // namespace diff --git a/src/decoder/test/physical_astc_block_test.cc b/src/decoder/test/physical_astc_block_test.cc new file mode 100644 index 0000000..8eafe46 --- /dev/null +++ b/src/decoder/test/physical_astc_block_test.cc @@ -0,0 +1,361 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/physical_astc_block.h" +#include "src/base/uint128.h" + +#include <gtest/gtest.h> + +#include <string> +#include <vector> + +using astc_codec::PhysicalASTCBlock; +using astc_codec::ColorEndpointMode; +using astc_codec::base::UInt128; + +namespace { + +static const PhysicalASTCBlock kErrorBlock(UInt128(0)); + +// Test to make sure that each of the constructors work and that +// they produce the same block encodings, since the ASTC blocks +// are little-endian +TEST(PhysicalASTCBlockTest, TestConstructors) { + // Little-endian reading of bytes + PhysicalASTCBlock blk1(0x0000000001FE000173ULL); + PhysicalASTCBlock blk2( + std::string("\x73\x01\x00\xFE\x01\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16)); + EXPECT_EQ(blk1.GetBlockBits(), blk2.GetBlockBits()); +} + +// Test to see if we properly decode the maximum value that a weight +// can take in an ASTC block based on the block mode encoding. We test +// against a valid case and various error cases +TEST(PhysicalASTCBlockTest, TestWeightRange) { + PhysicalASTCBlock blk1(0x0000000001FE000173ULL); + auto weight_range = blk1.WeightRange(); + ASSERT_TRUE(weight_range); + EXPECT_EQ(weight_range.value(), 7); + + // If we flip the high bit then we should have a range of 31, + // although then we have too many bits and this should error. + PhysicalASTCBlock blk2(0x0000000001FE000373ULL); + EXPECT_FALSE(blk2.WeightRange()); + + // One bit per weight -- range of 1 + PhysicalASTCBlock non_shared_cem(0x4000000000800D44ULL); + weight_range = non_shared_cem.WeightRange(); + ASSERT_TRUE(weight_range); + EXPECT_EQ(weight_range.value(), 1); + + // Error blocks have no weight range + EXPECT_FALSE(kErrorBlock.WeightRange()); +} + +// Test to see if we properly decode the weight grid width and height +// in an ASTC block based on the block mode encoding. We test against +// a valid case and various error cases +TEST(PhysicalASTCBlockTest, TestWeightDims) { + PhysicalASTCBlock blk1(0x0000000001FE000173ULL); + auto weight_dims = blk1.WeightGridDims(); + EXPECT_TRUE(weight_dims); + EXPECT_EQ(weight_dims.value()[0], 6); + EXPECT_EQ(weight_dims.value()[1], 5); + + // If we flip the high bit then we should have a range of 31, + // although then we have too many bits for the weight grid + // and this should error. + PhysicalASTCBlock blk2(0x0000000001FE000373ULL); + EXPECT_FALSE(blk2.WeightGridDims()); + EXPECT_EQ(blk2.IsIllegalEncoding().value(), + "Too many bits required for weight grid"); + + // Dual plane block with 3x5 weight dims + PhysicalASTCBlock blk3(0x0000000001FE0005FFULL); + weight_dims = blk3.WeightGridDims(); + ASSERT_TRUE(weight_dims); + EXPECT_EQ(weight_dims->at(0), 3); + EXPECT_EQ(weight_dims->at(1), 5); + + // Error blocks shouldn't have any weight dims + EXPECT_FALSE(kErrorBlock.WeightGridDims()); + + PhysicalASTCBlock non_shared_cem(0x4000000000800D44ULL); + weight_dims = non_shared_cem.WeightGridDims(); + ASSERT_TRUE(weight_dims); + EXPECT_EQ(weight_dims->at(0), 8); + EXPECT_EQ(weight_dims->at(1), 8); +} + +// Test to see whether or not the presence of a dual-plane bit +// is decoded properly. Error encodings are tested to *not* return +// that they have dual planes. +TEST(PhysicalASTCBlockTest, TestDualPlane) { + PhysicalASTCBlock blk1(0x0000000001FE000173ULL); + EXPECT_FALSE(blk1.IsDualPlane()); + EXPECT_FALSE(kErrorBlock.IsDualPlane()); + + // If we flip the dual plane bit, we will have too many bits + // for the weight grid and this should error + PhysicalASTCBlock blk2(0x0000000001FE000573ULL); + EXPECT_FALSE(blk2.IsDualPlane()); + EXPECT_FALSE(blk2.WeightGridDims()); + EXPECT_EQ(blk2.IsIllegalEncoding().value(), + "Too many bits required for weight grid"); + + // A dual plane with 3x5 weight grid should be supported + PhysicalASTCBlock blk3(0x0000000001FE0005FFULL); + EXPECT_TRUE(blk3.IsDualPlane()); + + // If we use the wrong block mode, then a valid block + // shouldn't have any dual plane + PhysicalASTCBlock blk4(0x0000000001FE000108ULL); + EXPECT_FALSE(blk4.IsDualPlane()); + EXPECT_FALSE(blk4.IsIllegalEncoding()); +} + +// Make sure that we properly calculate the number of bits used to encode +// the weight grid. Given error encodings or void extent blocks, this number +// should be zero +TEST(PhysicalASTCBlockTest, TestNumWeightBits) { + // 6x5 single-plane weight grid with 3-bit weights + // should have 90 bits for the weights. + PhysicalASTCBlock blk1(0x0000000001FE000173ULL); + EXPECT_EQ(90, blk1.NumWeightBits()); + + // Error block has no weight bits + EXPECT_FALSE(kErrorBlock.NumWeightBits()); + + // Void extent blocks have no weight bits + EXPECT_FALSE(PhysicalASTCBlock(0xFFF8003FFE000DFCULL).NumWeightBits()); + + // If we flip the dual plane bit, we will have too many bits + // for the weight grid and this should error and return no bits + PhysicalASTCBlock blk2(0x0000000001FE000573ULL); + EXPECT_FALSE(blk2.NumWeightBits()); + + // 3x5 dual-plane weight grid with 3-bit weights + // should have 90 bits for the weights. + PhysicalASTCBlock blk3(0x0000000001FE0005FFULL); + EXPECT_EQ(90, blk3.NumWeightBits()); +} + +// Test to make sure that our weight bits start where we expect them to. +// In other words, make sure that the calculation based on the block mode for +// where the weight bits start is accurate. +TEST(PhysicalASTCBlockTest, TestStartWeightBit) { + EXPECT_EQ(PhysicalASTCBlock(0x4000000000800D44ULL).WeightStartBit(), 64); + + // Error blocks have no weight start bit + EXPECT_FALSE(kErrorBlock.WeightStartBit()); + + // Void extent blocks have no weight start bit + EXPECT_FALSE(PhysicalASTCBlock(0xFFF8003FFE000DFCULL).WeightStartBit()); +} + +// Test to make sure that we catch various different reasons for error encoding +// of ASTC blocks, but also that certain encodings aren't errors. +TEST(PhysicalASTCBlockTest, TestErrorBlocks) { + // Various valid block modes + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE000173ULL).IsIllegalEncoding()); + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE0005FFULL).IsIllegalEncoding()); + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE000108ULL).IsIllegalEncoding()); + + // This is an error because it uses an invalid block mode + EXPECT_EQ(kErrorBlock.IsIllegalEncoding().value(), "Reserved block mode"); + + // This is an error because we have too many weight bits + PhysicalASTCBlock err_blk(0x0000000001FE000573ULL); + EXPECT_EQ(err_blk.IsIllegalEncoding().value(), + "Too many bits required for weight grid"); + + // This is an error because we have too many weights + PhysicalASTCBlock err_blk2 = PhysicalASTCBlock(0x0000000001FE0005A8ULL); + EXPECT_EQ(err_blk2.IsIllegalEncoding().value(), "Too many weights specified"); + + PhysicalASTCBlock err_blk3 = PhysicalASTCBlock(0x0000000001FE000588ULL); + EXPECT_EQ(err_blk3.IsIllegalEncoding().value(), "Too many weights specified"); + + // This is an error because we have too few weights + PhysicalASTCBlock err_blk4 = PhysicalASTCBlock(0x0000000001FE00002ULL); + EXPECT_EQ(err_blk4.IsIllegalEncoding().value(), + "Too few bits required for weight grid"); + + // Four partitions, dual plane -- should be error + // 2x2 weight grid, 3 bits per weight + PhysicalASTCBlock dual_plane_four_parts(0x000000000000001D1FULL); + EXPECT_FALSE(dual_plane_four_parts.NumPartitions()); + EXPECT_EQ(dual_plane_four_parts.IsIllegalEncoding().value(), + "Both four partitions and dual plane specified"); +} + +// Test to make sure that we properly identify and can manipulate void-extent +// blocks. These are ASTC blocks that only define a single color for the entire +// block. +TEST(PhysicalASTCBlockTest, TestVoidExtentBlocks) { + // Various valid block modes that aren't void extent blocks + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE000173ULL).IsVoidExtent()); + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE0005FFULL).IsVoidExtent()); + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE000108ULL).IsVoidExtent()); + + // Error block is not a void extent block + EXPECT_FALSE(kErrorBlock.IsVoidExtent()); + + // Void extent block is void extent block... + UInt128 void_extent_encoding(0, 0xFFF8003FFE000DFCULL); + EXPECT_FALSE(PhysicalASTCBlock(void_extent_encoding).IsIllegalEncoding()); + EXPECT_TRUE(PhysicalASTCBlock(void_extent_encoding).IsVoidExtent()); + + // If we modify the high 64 bits it shouldn't change anything + void_extent_encoding |= UInt128(0xdeadbeefdeadbeef, 0); + EXPECT_FALSE(PhysicalASTCBlock(void_extent_encoding).IsIllegalEncoding()); + EXPECT_TRUE(PhysicalASTCBlock(void_extent_encoding).IsVoidExtent()); +} + +TEST(PhysicalASTCBlockTest, TestVoidExtentCoordinates) { + // The void extent block should have texture coordinates from 0-8191 + auto coords = PhysicalASTCBlock(0xFFF8003FFE000DFCULL).VoidExtentCoords(); + EXPECT_EQ(coords->at(0), 0); + EXPECT_EQ(coords->at(1), 8191); + EXPECT_EQ(coords->at(2), 0); + EXPECT_EQ(coords->at(3), 8191); + + // If we set the coords to all 1's then it's still a void extent + // block, but there aren't any void extent coords. + EXPECT_FALSE(PhysicalASTCBlock(0xFFFFFFFFFFFFFDFCULL).IsIllegalEncoding()); + EXPECT_TRUE(PhysicalASTCBlock(0xFFFFFFFFFFFFFDFCULL).IsVoidExtent()); + EXPECT_FALSE(PhysicalASTCBlock(0xFFFFFFFFFFFFFDFCULL).VoidExtentCoords()); + + // If we set the void extent coords to something where the coords are + // >= each other, then the encoding is illegal. + EXPECT_TRUE(PhysicalASTCBlock(0x0008004002001DFCULL).IsIllegalEncoding()); + EXPECT_TRUE(PhysicalASTCBlock(0x0007FFC001FFFDFCULL).IsIllegalEncoding()); +} + +// Test to see if we can properly identify the number of partitions in a block +// In particular -- we need to make sure we properly identify single and +// multi-partition blocks, but also that void extent and error blocks don't +// return valid numbers of partitions +TEST(PhysicalASTCBlockTest, TestNumPartitions) { + // Various valid block modes, but all single partition + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE000173ULL).NumPartitions(), 1); + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE0005FFULL).NumPartitions(), 1); + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE000108ULL).NumPartitions(), 1); + + // Two to four partitions don't have enough bits for color. + EXPECT_FALSE(PhysicalASTCBlock(0x000000000000000973ULL).NumPartitions()); + EXPECT_FALSE(PhysicalASTCBlock(0x000000000000001173ULL).NumPartitions()); + EXPECT_FALSE(PhysicalASTCBlock(0x000000000000001973ULL).NumPartitions()); + + // Test against having more than one partition + PhysicalASTCBlock non_shared_cem(0x4000000000800D44ULL); + EXPECT_EQ(non_shared_cem.NumPartitions(), 2); +} + +// Test the color endpoint modes specified for how the endpoints are encoded. +// In particular, test that shared color endpoint modes work for multi-partition +// blocks and that non-shared color endpoint modes also work. +TEST(PhysicalASTCBlockTest, TestColorEndpointModes) { + // Four partitions -- one shared CEM + const auto blk1 = PhysicalASTCBlock(0x000000000000001961ULL); + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(blk1.GetEndpointMode(i), ColorEndpointMode::kLDRLumaDirect); + } + + // Void extent blocks have no endpoint modes + EXPECT_FALSE(PhysicalASTCBlock(0xFFF8003FFE000DFCULL).GetEndpointMode(0)); + + // Test out of range partitions + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE000173ULL).GetEndpointMode(1)); + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE000173ULL).GetEndpointMode(-1)); + EXPECT_FALSE(PhysicalASTCBlock(0x0000000001FE000173ULL).GetEndpointMode(100)); + + // Error blocks have no endpoint modes + EXPECT_FALSE(kErrorBlock.GetEndpointMode(0)); + + // Test non-shared CEMs + PhysicalASTCBlock non_shared_cem(0x4000000000800D44ULL); + EXPECT_EQ(non_shared_cem.GetEndpointMode(0), + ColorEndpointMode::kLDRLumaDirect); + EXPECT_EQ(non_shared_cem.GetEndpointMode(1), + ColorEndpointMode::kLDRLumaBaseOffset); +} + +// Make sure that if we have more than one partition then we have proper +// partition IDs (these determine which pixels correspond to which partition) +TEST(PhysicalASTCBlockTest, TestPartitionID) { + // Valid partitions + EXPECT_EQ(PhysicalASTCBlock(0x4000000000FFED44ULL).PartitionID(), 0x3FF); + EXPECT_EQ(PhysicalASTCBlock(0x4000000000AAAD44ULL).PartitionID(), 0x155); + + // Error blocks have no partition IDs + EXPECT_FALSE(kErrorBlock.PartitionID()); + + // Void extent blocks have no endpoint modes + EXPECT_FALSE(PhysicalASTCBlock(0xFFF8003FFE000DFCULL).PartitionID()); +} + +// Make sure that we're properly attributing the number of bits associated with +// the encoded color values. +TEST(PhysicalASTCBlockTest, TestNumColorBits) { + // If we're using a direct luma channel, then the number of color bits is 16 + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE000173ULL).NumColorValues(), 2); + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE000173ULL).NumColorBits(), 16); + + // Error blocks have nothing + EXPECT_FALSE(kErrorBlock.NumColorValues()); + EXPECT_FALSE(kErrorBlock.NumColorBits()); + + // Void extent blocks have four color values and 64 bits of color + EXPECT_EQ(PhysicalASTCBlock(0xFFF8003FFE000DFCULL).NumColorValues(), 4); + EXPECT_EQ(PhysicalASTCBlock(0xFFF8003FFE000DFCULL).NumColorBits(), 64); +} + +// Make sure that we're properly decoding the range of values that each of the +// encoded color values can take +TEST(PhysicalASTCBlockTest, TestColorValuesRange) { + // If we're using a direct luma channel, then we use two color values up to + // a full byte each. + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE000173ULL).ColorValuesRange(), 255); + + // Error blocks have nothing + EXPECT_FALSE(kErrorBlock.ColorValuesRange()); + + // Void extent blocks have four color values and 64 bits of color, so the + // color range for each is sixteen bits. + EXPECT_EQ(PhysicalASTCBlock(0xFFF8003FFE000DFCULL).ColorValuesRange(), + (1 << 16) - 1); +} + +// Test that we know where the color data starts. This is different mostly +// depending on whether or not the block is single-partition or void extent. +TEST(PhysicalASTCBlockTest, TestColorStartBits) { + // Void extent blocks start at bit 64 + EXPECT_EQ(PhysicalASTCBlock(0xFFF8003FFE000DFCULL).ColorStartBit(), 64); + + // Error blocks don't start anywhere + EXPECT_FALSE(kErrorBlock.ColorStartBit()); + + // Single partition blocks start at bit 17 + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE000173ULL).ColorStartBit(), 17); + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE0005FFULL).ColorStartBit(), 17); + EXPECT_EQ(PhysicalASTCBlock(0x0000000001FE000108ULL).ColorStartBit(), 17); + + // Multi-partition blocks start at bit 29 + EXPECT_EQ(PhysicalASTCBlock(0x4000000000FFED44ULL).ColorStartBit(), 29); + EXPECT_EQ(PhysicalASTCBlock(0x4000000000AAAD44ULL).ColorStartBit(), 29); +} + +} // namespace diff --git a/src/decoder/test/quantization_test.cc b/src/decoder/test/quantization_test.cc new file mode 100644 index 0000000..f882876 --- /dev/null +++ b/src/decoder/test/quantization_test.cc @@ -0,0 +1,288 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/quantization.h" +#include "src/decoder/integer_sequence_codec.h" + +#include <gtest/gtest.h> + +#include <functional> +#include <string> +#include <vector> + +namespace astc_codec { + +namespace { + +// Make sure that we never exceed the maximum range that we pass in. +TEST(QuantizationTest, TestQuantizeMaxRange) { + for (int i = kEndpointRangeMinValue; i < 256; ++i) { + EXPECT_LE(QuantizeCEValueToRange(255, i), i); + } + + for (int i = 1; i < kWeightRangeMaxValue; ++i) { + EXPECT_LE(QuantizeWeightToRange(64, i), i); + } +} + +// Make sure that whenever we unquantize and requantize a value we get back +// what we started with. +TEST(QuantizationTest, TestReversibility) { + for (auto itr = ISERangeBegin(); itr != ISERangeEnd(); itr++) { + const int range = *itr; + if (range <= kWeightRangeMaxValue) { + for (int j = 0; j <= range; ++j) { + const int q = UnquantizeWeightFromRange(j, range); + EXPECT_EQ(QuantizeWeightToRange(q, range), j); + } + } + + if (range >= kEndpointRangeMinValue) { + for (int j = 0; j <= range; ++j) { + const int q = UnquantizeCEValueFromRange(j, range); + EXPECT_EQ(QuantizeCEValueToRange(q, range), j); + } + } + } +} + +// Make sure that whenever we quantize a non-maximal value it gets sent to the +// proper range +TEST(QuantizationTest, TestQuantizationRange) { + for (auto itr = ISERangeBegin(); itr != ISERangeEnd(); itr++) { + const int range = *itr; + if (range >= kEndpointRangeMinValue) { + EXPECT_LE(QuantizeCEValueToRange(0, range), range); + EXPECT_LE(QuantizeCEValueToRange(4, range), range); + EXPECT_LE(QuantizeCEValueToRange(15, range), range); + EXPECT_LE(QuantizeCEValueToRange(22, range), range); + EXPECT_LE(QuantizeCEValueToRange(66, range), range); + EXPECT_LE(QuantizeCEValueToRange(91, range), range); + EXPECT_LE(QuantizeCEValueToRange(126, range), range); + } + + if (range <= kWeightRangeMaxValue) { + EXPECT_LE(QuantizeWeightToRange(0, range), range); + EXPECT_LE(QuantizeWeightToRange(4, range), range); + EXPECT_LE(QuantizeWeightToRange(15, range), range); + EXPECT_LE(QuantizeWeightToRange(22, range), range); + } + } +} + +// Make sure that whenever we unquantize a value it remains within [0, 255] +TEST(QuantizationTest, TestUnquantizationRange) { + EXPECT_LT(UnquantizeCEValueFromRange(2, 7), 256); + EXPECT_LT(UnquantizeCEValueFromRange(7, 7), 256); + EXPECT_LT(UnquantizeCEValueFromRange(39, 63), 256); + EXPECT_LT(UnquantizeCEValueFromRange(66, 79), 256); + EXPECT_LT(UnquantizeCEValueFromRange(91, 191), 256); + EXPECT_LT(UnquantizeCEValueFromRange(126, 255), 256); + EXPECT_LT(UnquantizeCEValueFromRange(255, 255), 256); + + EXPECT_LE(UnquantizeWeightFromRange(0, 1), 64); + EXPECT_LE(UnquantizeWeightFromRange(2, 7), 64); + EXPECT_LE(UnquantizeWeightFromRange(7, 7), 64); + EXPECT_LE(UnquantizeWeightFromRange(29, 31), 64); +} + +// When we quantize a value, it should use the largest quantization range that +// does not exceed the desired range. +TEST(QuantizationTest, TestUpperBoundRanges) { + auto expected_range_itr = ISERangeBegin(); + for (int desired_range = 1; desired_range < 256; ++desired_range) { + if (desired_range == *(expected_range_itr + 1)) { + ++expected_range_itr; + } + const int expected_range = *expected_range_itr; + ASSERT_LE(expected_range, desired_range); + + if (desired_range >= kEndpointRangeMinValue) { + EXPECT_EQ(QuantizeCEValueToRange(0, desired_range), + QuantizeCEValueToRange(0, expected_range)); + + EXPECT_EQ(QuantizeCEValueToRange(208, desired_range), + QuantizeCEValueToRange(208, expected_range)); + + EXPECT_EQ(QuantizeCEValueToRange(173, desired_range), + QuantizeCEValueToRange(173, expected_range)); + + EXPECT_EQ(QuantizeCEValueToRange(13, desired_range), + QuantizeCEValueToRange(13, expected_range)); + + EXPECT_EQ(QuantizeCEValueToRange(255, desired_range), + QuantizeCEValueToRange(255, expected_range)); + } + + if (desired_range <= kWeightRangeMaxValue) { + EXPECT_EQ(QuantizeWeightToRange(0, desired_range), + QuantizeWeightToRange(0, expected_range)); + + EXPECT_EQ(QuantizeWeightToRange(63, desired_range), + QuantizeWeightToRange(63, expected_range)); + + EXPECT_EQ(QuantizeWeightToRange(12, desired_range), + QuantizeWeightToRange(12, expected_range)); + + EXPECT_EQ(QuantizeWeightToRange(23, desired_range), + QuantizeWeightToRange(23, expected_range)); + } + } + + // Make sure that we covered all the possible ranges + ASSERT_EQ(std::next(expected_range_itr), ISERangeEnd()); +} + +// Make sure that quantizing to the largest range is the identity function. +TEST(QuantizationTest, TestIdentity) { + for (int i = 0; i < 256; ++i) { + EXPECT_EQ(QuantizeCEValueToRange(i, 255), i); + } + + // Note: This doesn't apply to weights since there's a weird hack to convert + // values from [0, 31] to [0, 64]. +} + +// Make sure that bit quantization is monotonic with respect to the input, +// since quantizing and dequantizing bits is a matter of truncation and bit +// replication +TEST(QuantizationTest, TestMonotonicBitPacking) { + for (int num_bits = 3; num_bits < 8; ++num_bits) { + const int range = (1 << num_bits) - 1; + int last_quant_val = -1; + for (int i = 0; i < 256; ++i) { + const int quant_val = QuantizeCEValueToRange(i, range); + EXPECT_LE(last_quant_val, quant_val); + last_quant_val = quant_val; + } + + // Also expect the last quantization val to be equal to the range + EXPECT_EQ(last_quant_val, range); + + if (range <= kWeightRangeMaxValue) { + last_quant_val = -1; + for (int i = 0; i <= 64; ++i) { + const int quant_val = QuantizeWeightToRange(i, range); + EXPECT_LE(last_quant_val, quant_val); + last_quant_val = quant_val; + } + EXPECT_EQ(last_quant_val, range); + } + } +} + +// Make sure that bit quantization reflects that quantized values below the bit +// replication threshold get mapped to zero +TEST(QuantizationTest, TestSmallBitPacking) { + for (int num_bits = 1; num_bits <= 8; ++num_bits) { + const int range = (1 << num_bits) - 1; + + // The largest number that should map to zero is one less than half of the + // smallest representation w.r.t. range. For example: if we have a range + // of 7, it means that we have 3 total bits abc for quantized values. If we + // unquantize to 8 bits, it means that our resulting value will be abcabcab. + // Hence, we map 000 to 0 and 001 to 0b00100100 = 36. The earliest value + // that should not map to zero with three bits is therefore 0b00001111 = 15. + // This ends up being (1 << (8 - 3 - 1)) - 1. We don't use 0b00011111 = 31 + // because this would "round up" to 1 during quantization. This value is not + // necessarily the largest, but it is the largest that we can *guarantee* + // should map to zero. + + if (range >= kEndpointRangeMinValue) { + constexpr int cev_bits = 8; + const int half_max_quant_bits = std::max(0, cev_bits - num_bits - 1); + const int largest_cev_to_zero = (1 << half_max_quant_bits) - 1; + EXPECT_EQ(QuantizeCEValueToRange(largest_cev_to_zero, range), 0) + << " Largest CEV to zero: " << largest_cev_to_zero + << " Range: " << range; + } + + if (range <= kWeightRangeMaxValue) { + constexpr int weight_bits = 6; + const int half_max_quant_bits = std::max(0, weight_bits - num_bits - 1); + const int largest_weight_to_zero = (1 << half_max_quant_bits) - 1; + EXPECT_EQ(QuantizeWeightToRange(largest_weight_to_zero, range), 0) + << " Largest weight to zero: " << largest_weight_to_zero + << " Range: " << range; + } + } +} + +// Test specific quint and trit weight encodings with values that were obtained +// using the reference ASTC codec. +TEST(QuantizationTest, TestSpecificQuintTritPackings) { + std::vector<int> vals = { 4, 6, 4, 6, 7, 5, 7, 5 }; + std::vector<int> quantized; + + // Test a quint packing + std::transform( + vals.begin(), vals.end(), std::back_inserter(quantized), + std::bind(UnquantizeWeightFromRange, std::placeholders::_1, 9)); + const std::vector<int> quintExpected = {14, 21, 14, 21, 43, 50, 43, 50 }; + EXPECT_EQ(quantized, quintExpected); + + // Test a trit packing + std::transform( + vals.begin(), vals.end(), quantized.begin(), + std::bind(UnquantizeWeightFromRange, std::placeholders::_1, 11)); + const std::vector<int> tritExpected = { 5, 23, 5, 23, 41, 59, 41, 59 }; + EXPECT_EQ(quantized, tritExpected); +} + +// Make sure that we properly die when we pass in values below the minimum +// allowed ranges for our quantization intervals. +TEST(QuantizationDeathTest, TestInvalidMinRange) { + for (int i = 0; i < kEndpointRangeMinValue; ++i) { + EXPECT_DEBUG_DEATH(QuantizeCEValueToRange(0, i), ""); + EXPECT_DEBUG_DEATH(UnquantizeCEValueFromRange(0, i), ""); + } + + EXPECT_DEBUG_DEATH(QuantizeWeightToRange(0, 0), ""); + EXPECT_DEBUG_DEATH(UnquantizeWeightFromRange(0, 0), ""); +} + +// Make sure that we properly die when we pass in bogus values. +TEST(QuantizationDeathTest, TestOutOfRange) { + EXPECT_DEBUG_DEATH(QuantizeCEValueToRange(-1, 10), ""); + EXPECT_DEBUG_DEATH(QuantizeCEValueToRange(256, 7), ""); + EXPECT_DEBUG_DEATH(QuantizeCEValueToRange(10000, 17), ""); + + EXPECT_DEBUG_DEATH(UnquantizeCEValueFromRange(-1, 10), ""); + EXPECT_DEBUG_DEATH(UnquantizeCEValueFromRange(8, 7), ""); + EXPECT_DEBUG_DEATH(UnquantizeCEValueFromRange(-1000, 17), ""); + + EXPECT_DEBUG_DEATH(QuantizeCEValueToRange(0, -7), ""); + EXPECT_DEBUG_DEATH(UnquantizeCEValueFromRange(0, -17), ""); + + EXPECT_DEBUG_DEATH(QuantizeCEValueToRange(0, 257), ""); + EXPECT_DEBUG_DEATH(UnquantizeCEValueFromRange(0, 256), ""); + + EXPECT_DEBUG_DEATH(QuantizeWeightToRange(-1, 10), ""); + EXPECT_DEBUG_DEATH(QuantizeWeightToRange(256, 7), ""); + EXPECT_DEBUG_DEATH(QuantizeWeightToRange(10000, 17), ""); + + EXPECT_DEBUG_DEATH(UnquantizeWeightFromRange(-1, 10), ""); + EXPECT_DEBUG_DEATH(UnquantizeWeightFromRange(8, 7), ""); + EXPECT_DEBUG_DEATH(UnquantizeWeightFromRange(-1000, 17), ""); + + EXPECT_DEBUG_DEATH(QuantizeWeightToRange(0, -7), ""); + EXPECT_DEBUG_DEATH(UnquantizeWeightFromRange(0, -17), ""); + + EXPECT_DEBUG_DEATH(QuantizeWeightToRange(0, 32), ""); + EXPECT_DEBUG_DEATH(UnquantizeWeightFromRange(0, 64), ""); +} + +} // namespace + +} // namespace astc_codec diff --git a/src/decoder/test/weight_infill_test.cc b/src/decoder/test/weight_infill_test.cc new file mode 100644 index 0000000..79c7745 --- /dev/null +++ b/src/decoder/test/weight_infill_test.cc @@ -0,0 +1,69 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/decoder/weight_infill.h" +#include "src/decoder/footprint.h" + +#include <gtest/gtest.h> + +#include <vector> + +namespace astc_codec { + +namespace { + +// Make sure that the physical size of the bit representations for certain +// dimensions of weight grids matches our expectations +TEST(ASTCWeightInfillTest, TestGetBitCount) { + // Bit encodings + EXPECT_EQ(32, CountBitsForWeights(4, 4, 3)); + EXPECT_EQ(48, CountBitsForWeights(4, 4, 7)); + EXPECT_EQ(24, CountBitsForWeights(2, 4, 7)); + EXPECT_EQ(8, CountBitsForWeights(2, 4, 1)); + + // Trit encodings + EXPECT_EQ(32, CountBitsForWeights(4, 5, 2)); + EXPECT_EQ(26, CountBitsForWeights(4, 4, 2)); + EXPECT_EQ(52, CountBitsForWeights(4, 5, 5)); + EXPECT_EQ(42, CountBitsForWeights(4, 4, 5)); + + // Quint encodings + EXPECT_EQ(21, CountBitsForWeights(3, 3, 4)); + EXPECT_EQ(38, CountBitsForWeights(4, 4, 4)); + EXPECT_EQ(49, CountBitsForWeights(3, 7, 4)); + EXPECT_EQ(52, CountBitsForWeights(4, 3, 19)); + EXPECT_EQ(70, CountBitsForWeights(4, 4, 19)); +} + +// Make sure that we bilerp our weights properly +TEST(ASTCWeightInfillTest, TestInfillBilerp) { + std::vector<int> weights = InfillWeights( + {{ 1, 3, 5, 3, 5, 7, 5, 7, 9 }}, Footprint::Get5x5(), 3, 3); + + std::vector<int> expected_weights = { + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 9 }; + + ASSERT_EQ(weights.size(), expected_weights.size()); + for (int i = 0; i < weights.size(); ++i) { + EXPECT_EQ(weights[i], expected_weights[i]); + } +} + +} // namespace + +} // namespace astc_codec |