From b56a50064caf2a590ba43699e0074690fcd431bf Mon Sep 17 00:00:00 2001 From: Jeff McGlynn Date: Wed, 20 Jun 2018 11:34:20 -0700 Subject: Initial version of astc-codec for open source release Contains an implementation of an ASTC decoder that is able to pass the dEQP ASTC LDR tests. astc-codec has no external dependencies for the main library, only for test code, and is licensed under the Apache license. Components: include/ - Public API that can decode ASTC LDR data into a RGBA UNORM8 buffer. src/base/ - Base library with common functionality not directly related to ASTC decoding. Contains a uint128 implementation, BitStream for reading/writing bits with a primitive (or uint128 type), Optional implementation (to not take a dependency on C++17), and more. src/decoder/ - Internal implementation of the ASTC decoder. src/base/test/, src/decoder/test/ - Unit tests (and a fuzzing test) for the astc decoder. src/decoder/testdata/ - Sample ASTC images and golden image results for testing. src/decoder/tools/ - A tool to inspect contents of an ASTC file. third_party/ - Third party libraries, only used for tests. Change-Id: Ia98e5a7dc847daa3d3a48c5e62d94b8fb1cb98bd --- BUILD.bazel | 13 + README.md | 37 +- include/astc-codec/astc-codec.h | 75 ++ src/.clang-format | 4 + src/base/BUILD.bazel | 45 ++ src/base/bit_stream.h | 77 ++ src/base/bottom_n.h | 78 ++ src/base/math_utils.h | 80 ++ src/base/optional.h | 520 ++++++++++++ src/base/string_utils.h | 68 ++ src/base/test/bit_stream_test.cpp | 141 ++++ src/base/test/bottom_n_test.cpp | 99 +++ src/base/test/math_utils_test.cpp | 78 ++ src/base/test/optional_test.cpp | 481 +++++++++++ src/base/test/string_utils_test.cpp | 110 +++ src/base/test/type_traits_test.cpp | 128 +++ src/base/test/uint128_test.cpp | 140 ++++ src/base/type_traits.h | 172 ++++ src/base/uint128.h | 175 ++++ src/decoder/BUILD.bazel | 238 ++++++ src/decoder/astc_file.cc | 185 +++++ src/decoder/astc_file.h | 97 +++ src/decoder/codec.cc | 132 ++++ src/decoder/codec.h | 41 + src/decoder/endpoint_codec.cc | 967 +++++++++++++++++++++++ src/decoder/endpoint_codec.h | 90 +++ src/decoder/footprint.cc | 162 ++++ src/decoder/footprint.h | 106 +++ src/decoder/integer_sequence_codec.cc | 562 +++++++++++++ src/decoder/integer_sequence_codec.h | 169 ++++ src/decoder/intermediate_astc_block.cc | 591 ++++++++++++++ src/decoder/intermediate_astc_block.h | 128 +++ src/decoder/logical_astc_block.cc | 262 ++++++ src/decoder/logical_astc_block.h | 127 +++ src/decoder/partition.cc | 600 ++++++++++++++ src/decoder/partition.h | 97 +++ src/decoder/physical_astc_block.cc | 761 ++++++++++++++++++ src/decoder/physical_astc_block.h | 128 +++ src/decoder/quantization.cc | 462 +++++++++++ src/decoder/quantization.h | 65 ++ src/decoder/test/astc_fuzzer.cc | 36 + src/decoder/test/codec_test.cc | 181 +++++ src/decoder/test/endpoint_codec_test.cc | 464 +++++++++++ src/decoder/test/footprint_test.cc | 97 +++ src/decoder/test/image_utils.h | 217 +++++ src/decoder/test/integer_sequence_codec_test.cc | 337 ++++++++ src/decoder/test/intermediate_astc_block_test.cc | 453 +++++++++++ src/decoder/test/logical_astc_block_test.cc | 273 +++++++ src/decoder/test/partition_test.cc | 263 ++++++ src/decoder/test/physical_astc_block_test.cc | 361 +++++++++ src/decoder/test/quantization_test.cc | 288 +++++++ src/decoder/test/weight_infill_test.cc | 69 ++ src/decoder/testdata/atlas_small_4x4.astc | Bin 0 -> 65552 bytes src/decoder/testdata/atlas_small_4x4.bmp | Bin 0 -> 262282 bytes src/decoder/testdata/atlas_small_5x5.astc | Bin 0 -> 43280 bytes src/decoder/testdata/atlas_small_5x5.bmp | Bin 0 -> 262282 bytes src/decoder/testdata/atlas_small_6x6.astc | Bin 0 -> 29600 bytes src/decoder/testdata/atlas_small_6x6.bmp | Bin 0 -> 262282 bytes src/decoder/testdata/atlas_small_8x8.astc | Bin 0 -> 16400 bytes src/decoder/testdata/atlas_small_8x8.bmp | Bin 0 -> 262282 bytes src/decoder/testdata/checkerboard.astc | Bin 0 -> 80 bytes src/decoder/testdata/checkered_10.astc | Bin 0 -> 1616 bytes src/decoder/testdata/checkered_11.astc | Bin 0 -> 1952 bytes src/decoder/testdata/checkered_12.astc | Bin 0 -> 2320 bytes src/decoder/testdata/checkered_4.astc | Bin 0 -> 272 bytes src/decoder/testdata/checkered_5.astc | Bin 0 -> 416 bytes src/decoder/testdata/checkered_6.astc | Bin 0 -> 592 bytes src/decoder/testdata/checkered_7.astc | Bin 0 -> 800 bytes src/decoder/testdata/checkered_8.astc | Bin 0 -> 1040 bytes src/decoder/testdata/checkered_9.astc | Bin 0 -> 1312 bytes src/decoder/testdata/footprint_10x10.astc | Bin 0 -> 272 bytes src/decoder/testdata/footprint_10x10.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_10x5.astc | Bin 0 -> 464 bytes src/decoder/testdata/footprint_10x5.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_10x6.astc | Bin 0 -> 400 bytes src/decoder/testdata/footprint_10x6.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_10x8.astc | Bin 0 -> 272 bytes src/decoder/testdata/footprint_10x8.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_12x10.astc | Bin 0 -> 208 bytes src/decoder/testdata/footprint_12x10.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_12x12.astc | Bin 0 -> 160 bytes src/decoder/testdata/footprint_12x12.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_4x4.astc | Bin 0 -> 1040 bytes src/decoder/testdata/footprint_4x4.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_5x4.astc | Bin 0 -> 912 bytes src/decoder/testdata/footprint_5x4.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_5x5.astc | Bin 0 -> 800 bytes src/decoder/testdata/footprint_5x5.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_6x5.astc | Bin 0 -> 688 bytes src/decoder/testdata/footprint_6x5.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_6x6.astc | Bin 0 -> 592 bytes src/decoder/testdata/footprint_6x6.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_8x5.astc | Bin 0 -> 464 bytes src/decoder/testdata/footprint_8x5.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_8x6.astc | Bin 0 -> 400 bytes src/decoder/testdata/footprint_8x6.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/footprint_8x8.astc | Bin 0 -> 272 bytes src/decoder/testdata/footprint_8x8.bmp | Bin 0 -> 3210 bytes src/decoder/testdata/rgb_12x12.astc | Bin 0 -> 7312 bytes src/decoder/testdata/rgb_12x12.bmp | Bin 0 -> 193674 bytes src/decoder/testdata/rgb_4x4.astc | Bin 0 -> 64528 bytes src/decoder/testdata/rgb_4x4.bmp | Bin 0 -> 193674 bytes src/decoder/testdata/rgb_5x4.astc | Bin 0 -> 51856 bytes src/decoder/testdata/rgb_5x4.bmp | Bin 0 -> 193674 bytes src/decoder/testdata/rgb_6x6.astc | Bin 0 -> 29200 bytes src/decoder/testdata/rgb_6x6.bmp | Bin 0 -> 193674 bytes src/decoder/testdata/rgb_8x8.astc | Bin 0 -> 16144 bytes src/decoder/testdata/rgb_8x8.bmp | Bin 0 -> 193674 bytes src/decoder/tools/astc_inspector_cli.cc | 785 ++++++++++++++++++ src/decoder/types.h | 74 ++ src/decoder/weight_infill.cc | 122 +++ src/decoder/weight_infill.h | 38 + 112 files changed, 12548 insertions(+), 1 deletion(-) create mode 100644 include/astc-codec/astc-codec.h create mode 100644 src/.clang-format create mode 100644 src/base/BUILD.bazel create mode 100644 src/base/bit_stream.h create mode 100644 src/base/bottom_n.h create mode 100644 src/base/math_utils.h create mode 100644 src/base/optional.h create mode 100644 src/base/string_utils.h create mode 100644 src/base/test/bit_stream_test.cpp create mode 100644 src/base/test/bottom_n_test.cpp create mode 100644 src/base/test/math_utils_test.cpp create mode 100644 src/base/test/optional_test.cpp create mode 100644 src/base/test/string_utils_test.cpp create mode 100644 src/base/test/type_traits_test.cpp create mode 100644 src/base/test/uint128_test.cpp create mode 100644 src/base/type_traits.h create mode 100644 src/base/uint128.h create mode 100644 src/decoder/BUILD.bazel create mode 100644 src/decoder/astc_file.cc create mode 100644 src/decoder/astc_file.h create mode 100644 src/decoder/codec.cc create mode 100644 src/decoder/codec.h create mode 100644 src/decoder/endpoint_codec.cc create mode 100644 src/decoder/endpoint_codec.h create mode 100644 src/decoder/footprint.cc create mode 100644 src/decoder/footprint.h create mode 100644 src/decoder/integer_sequence_codec.cc create mode 100644 src/decoder/integer_sequence_codec.h create mode 100644 src/decoder/intermediate_astc_block.cc create mode 100644 src/decoder/intermediate_astc_block.h create mode 100644 src/decoder/logical_astc_block.cc create mode 100644 src/decoder/logical_astc_block.h create mode 100644 src/decoder/partition.cc create mode 100644 src/decoder/partition.h create mode 100644 src/decoder/physical_astc_block.cc create mode 100644 src/decoder/physical_astc_block.h create mode 100644 src/decoder/quantization.cc create mode 100644 src/decoder/quantization.h create mode 100644 src/decoder/test/astc_fuzzer.cc create mode 100644 src/decoder/test/codec_test.cc create mode 100644 src/decoder/test/endpoint_codec_test.cc create mode 100644 src/decoder/test/footprint_test.cc create mode 100644 src/decoder/test/image_utils.h create mode 100644 src/decoder/test/integer_sequence_codec_test.cc create mode 100644 src/decoder/test/intermediate_astc_block_test.cc create mode 100644 src/decoder/test/logical_astc_block_test.cc create mode 100644 src/decoder/test/partition_test.cc create mode 100644 src/decoder/test/physical_astc_block_test.cc create mode 100644 src/decoder/test/quantization_test.cc create mode 100644 src/decoder/test/weight_infill_test.cc create mode 100644 src/decoder/testdata/atlas_small_4x4.astc create mode 100644 src/decoder/testdata/atlas_small_4x4.bmp create mode 100644 src/decoder/testdata/atlas_small_5x5.astc create mode 100644 src/decoder/testdata/atlas_small_5x5.bmp create mode 100644 src/decoder/testdata/atlas_small_6x6.astc create mode 100644 src/decoder/testdata/atlas_small_6x6.bmp create mode 100644 src/decoder/testdata/atlas_small_8x8.astc create mode 100644 src/decoder/testdata/atlas_small_8x8.bmp create mode 100644 src/decoder/testdata/checkerboard.astc create mode 100644 src/decoder/testdata/checkered_10.astc create mode 100644 src/decoder/testdata/checkered_11.astc create mode 100644 src/decoder/testdata/checkered_12.astc create mode 100644 src/decoder/testdata/checkered_4.astc create mode 100644 src/decoder/testdata/checkered_5.astc create mode 100644 src/decoder/testdata/checkered_6.astc create mode 100644 src/decoder/testdata/checkered_7.astc create mode 100644 src/decoder/testdata/checkered_8.astc create mode 100644 src/decoder/testdata/checkered_9.astc create mode 100644 src/decoder/testdata/footprint_10x10.astc create mode 100644 src/decoder/testdata/footprint_10x10.bmp create mode 100644 src/decoder/testdata/footprint_10x5.astc create mode 100644 src/decoder/testdata/footprint_10x5.bmp create mode 100644 src/decoder/testdata/footprint_10x6.astc create mode 100644 src/decoder/testdata/footprint_10x6.bmp create mode 100644 src/decoder/testdata/footprint_10x8.astc create mode 100644 src/decoder/testdata/footprint_10x8.bmp create mode 100644 src/decoder/testdata/footprint_12x10.astc create mode 100644 src/decoder/testdata/footprint_12x10.bmp create mode 100644 src/decoder/testdata/footprint_12x12.astc create mode 100644 src/decoder/testdata/footprint_12x12.bmp create mode 100644 src/decoder/testdata/footprint_4x4.astc create mode 100644 src/decoder/testdata/footprint_4x4.bmp create mode 100644 src/decoder/testdata/footprint_5x4.astc create mode 100644 src/decoder/testdata/footprint_5x4.bmp create mode 100644 src/decoder/testdata/footprint_5x5.astc create mode 100644 src/decoder/testdata/footprint_5x5.bmp create mode 100644 src/decoder/testdata/footprint_6x5.astc create mode 100644 src/decoder/testdata/footprint_6x5.bmp create mode 100644 src/decoder/testdata/footprint_6x6.astc create mode 100644 src/decoder/testdata/footprint_6x6.bmp create mode 100644 src/decoder/testdata/footprint_8x5.astc create mode 100644 src/decoder/testdata/footprint_8x5.bmp create mode 100644 src/decoder/testdata/footprint_8x6.astc create mode 100644 src/decoder/testdata/footprint_8x6.bmp create mode 100644 src/decoder/testdata/footprint_8x8.astc create mode 100644 src/decoder/testdata/footprint_8x8.bmp create mode 100644 src/decoder/testdata/rgb_12x12.astc create mode 100644 src/decoder/testdata/rgb_12x12.bmp create mode 100644 src/decoder/testdata/rgb_4x4.astc create mode 100644 src/decoder/testdata/rgb_4x4.bmp create mode 100644 src/decoder/testdata/rgb_5x4.astc create mode 100644 src/decoder/testdata/rgb_5x4.bmp create mode 100644 src/decoder/testdata/rgb_6x6.astc create mode 100644 src/decoder/testdata/rgb_6x6.bmp create mode 100644 src/decoder/testdata/rgb_8x8.astc create mode 100644 src/decoder/testdata/rgb_8x8.bmp create mode 100644 src/decoder/tools/astc_inspector_cli.cc create mode 100644 src/decoder/types.h create mode 100644 src/decoder/weight_infill.cc create mode 100644 src/decoder/weight_infill.h diff --git a/BUILD.bazel b/BUILD.bazel index c947e83..8fc9eca 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -13,3 +13,16 @@ # limitations under the License. licenses(["notice"]) + +cc_library( + name = "api", + hdrs = ["include/astc-codec/astc-codec.h"], + visibility = ["//src/decoder:__pkg__"], +) + +cc_library( + name = "astc_codec", + deps = ["//src/decoder:codec"], + includes = ["include"], + visibility = ["//visibility:public"], +) diff --git a/README.md b/README.md index 41daba0..7d53cb8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,41 @@ astc-codec is a software ASTC decoder implementation, which supports the ASTC LDR profile. +Example usage: + +``` +#include + +// ... + +std::vector astc = LoadMyASTCData(); +const size_t width = 640; +const size_t height = 480; + +std::vector result; +result.resize(width * height * 4); + +bool result = astc_codec::ASTCDecompressToRGBA( + astc.data(), astc.size(), width, height, astc_codec::FootprintType::k4x4, + result.data(), result.size(), /* stride */ width * 4); +``` + +## Building + +Install [Bazel](https://bazel.build/), and then run: + +``` +bazel build :astc_codec -c opt +``` + +astc-codec has been tested on Mac and Linux. + +## Run Tests + +``` +bazel test //... +``` + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for important contributing requirements. @@ -12,4 +47,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for important contributing requirements. astc-codec project is licensed under the Apache License Version 2.0. You can find a copy of it in [LICENSE](LICENSE). -This is not an official Google product. +This is not an officially supported Google product. diff --git a/include/astc-codec/astc-codec.h b/include/astc-codec/astc-codec.h new file mode 100644 index 0000000..1d41218 --- /dev/null +++ b/include/astc-codec/astc-codec.h @@ -0,0 +1,75 @@ +// 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. + +#ifndef ASTC_CODEC_ASTC_CODEC_H_ +#define ASTC_CODEC_ASTC_CODEC_H_ + +#include +#include + +namespace astc_codec { + +// These are the valid ASTC footprints according to the specification in +// Section C.2.7. +enum class FootprintType { + k4x4, + k5x4, + k5x5, + k6x5, + k6x6, + k8x5, + k8x6, + k10x5, + k10x6, + k8x8, + k10x8, + k10x10, + k12x10, + k12x12, + + kCount +}; + +// Decompresses ASTC LDR image data to a RGBA32 buffer. +// +// Supports formats defined in the KHR_texture_compression_astc_ldr spec and +// returns UNORM8 values. sRGB is not supported, and should be implemented +// by the caller. +// +// |astc_data| - Compressed ASTC image buffer, must be at least |astc_data_size| +// bytes long. +// |astc_data_size| - The size of |astc_data|, in bytes. +// |width| - Image width, in pixels. +// |height| - Image height, in pixels. +// |footprint| - The ASTC footprint (block size) of the compressed image buffer. +// |out_buffer| - Pointer to a buffer where the decompressed image will be +// stored, must be at least |out_buffer_size| bytes long. +// |out_buffer_size| - The size of |out_buffer|, in bytes, at least +// height*out_buffer_stride. If this is too small, this +// function will return false and no data will be +// decompressed. +// |out_buffer_stride| - The stride that should be used to store rows of the +// decoded image, must be at least 4*width bytes. +// +// Returns true if the decompression succeeded, or false if decompression +// failed, or if the astc_data_size was too small for the given width, height, +// and footprint, or if out_buffer_size is too small. +bool ASTCDecompressToRGBA(const uint8_t* astc_data, size_t astc_data_size, + size_t width, size_t height, FootprintType footprint, + uint8_t* out_buffer, size_t out_buffer_size, + size_t out_buffer_stride); + +} // namespace astc_codec + +#endif // ASTC_CODEC_ASTC_CODEC_H_ diff --git a/src/.clang-format b/src/.clang-format new file mode 100644 index 0000000..9a00ee2 --- /dev/null +++ b/src/.clang-format @@ -0,0 +1,4 @@ +BasedOnStyle: Google +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Inline +SpaceAfterTemplateKeyword: false diff --git a/src/base/BUILD.bazel b/src/base/BUILD.bazel new file mode 100644 index 0000000..9d8b9a0 --- /dev/null +++ b/src/base/BUILD.bazel @@ -0,0 +1,45 @@ +# 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. + +cc_library( + name = "base", + hdrs = [ + "bit_stream.h", + "bottom_n.h", + "math_utils.h", + "optional.h", + "string_utils.h", + "type_traits.h", + "uint128.h", + ], + visibility = ["//src/decoder:__pkg__"], +) + +cc_test( + name = "base_test", + srcs = [ + "test/bit_stream_test.cpp", + "test/bottom_n_test.cpp", + "test/math_utils_test.cpp", + "test/optional_test.cpp", + "test/string_utils_test.cpp", + "test/type_traits_test.cpp", + "test/uint128_test.cpp", + ], + deps = [ + "@gtest//:gtest_main", + ":base", + ], +) + diff --git a/src/base/bit_stream.h b/src/base/bit_stream.h new file mode 100644 index 0000000..c878197 --- /dev/null +++ b/src/base/bit_stream.h @@ -0,0 +1,77 @@ +// 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. + +#ifndef ASTC_CODEC_BASE_BIT_STREAM_H_ +#define ASTC_CODEC_BASE_BIT_STREAM_H_ + +#include +#include + +namespace astc_codec { +namespace base { + +// Represents a stream of bits that can be read or written in arbitrary-sized +// chunks. +template +class BitStream { + public: + // Creates an empty BitStream. + BitStream() = default; + BitStream(IntType data, uint32_t data_size) + : data_(data), data_size_(data_size) { + assert(data_size_ <= sizeof(data_) * 8); + } + + // Return the number of bits in the stream. + uint32_t Bits() const { return data_size_; } + + // Put |size| bits into the stream. + // Fails if there is not enough space in the buffer to store the bits. + template + void PutBits(ResultType x, uint32_t size) { + assert(data_size_ + size <= sizeof(data_) * 8); + + data_ |= (IntType(x) & MaskFor(size)) << data_size_; + data_size_ += size; + } + + // Get |count| bits from the stream. + // Returns true if |count| bits were successfully retrieved. + template + bool GetBits(uint32_t count, ResultType* result) { + if (count <= data_size_) { + *result = static_cast(data_ & MaskFor(count)); + data_ = data_ >> count; + data_size_ -= count; + return true; + } else { + *result = 0; + return false; + } + } + + private: + IntType MaskFor(uint32_t bits) const { + return (bits == sizeof(IntType) * 8) ? ~IntType(0) + : (IntType(1) << bits) - 1; + } + + IntType data_ = 0; + uint32_t data_size_ = 0; +}; + +} // namespace base +} // namespace astc_codec + +#endif // ASTC_CODEC_BASE_BIT_STREAM_H_ diff --git a/src/base/bottom_n.h b/src/base/bottom_n.h new file mode 100644 index 0000000..4edc8ef --- /dev/null +++ b/src/base/bottom_n.h @@ -0,0 +1,78 @@ +// 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. + +#ifndef ASTC_CODEC_BASE_BOTTOM_N_H_ +#define ASTC_CODEC_BASE_BOTTOM_N_H_ + +#include +#include +#include + +namespace astc_codec { +namespace base { + +// Used to aggregate the lowest N values of data supplied. +template> +class BottomN { + public: + typedef std::vector ContainerType; + + // Creates an empty BottomN with limit |max_size|. + BottomN(size_t max_size) : max_size_(max_size) { } + + bool Empty() const { return data_.empty(); } + size_t Size() const { return data_.size(); } + + const T& Top() const { return data_.front(); } + + void Push(const T& value) { + if (data_.size() < max_size_ || compare_(value, Top())) { + data_.push_back(value); + std::push_heap(data_.begin(), data_.end(), compare_); + + if (Size() > max_size_) { + PopTop(); + } + } + } + + std::vector Pop() { + const size_t len = Size(); + std::vector result(len); + + for (size_t i = 0; i < len; ++i) { + result[len - i - 1] = PopTop(); + } + + return result; + } + + private: + T PopTop() { + std::pop_heap(data_.begin(), data_.end(), compare_); + T result = data_.back(); + data_.pop_back(); + return result; + } + + ContainerType data_; + CompareFn compare_; + + const size_t max_size_; +}; + +} // namespace base +} // namespace astc_codec + +#endif // ASTC_CODEC_BASE_BOTTOM_N_H_ diff --git a/src/base/math_utils.h b/src/base/math_utils.h new file mode 100644 index 0000000..48f1a24 --- /dev/null +++ b/src/base/math_utils.h @@ -0,0 +1,80 @@ +// 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. + +#ifndef ASTC_CODEC_BASE_MATH_UTILS_H_ +#define ASTC_CODEC_BASE_MATH_UTILS_H_ + +#include "src/base/uint128.h" + +#include +#include +#include + +namespace astc_codec { +namespace base { + +inline int Log2Floor(uint32_t n) { + if (n == 0) { + return -1; + } + + int log = 0; + uint32_t value = n; + for (int i = 4; i >= 0; --i) { + int shift = (1 << i); + uint32_t x = value >> shift; + if (x != 0) { + value = x; + log += shift; + } + } + assert(value == 1); + return log; +} + +inline int CountOnes(uint32_t n) { + n -= ((n >> 1) & 0x55555555); + n = ((n >> 2) & 0x33333333) + (n & 0x33333333); + return static_cast((((n + (n >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24); +} + +template +inline T ReverseBits(T value) { + uint32_t s = sizeof(value) * 8; + T mask = ~T(0); + while ((s >>= 1) > 0) { + mask ^= (mask << s); + value = ((value >> s) & mask) | ((value << s) & ~mask); + } + + return value; +} + +template +inline T GetBits(T source, uint32_t offset, uint32_t count) { + static_assert(std::is_same::value || std::is_unsigned::value, + "T must be unsigned."); + + const uint32_t total_bits = sizeof(T) * 8; + assert(count > 0); + assert(offset + count <= total_bits); + + const T mask = count == total_bits ? ~T(0) : ~T(0) >> (total_bits - count); + return (source >> offset) & mask; +} + +} // namespace base +} // namespace astc_codec + +#endif // ASTC_CODEC_BASE_MATH_UTILS_H_ diff --git a/src/base/optional.h b/src/base/optional.h new file mode 100644 index 0000000..5ede4af --- /dev/null +++ b/src/base/optional.h @@ -0,0 +1,520 @@ +// 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. + +#ifndef ASTC_CODEC_BASE_OPTIONAL_H_ +#define ASTC_CODEC_BASE_OPTIONAL_H_ + +#include "src/base/type_traits.h" + +#include +#include +#include +#include + +#include + +// Optional - a template class to store an optional value of type T. +// +// Usage examples: +// +// Initialization and construction: +// Optional foo; // |foo| doesn't contain a value. +// Optional foo(Foo(10)); // |foo| contains a copy-constructed value. +// Optional foo2(foo); // |foo2| contains a copy of |foo|'s value. +// Optional foo3(std::move(foo2)); // Guess what? +// +// Assignment: +// Foo foo_value(0); +// Optional foo; // |foo| is empty. +// Optional foo2; // |foo2| is empty. +// foo2 = foo; // |foo2| is still empty. +// foo = foo_value; // set value of |foo| to a copy of |foo_value| +// foo = std::move(foo_value); // move |foo_value| into |foo|. +// foo2 = foo; // now |foo2| has a copy of |foo|'s value. +// foo = kNullopt; // unset |foo|, it has no value. +// +// Checking and accessing value: +// if (foo) { +// // |foo| has a value. +// doStuff(*foo); // |*foo| is the value inside |foo|. +// foo->callMethod(); // Same as (*foo).callMethod(). +// } else { +// // |foo| is empty. +// } +// +// foo.value() // Same as *foo +// foo.valueOr() // Return is |foo| has no value. +// +// In-place construction: +// +// Optional foo; // |foo| is empty. +// foo.emplace(20); // |foo| now contains a value constructed as Foo(20) +// +// Optional foo(kInplace, 20); // |foo| is initialized with a value +// // that is constructed in-place as +// // Foo(20). +// +// return makeOptional(20); // Takes Foo constructor arguments +// // directly. +// +// Returning values: +// +// Optional myFunc(...) { +// if (someCondition) { +// return Foo(10); // call Optional(Foo&) constructor. +// } else { +// return {}; // call Optional() constructor, which +// // builds an empty value. +// } +// } +// +// Memory layout: +// Optional is equivalent to: +// +// struct { +// bool flag; +// Foo value; +// }; +// +// in terms of memory layout. This means it *doubles* the size of integral +// types. Also: +// +// - Optional can be constructed from anything that constructs a Foo. +// +// - Same with Optional(kInplace, Args...) where Args... matches any +// arguments that can be passed to a Foo constructor. +// +// - Comparison operators are provided. Beware: an empty Optional +// is always smaller than any Foo value. + +namespace astc_codec { +namespace base { + +namespace details { + +// Base classes to reduce the number of instantiations of the Optional's +// internal members. +class OptionalFlagBase { + public: + void setConstructed(bool constructed) { mConstructed = constructed; } + constexpr bool constructed() const { return mConstructed; } + constexpr operator bool() const { return constructed(); } + bool hasValue() const { return constructed(); } + + constexpr OptionalFlagBase(bool constructed = false) + : mConstructed(constructed) { } + + private: + bool mConstructed = false; +}; + +template +class OptionalStorageBase { + protected: + using StoreT = typename std::aligned_storage::type; + StoreT mStorage = {}; +}; + +} // namespace details + +// A tag type for empty optional construction +struct NulloptT { + constexpr explicit NulloptT(int) { } +}; + +// A tag type for inplace value construction +struct InplaceT { + constexpr explicit InplaceT(int) { } +}; + +// Tag values for null optional and inplace construction +constexpr NulloptT kNullopt{1}; +constexpr InplaceT kInplace{1}; + +// Forward declaration for an early use +template +class Optional; + +// A type trait for checking if a type is an optional instantiation +// Note: if you want to refer to the template name inside the template, +// you need to declare this alias outside of it - because the +// class name inside of the template stands for an instantiated template +// E.g, for template class Foo if you say 'Foo' inside the class, it +// actually means Foo; +template +using is_any_optional = + is_template_instantiation_of::type, Optional>; + +template +class Optional + : private details::OptionalFlagBase, + private details::OptionalStorageBase::value> { + // make sure all optionals are buddies - this is needed to implement + // conversion from optionals of other types + template + friend class Optional; + + template + using self = Optional; + + using base_flag = details::OptionalFlagBase; + using base_storage = + details::OptionalStorageBase::value>; + + public: + // std::optional will have this, so let's provide it + using value_type = T; + + // make sure we forbid some Optional instantiations where things may get + // really messy + static_assert(!std::is_same::type, NulloptT>::value, + "Optional of NulloptT is not allowed"); + static_assert(!std::is_same::type, InplaceT>::value, + "Optional of InplaceT is not allowed"); + static_assert(!std::is_reference::value, + "Optional references are not allowed: use a pointer instead"); + + // constructors + constexpr Optional() { } + constexpr Optional(NulloptT) { } + + Optional(const Optional& other) : base_flag(other.constructed()) { + if (this->constructed()) { + new (&get()) T(other.get()); + } + } + Optional(Optional&& other) : base_flag(other.constructed()) { + if (this->constructed()) { + new (&get()) T(std::move(other.get())); + } + } + + // Conversion constructor from optional of similar type + template::value && + std::is_constructible::value>> + Optional(const Optional& other) : base_flag(other.constructed()) { + if (this->constructed()) { + new (&get()) T(other.get()); + } + } + + // Move-conversion constructor + template::value && + std::is_constructible::value>> + Optional(Optional&& other) : base_flag(other.constructed()) { + if (this->constructed()) { + new (&get()) T(std::move(other.get())); + } + } + + // Construction from a raw value + Optional(const T& value) : base_flag(true) { new (&get()) T(value); } + // Move construction from a raw value + Optional(T&& value) : base_flag(true) { new (&get()) T(std::move(value)); } + + // Inplace construction from a list of |T|'s ctor arguments + template + Optional(InplaceT, Args&&... args) : base_flag(true) { + new (&get()) T(std::forward(args)...); + } + + // Inplace construction from an initializer list passed into |T|'s ctor + template>>> + Optional(InplaceT, std::initializer_list il) : base_flag(true) { + new (&get()) T(il); + } + + // direct assignment + Optional& operator=(const Optional& other) { + if (&other == this) { + return *this; + } + + if (this->constructed()) { + if (other.constructed()) { + get() = other.get(); + } else { + destruct(); + this->setConstructed(false); + } + } else { + if (other.constructed()) { + new (&get()) T(other.get()); + this->setConstructed(true); + } else { + ; // we're good + } + } + return *this; + } + + // move assignment + Optional& operator=(Optional&& other) { + if (this->constructed()) { + if (other.constructed()) { + get() = std::move(other.get()); + } else { + destruct(); + this->setConstructed(false); + } + } else { + if (other.constructed()) { + new (&get()) T(std::move(other.get())); + this->setConstructed(true); + } else { + ; // we're good + } + } + return *this; + } + + // conversion assignment + template::type, T>> + Optional& operator=(const Optional& other) { + if (this->constructed()) { + if (other.constructed()) { + get() = other.get(); + } else { + destruct(); + this->setConstructed(false); + } + } else { + if (other.constructed()) { + new (&get()) T(other.get()); + this->setConstructed(true); + } else { + ; // we're good + } + } + return *this; + } + + // conversion move assignment + template::type, T>> + Optional& operator=(Optional&& other) { + if (this->constructed()) { + if (other.constructed()) { + get() = std::move(other.get()); + } else { + destruct(); + this->setConstructed(false); + } + } else { + if (other.constructed()) { + new (&get()) T(std::move(other.get())); + this->setConstructed(true); + } else { + ; // we're good + } + } + return *this; + } + + // the most complicated one: forwarding constructor for anything convertible + // to |T|, excluding the stuff implemented above explicitly + template::type>::value && + std::is_convertible::type, T>::value>> + Optional& operator=(U&& other) { + if (this->constructed()) { + get() = std::forward(other); + } else { + new (&get()) T(std::forward(other)); + this->setConstructed(true); + } + return *this; + } + + // Adopt value checkers from the parent + using base_flag::operator bool; + using base_flag::hasValue; + + T& value() { + assert(this->constructed()); + return get(); + } + constexpr const T& value() const { + assert(this->constructed()); + return get(); + } + + T* ptr() { return this->constructed() ? &get() : nullptr; } + constexpr const T* ptr() const { + return this->constructed() ? &get() : nullptr; + } + + // Value getter with fallback + template::type, T>> + constexpr T valueOr(U&& defaultValue) const { + return this->constructed() ? get() : std::move(defaultValue); + } + + // Pointer-like operators + T& operator*() { + assert(this->constructed()); + return get(); + } + constexpr const T& operator*() const { + assert(this->constructed()); + return get(); + } + + T* operator->() { + assert(this->constructed()); + return &get(); + } + constexpr const T* operator->() const { + assert(this->constructed()); + return &get(); + } + + ~Optional() { + if (this->constructed()) { + destruct(); + } + } + + void clear() { + if (this->constructed()) { + destruct(); + this->setConstructed(false); + } + } + + template::type, T>> + void reset(U&& u) { + *this = std::forward(u); + } + + // In-place construction with possible destruction of the old value + template + void emplace(Args&&... args) { + if (this->constructed()) { + destruct(); + } + new (&get()) T(std::forward(args)...); + this->setConstructed(true); + } + + // In-place construction with possible destruction of the old value + // initializer-list version + template>>> + void emplace(std::initializer_list il) { + if (this->constructed()) { + destruct(); + } + new (&get()) T(il); + this->setConstructed(true); + } + + private: + // A helper function to convert the internal raw storage to T& + constexpr const T& get() const { + return *reinterpret_cast( + reinterpret_cast(&this->mStorage)); + } + + // Same thing, mutable + T& get() { return const_cast(const_cast(this)->get()); } + + // Shortcut for a destructor call for the stored object + void destruct() { get().T::~T(); } +}; + +template +Optional::type> makeOptional(T&& t) { + return Optional::type>(std::forward(t)); +} + +template +Optional::type> makeOptional(Args&&... args) { + return Optional::type>(kInplace, + std::forward(args)...); +} + +template +bool operator==(const Optional& l, const Optional& r) { + return l.hasValue() ? r.hasValue() && *l == *r : !r.hasValue(); +} +template +bool operator==(const Optional& l, NulloptT) { + return !l; +} +template +bool operator==(NulloptT, const Optional& r) { + return !r; +} +template +bool operator==(const Optional& l, const T& r) { + return bool(l) && *l == r; +} +template +bool operator==(const T& l, const Optional& r) { + return bool(r) && l == *r; +} + +template +bool operator!=(const Optional& l, const Optional& r) { + return !(l == r); +} +template +bool operator!=(const Optional& l, NulloptT) { + return bool(l); +} +template +bool operator!=(NulloptT, const Optional& r) { + return bool(r); +} +template +bool operator!=(const Optional& l, const T& r) { + return !l || !(*l == r); +} +template +bool operator!=(const T& l, const Optional& r) { + return !r || !(l == *r); +} + +template +bool operator<(const Optional& l, const Optional& r) { + return !r ? false : (!l ? true : *l < *r); +} +template +bool operator<(const Optional&, NulloptT) { + return false; +} +template +bool operator<(NulloptT, const Optional& r) { + return bool(r); +} +template +bool operator<(const Optional& l, const T& r) { + return !l || *l < r; +} +template +bool operator<(const T& l, const Optional& r) { + return bool(r) && l < *r; +} + +} // namespace base +} // namespace astc_codec + +#endif // ASTC_CODEC_BASE_OPTIONAL_H_ diff --git a/src/base/string_utils.h b/src/base/string_utils.h new file mode 100644 index 0000000..c450b27 --- /dev/null +++ b/src/base/string_utils.h @@ -0,0 +1,68 @@ +// 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. + +#ifndef ASTC_CODEC_BASE_STRING_UTILS_H_ +#define ASTC_CODEC_BASE_STRING_UTILS_H_ + +#include +#include + +namespace astc_codec { +namespace base { + +// Iterates over a string's parts using |splitBy| as a delimiter. +// |splitBy| must be a nonempty string well, or it's a no-op. +// Otherwise, |func| is called on each of the splits, excluding the +// characters that are part of |splitBy|. If two |splitBy|'s occur in a row, +// |func| will be called on a StringView("") in between. See +// StringUtils_unittest.cpp for the full story. +template +void Split(const std::string& str, const std::string& splitBy, Func func) { + if (splitBy.empty()) { + return; + } + + size_t splitSize = splitBy.size(); + size_t begin = 0; + size_t end = str.find(splitBy); + + while (true) { + func(str.substr(begin, end - begin)); + if (end == std::string::npos) { + return; + } + + begin = end + splitSize; + end = str.find(splitBy, begin); + } +} + +static int32_t ParseInt32(const char* str, int32_t deflt) { + using std::numeric_limits; + + char* error = nullptr; + int64_t value = strtol(str, &error, 0); + // Limit long values to int32 min/max. Needed for lp64; no-op on 32 bits. + if (value > std::numeric_limits::max()) { + value = std::numeric_limits::max(); + } else if (value < std::numeric_limits::min()) { + value = std::numeric_limits::min(); + } + return (error == str) ? deflt : static_cast(value); +} + +} // namespace base +} // namespace astc_codec + +#endif // ASTC_CODEC_BASE_STRING_UTILS_H_ diff --git a/src/base/test/bit_stream_test.cpp b/src/base/test/bit_stream_test.cpp new file mode 100644 index 0000000..0c4b3c9 --- /dev/null +++ b/src/base/test/bit_stream_test.cpp @@ -0,0 +1,141 @@ +// 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/base/bit_stream.h" + +#include + +namespace astc_codec { +namespace base { + +namespace { + static constexpr uint64_t kAllBits = 0xFFFFFFFFFFFFFFFF; + static constexpr uint64_t k40Bits = 0x000000FFFFFFFFFF; +} + +TEST(BitStream, Decode) { + { + BitStream stream(0, 1); + + uint64_t bits = kAllBits; + EXPECT_TRUE(stream.GetBits(1, &bits)); + EXPECT_EQ(bits, 0); + EXPECT_FALSE(stream.GetBits(1, &bits)); + } + + { + BitStream stream(0b1010101010101010, 32); + EXPECT_EQ(stream.Bits(), 32); + + uint64_t bits = 0; + EXPECT_TRUE(stream.GetBits(1, &bits)); + EXPECT_EQ(bits, 0); + + EXPECT_TRUE(stream.GetBits(3, &bits)); + EXPECT_EQ(bits, 0b101); + + EXPECT_TRUE(stream.GetBits(8, &bits)); + EXPECT_EQ(bits, 0b10101010); + + EXPECT_EQ(stream.Bits(), 20); + + EXPECT_TRUE(stream.GetBits(20, &bits)); + EXPECT_EQ(bits, 0b1010); + EXPECT_EQ(stream.Bits(), 0); + } + + { + BitStream stream(kAllBits, 64); + EXPECT_EQ(stream.Bits(), 64); + + uint64_t bits = 0; + EXPECT_TRUE(stream.GetBits(64, &bits)); + EXPECT_EQ(bits, kAllBits); + EXPECT_EQ(stream.Bits(), 0); + } + + { + BitStream stream(kAllBits, 64); + EXPECT_EQ(stream.Bits(), 64); + + uint64_t bits = 0; + EXPECT_TRUE(stream.GetBits(40, &bits)); + EXPECT_EQ(bits, k40Bits); + EXPECT_EQ(stream.Bits(), 24); + } + + { + BitStream stream(kAllBits, 32); + + uint64_t bits = 0; + EXPECT_TRUE(stream.GetBits(0, &bits)); + EXPECT_EQ(bits, 0); + EXPECT_TRUE(stream.GetBits(32, &bits)); + EXPECT_EQ(bits, k40Bits & 0xFFFFFFFF); + EXPECT_TRUE(stream.GetBits(0, &bits)); + EXPECT_EQ(bits, 0); + EXPECT_EQ(stream.Bits(), 0); + } +} + +TEST(BitStream, Encode) { + { + BitStream stream; + + stream.PutBits(0, 1); + stream.PutBits(0b11, 2); + EXPECT_EQ(stream.Bits(), 3); + + uint64_t bits = 0; + EXPECT_TRUE(stream.GetBits(3, &bits)); + EXPECT_EQ(bits, 0b110); + } + + { + BitStream stream; + + uint64_t bits = 0; + stream.PutBits(kAllBits, 64); + EXPECT_EQ(stream.Bits(), 64); + + EXPECT_TRUE(stream.GetBits(64, &bits)); + EXPECT_EQ(bits, kAllBits); + EXPECT_EQ(stream.Bits(), 0); + } + + { + BitStream stream; + stream.PutBits(kAllBits, 40); + + uint64_t bits = 0; + EXPECT_TRUE(stream.GetBits(40, &bits)); + EXPECT_EQ(bits, k40Bits); + EXPECT_EQ(stream.Bits(), 0); + } + + { + BitStream stream; + stream.PutBits(0, 0); + stream.PutBits(kAllBits, 32); + stream.PutBits(0, 0); + + uint64_t bits = 0; + EXPECT_TRUE(stream.GetBits(32, &bits)); + EXPECT_EQ(bits, k40Bits & 0xFFFFFFFF); + EXPECT_EQ(stream.Bits(), 0); + } +} + +} // namespace base +} // namespace astc_codec diff --git a/src/base/test/bottom_n_test.cpp b/src/base/test/bottom_n_test.cpp new file mode 100644 index 0000000..8a48d30 --- /dev/null +++ b/src/base/test/bottom_n_test.cpp @@ -0,0 +1,99 @@ +// 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/base/bottom_n.h" + +#include +#include + +namespace astc_codec { +namespace base { + +using ::testing::ElementsAre; + +template +static void pushAll(BottomN& heap, const T (&arr)[N]) { + for (auto i : arr) { + heap.Push(i); + } +} + +TEST(BottomN, Sort) { + { + BottomN heap(10); + EXPECT_TRUE(heap.Empty()); + pushAll(heap, {1, 2}); + + EXPECT_EQ(heap.Size(), 2); + EXPECT_FALSE(heap.Empty()); + EXPECT_THAT(heap.Pop(), ElementsAre(1, 2)); + } + + { + BottomN heap(6); + pushAll(heap, {1, 4, 3, 2, 2, 1}); + + EXPECT_EQ(heap.Size(), 6); + EXPECT_THAT(heap.Pop(), ElementsAre(1, 1, 2, 2, 3, 4)); + } +} + +TEST(BottomN, Bounds) { + { + BottomN heap(4); + pushAll(heap, {1, 2, 3, 4}); + EXPECT_EQ(heap.Size(), 4); + + heap.Push(0); + EXPECT_EQ(heap.Size(), 4); + + EXPECT_THAT(heap.Pop(), ElementsAre(0, 1, 2, 3)); + } + + { + BottomN heap(4); + pushAll(heap, {4, 3, 2, 1}); + EXPECT_EQ(heap.Size(), 4); + + pushAll(heap, {4, 4, 4, 4}); + EXPECT_EQ(heap.Size(), 4); + + EXPECT_THAT(heap.Pop(), ElementsAre(1, 2, 3, 4)); + } + + { + BottomN heap(4); + pushAll(heap, {4, 3, 2, 1}); + EXPECT_EQ(heap.Size(), 4); + + pushAll(heap, {5, 5, 5, 5}); + EXPECT_EQ(heap.Size(), 4); + + EXPECT_THAT(heap.Pop(), ElementsAre(1, 2, 3, 4)); + } + + { + BottomN heap(4); + pushAll(heap, {4, 3, 2, 1}); + EXPECT_EQ(heap.Size(), 4); + + pushAll(heap, {0, 0, 0, 0}); + EXPECT_EQ(heap.Size(), 4); + + EXPECT_THAT(heap.Pop(), ElementsAre(0, 0, 0, 0)); + } +} + +} // namespace base +} // namespace astc_codec diff --git a/src/base/test/math_utils_test.cpp b/src/base/test/math_utils_test.cpp new file mode 100644 index 0000000..0371e11 --- /dev/null +++ b/src/base/test/math_utils_test.cpp @@ -0,0 +1,78 @@ +// 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/base/math_utils.h" + +#include + +namespace astc_codec { +namespace base { + +TEST(MathUtils, Log2Floor) { + EXPECT_EQ(-1, Log2Floor(0)); + + for (int i = 0; i < 32; i++) { + uint32_t n = 1U << i; + EXPECT_EQ(i, Log2Floor(n)); + if (n > 2) { + EXPECT_EQ(i - 1, Log2Floor(n - 1)); + EXPECT_EQ(i, Log2Floor(n + 1)); + } + } +} + +TEST(MathUtils, CountOnes) { + EXPECT_EQ(0, CountOnes(0)); + EXPECT_EQ(1, CountOnes(1)); + EXPECT_EQ(32, CountOnes(static_cast(~0U))); + EXPECT_EQ(1, CountOnes(0x8000000)); + + for (int i = 0; i < 32; i++) { + EXPECT_EQ(1, CountOnes(1U << i)); + EXPECT_EQ(31, CountOnes(static_cast(~0U) ^ (1U << i))); + } +} + +TEST(MathUtils, ReverseBits) { + EXPECT_EQ(ReverseBits(0u), 0u); + EXPECT_EQ(ReverseBits(1u), 1u << 31); + EXPECT_EQ(ReverseBits(0xffffffff), 0xffffffff); + EXPECT_EQ(ReverseBits(0x00000001), 0x80000000); + EXPECT_EQ(ReverseBits(0x80000000), 0x00000001); + EXPECT_EQ(ReverseBits(0xaaaaaaaa), 0x55555555); + EXPECT_EQ(ReverseBits(0x55555555), 0xaaaaaaaa); + EXPECT_EQ(ReverseBits(0x7d5d7f53), 0xcafebabe); + EXPECT_EQ(ReverseBits(0xcafebabe), 0x7d5d7f53); +} + +TEST(MathUtils, GetBits) { + EXPECT_EQ(GetBits(0u, 0, 1), 0u); + EXPECT_EQ(GetBits(0u, 0, 32), 0u); + EXPECT_EQ(GetBits(0x00000001u, 0, 1), 0x00000001); + EXPECT_EQ(GetBits(0x00000001u, 0, 32), 0x00000001); + EXPECT_EQ(GetBits(0x00000001u, 1, 31), 0x00000000); + EXPECT_EQ(GetBits(0x00000001u, 31, 1), 0x00000000); + + EXPECT_DEBUG_DEATH(GetBits(0x00000000u, 1, 32), ""); + EXPECT_DEBUG_DEATH(GetBits(0x00000000u, 32, 0), ""); + EXPECT_DEBUG_DEATH(GetBits(0x00000000u, 32, 1), ""); + + EXPECT_EQ(GetBits(0XFFFFFFFFu, 0, 4), 0x0000000F); + EXPECT_EQ(GetBits(0XFFFFFFFFu, 16, 16), 0xFFFF); + EXPECT_EQ(GetBits(0x80000000u, 31, 1), 1); + EXPECT_EQ(GetBits(0xCAFEBABEu, 24, 8), 0xCA); +} + +} // namespace base +} // namespace astc_codec diff --git a/src/base/test/optional_test.cpp b/src/base/test/optional_test.cpp new file mode 100644 index 0000000..1eeefbd --- /dev/null +++ b/src/base/test/optional_test.cpp @@ -0,0 +1,481 @@ +// 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/base/optional.h" + +#include + +#include +#include + +namespace astc_codec { +namespace base { + +TEST(Optional, TypeProperties) { + // Making sure optional has the correct alignment and doesn't waste too much + // space + + static_assert(sizeof(Optional) == 2, "bad Optional size"); + static_assert(std::alignment_of>::value == + std::alignment_of::value, + "bad Optional alignment"); + + static_assert(sizeof(Optional) == 2, "bad Optional size"); + static_assert(std::alignment_of>::value == + std::alignment_of::value, + "bad Optional alignment"); + + static_assert(sizeof(Optional) == 4, "bad Optional size"); + static_assert(std::alignment_of>::value == + std::alignment_of::value, + "bad Optional alignment"); + + static_assert(sizeof(Optional) == 8, "bad Optional size"); + static_assert(std::alignment_of>::value == + std::alignment_of::value, + "bad Optional alignment"); + + static_assert(sizeof(Optional) == 16, "bad Optional size"); + static_assert(std::alignment_of>::value == + std::alignment_of::value, + "bad Optional alignment"); + + struct S128 { + int64_t data[2]; + }; + + static_assert(sizeof(Optional) == 3 * sizeof(int64_t), + "bad Optional size"); + static_assert(std::alignment_of>::value == + std::alignment_of::value, + "bad Optional alignment"); +} + +TEST(Optional, ConstructFromValue) { + { + Optional o; + EXPECT_FALSE(o); + } + { + Optional o = {}; + EXPECT_FALSE(o); + } + { + Optional o = kNullopt; + EXPECT_FALSE(o); + } + { + Optional o(1); + EXPECT_TRUE(o); + EXPECT_EQ(1, *o); + } + { + // check the std::decay<> constructor + Optional o = static_cast(1); + EXPECT_TRUE(o); + EXPECT_EQ(1, *o); + } + { + Optional o = 1; + EXPECT_TRUE(o); + EXPECT_EQ(1, *o); + } + { + Optional o{1}; + EXPECT_TRUE(o); + EXPECT_EQ(1, *o); + } + { + short val = 10; + Optional o = val; + EXPECT_TRUE(o); + EXPECT_EQ(10, *o); + } + { + Optional> o(kInplace, 10); + EXPECT_TRUE(o); + EXPECT_EQ((std::vector(10)), *o); + } + { + Optional> o(kInplace, {1, 2, 3, 4}); + EXPECT_TRUE(o); + EXPECT_EQ((std::vector{1, 2, 3, 4}), *o); + } +} + +TEST(Optional, ConstructFromOptional) { + { + Optional o = Optional(); + EXPECT_FALSE(o); + } + { + Optional o2; + Optional o(o2); + EXPECT_FALSE(o); + } + { + Optional o2 = 42; + Optional o(o2); + EXPECT_TRUE(o); + EXPECT_EQ(42, *o); + } + { + Optional o(Optional(1)); + EXPECT_TRUE(o); + EXPECT_EQ(1, *o); + } + { + Optional o2 = 2; + Optional o = o2; + EXPECT_TRUE(o); + EXPECT_EQ(2, *o); + } + { + Optional> o2 = std::vector{20, 30, 40}; + Optional> o = o2; + EXPECT_TRUE(o); + EXPECT_EQ((std::vector{20, 30, 40}), *o); + } +} + +TEST(Optional, Assign) { + { + Optional o; + o = 1; + EXPECT_TRUE(o); + EXPECT_EQ(1, *o); + + o = 2; + EXPECT_TRUE(o); + EXPECT_EQ(2, *o); + + o = kNullopt; + EXPECT_FALSE(o); + + o = Optional(10); + EXPECT_TRUE(o); + EXPECT_EQ(10, *o); + + Optional o2; + o = o2; + EXPECT_FALSE(o); + + o = 2u; + EXPECT_TRUE(o); + EXPECT_EQ(2, *o); + + o = Optional(); + EXPECT_FALSE(o); + + o = Optional(20); + EXPECT_TRUE(o); + EXPECT_EQ(20, *o); + + Optional o3(200); + o = o3; + EXPECT_TRUE(o); + EXPECT_EQ(200, *o); + + o = {}; + EXPECT_FALSE(o); + + // check the std::decay<> assignment + o = static_cast(1); + EXPECT_TRUE(o); + EXPECT_EQ(1, *o); + } +} + +TEST(Optional, MakeOptional) { + { + auto o = makeOptional(1); + static_assert(std::is_same>::value, + "Bad type deduction in makeOptional()"); + EXPECT_TRUE(o); + EXPECT_EQ(1, *o); + } + { + auto o = makeOptional(std::vector{'1', '2'}); + static_assert(std::is_same>>::value, + "Bad type deduction in makeOptional()"); + EXPECT_TRUE(o); + EXPECT_EQ((std::vector{'1', '2'}), *o); + } + { + // check std::decay<> in the factory function + auto o = makeOptional("String"); + static_assert(std::is_same>::value, + "Bad type deduction in makeOptional()"); + EXPECT_TRUE(o); + EXPECT_STREQ("String", *o); + } + { + auto o = makeOptional("String"); + static_assert(std::is_same>::value, + "Bad type deduction in makeOptional()"); + EXPECT_TRUE(o); + EXPECT_STREQ("String", o->c_str()); + } + { + auto o = makeOptional(5, 'b'); + static_assert(std::is_same>::value, + "Bad type deduction in makeOptional()"); + EXPECT_TRUE(o); + EXPECT_STREQ("bbbbb", o->c_str()); + } + { + auto o = makeOptional(); + static_assert(std::is_same>::value, + "Bad type deduction in makeOptional()"); + EXPECT_TRUE(o); + EXPECT_STREQ("", o->c_str()); + } +} + +TEST(Optional, Move) { + auto o = makeOptional(std::unique_ptr(new int(10))); + { + decltype(o) o2 = std::move(o); + EXPECT_TRUE(o); + EXPECT_TRUE(o2); + EXPECT_FALSE(bool(*o)); + EXPECT_TRUE(bool(*o2)); + EXPECT_EQ(10, **o2); + + decltype(o) o3; + o3 = std::move(o2); + EXPECT_TRUE(o2); + EXPECT_TRUE(o3); + EXPECT_FALSE(bool(*o2)); + EXPECT_TRUE(bool(*o3)); + EXPECT_EQ(10, **o3); + + o3 = std::move(o2); + EXPECT_TRUE(o2); + EXPECT_TRUE(o3); + EXPECT_FALSE(bool(*o2)); + EXPECT_FALSE(bool(*o3)); + } + + { + decltype(o) o1; + decltype(o) o2 = std::move(o1); + EXPECT_FALSE(o1); + EXPECT_FALSE(o2); + + o2 = std::move(o1); + EXPECT_FALSE(o1); + EXPECT_FALSE(o2); + + decltype(o) o3{kInplace, new int(20)}; + o3 = std::move(o1); + EXPECT_FALSE(o1); + EXPECT_FALSE(o3); + } +} + +TEST(Optional, Value) { + auto o = makeOptional(1); + EXPECT_EQ(1, o.value()); + EXPECT_EQ(1, o.valueOr(2)); + + o = kNullopt; + EXPECT_EQ(2, o.valueOr(2)); +} + +TEST(Optional, Clear) { + auto o = makeOptional(1); + o.clear(); + EXPECT_FALSE(o); + + o.clear(); + EXPECT_FALSE(o); +} + +TEST(Optional, Emplace) { + auto o = makeOptional(std::vector{1, 2, 3, 4}); + o.emplace(3, 1); + EXPECT_TRUE(o); + EXPECT_EQ((std::vector{1, 1, 1}), *o); + EXPECT_EQ(3U, o->capacity()); + + o.clear(); + o.emplace({1, 2}); + EXPECT_TRUE(o); + EXPECT_EQ((std::vector{1, 2}), *o); + EXPECT_EQ(2U, o->capacity()); +} + +TEST(Optional, Reset) { + auto o = makeOptional(std::vector{1, 2, 3, 4}); + o.reset(std::vector{4, 3}); + EXPECT_TRUE(o); + EXPECT_EQ((std::vector{4, 3}), *o); + EXPECT_EQ(2U, o->capacity()); + + o.clear(); + o.reset(std::vector{1}); + EXPECT_EQ((std::vector{1}), *o); + EXPECT_EQ(1U, o->capacity()); +} + +TEST(Optional, CompareEqual) { + EXPECT_TRUE(makeOptional(1) == makeOptional(1)); + EXPECT_TRUE(makeOptional(1) == 1); + EXPECT_TRUE(1 == makeOptional(1)); + EXPECT_FALSE(makeOptional(1) == makeOptional(2)); + EXPECT_FALSE(makeOptional(2) == 1); + EXPECT_FALSE(2 == makeOptional(1)); + EXPECT_TRUE(makeOptional(1) != makeOptional(2)); + EXPECT_TRUE(makeOptional(1) != 2); + EXPECT_TRUE(1 != makeOptional(2)); + + EXPECT_FALSE(makeOptional(1) == kNullopt); + EXPECT_FALSE(makeOptional(1) == Optional()); + EXPECT_FALSE(kNullopt == makeOptional(1)); + EXPECT_FALSE(Optional() == makeOptional(1)); + EXPECT_TRUE(makeOptional(1) != kNullopt); + EXPECT_TRUE(makeOptional(1) != Optional()); + EXPECT_TRUE(kNullopt != makeOptional(1)); + EXPECT_TRUE(Optional() != makeOptional(1)); + + EXPECT_TRUE(kNullopt == Optional()); + EXPECT_TRUE(kNullopt == Optional()); + EXPECT_FALSE(kNullopt != Optional()); + EXPECT_FALSE(kNullopt != Optional()); + EXPECT_TRUE(Optional() == Optional()); + EXPECT_FALSE(Optional() != Optional()); +} + +TEST(Optional, CompareLess) { + EXPECT_TRUE(makeOptional(1) < makeOptional(2)); + EXPECT_TRUE(1 < makeOptional(2)); + EXPECT_TRUE(makeOptional(1) < 2); + + EXPECT_FALSE(makeOptional(1) < makeOptional(1)); + EXPECT_FALSE(1 < makeOptional(1)); + EXPECT_FALSE(makeOptional(1) < 1); + EXPECT_FALSE(makeOptional(2) < makeOptional(1)); + EXPECT_FALSE(2 < makeOptional(1)); + EXPECT_FALSE(makeOptional(2) < 1); + + EXPECT_TRUE(kNullopt < makeOptional(2)); + EXPECT_TRUE(Optional() < makeOptional(2)); + EXPECT_TRUE(Optional() < 2); + EXPECT_FALSE(makeOptional(2) < kNullopt); + EXPECT_FALSE(makeOptional(2) < Optional()); + EXPECT_FALSE(2 < Optional()); + + EXPECT_FALSE(kNullopt < Optional()); + EXPECT_FALSE(Optional() < kNullopt); +} + +TEST(Optional, Destruction) { + // create a reference counting class to check if we delete everything + // we've created + struct Track { + Track(int& val) : mVal(val) { ++mVal.get(); } + Track(std::initializer_list vals) : mVal(**vals.begin()) { + ++mVal.get(); + } + Track(const Track& other) : mVal(other.mVal) { ++mVal.get(); } + Track(Track&& other) : mVal(other.mVal) { ++mVal.get(); } + Track& operator=(const Track& other) { + --mVal.get(); + mVal = other.mVal; + ++mVal.get(); + return *this; + } + Track& operator=(Track&& other) { + --mVal.get(); + mVal = other.mVal; + ++mVal.get(); + return *this; + } + + ~Track() { --mVal.get(); } + + std::reference_wrapper mVal; + }; + + int counter = 0; + { + auto o = makeOptional(Track(counter)); + EXPECT_EQ(1, counter); + } + EXPECT_EQ(0, counter); + + { + auto o = makeOptional(Track(counter)); + EXPECT_EQ(1, counter); + o.clear(); + EXPECT_EQ(0, counter); + } + EXPECT_EQ(0, counter); + + { + auto o = makeOptional(Track(counter)); + EXPECT_EQ(1, counter); + int counter2 = 0; + o.emplace(counter2); + EXPECT_EQ(0, counter); + EXPECT_EQ(1, counter2); + o = Track(counter); + EXPECT_EQ(1, counter); + EXPECT_EQ(0, counter2); + + auto o2 = o; + EXPECT_EQ(2, counter); + EXPECT_EQ(0, counter2); + } + EXPECT_EQ(0, counter); + + { + auto o = makeOptional(Track(counter)); + auto o2 = std::move(o); + EXPECT_EQ(2, counter); + o = o2; + EXPECT_EQ(2, counter); + } + EXPECT_EQ(0, counter); + + int counter2 = 0; + { + Optional o; + o.emplace(counter); + EXPECT_EQ(1, counter); + + o.emplace(counter2); + EXPECT_EQ(0, counter); + EXPECT_EQ(1, counter2); + } + EXPECT_EQ(0, counter); + EXPECT_EQ(0, counter2); + + { + Optional o; + o.emplace({&counter}); + EXPECT_EQ(1, counter); + + counter2 = 0; + o.emplace({&counter2}); + EXPECT_EQ(0, counter); + EXPECT_EQ(1, counter2); + } + EXPECT_EQ(0, counter); + EXPECT_EQ(0, counter2); +} + +} // namespace base +} // namespace astc_codec diff --git a/src/base/test/string_utils_test.cpp b/src/base/test/string_utils_test.cpp new file mode 100644 index 0000000..209da54 --- /dev/null +++ b/src/base/test/string_utils_test.cpp @@ -0,0 +1,110 @@ +// 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/base/string_utils.h" + +#include + +#include +#include +#include + +namespace astc_codec { +namespace base { + +TEST(StringUtils, Split) { + std::vector results; + + auto testFunc = [&results](std::string&& s) { + results.push_back(std::move(s)); + }; + + Split("", "abc", testFunc); + EXPECT_EQ(results.size(), 1); + + Split("abc", "", testFunc); + EXPECT_EQ(results.size(), 1); + + results.clear(); + Split("abc", "a", testFunc); + EXPECT_EQ(results.size(), 2); + EXPECT_EQ(results[0], ""); + EXPECT_EQ(results[1], "bc"); + + results.clear(); + Split("aaa", "a", testFunc); + EXPECT_EQ(4, results.size()); + EXPECT_EQ("", results[0]); + EXPECT_EQ("", results[1]); + EXPECT_EQ("", results[2]); + EXPECT_EQ("", results[3]); + + results.clear(); + Split("1a2a3a4", "a", testFunc); + EXPECT_EQ(4, results.size()); + EXPECT_EQ("1", results[0]); + EXPECT_EQ("2", results[1]); + EXPECT_EQ("3", results[2]); + EXPECT_EQ("4", results[3]); + + results.clear(); + Split("1a2aa3a4", "a", testFunc); + EXPECT_EQ(5, results.size()); + EXPECT_EQ("1", results[0]); + EXPECT_EQ("2", results[1]); + EXPECT_EQ("", results[2]); + EXPECT_EQ("3", results[3]); + EXPECT_EQ("4", results[4]); + + results.clear(); + Split("The quick brown fox jumped over the lazy dog", + " ", testFunc); + EXPECT_EQ(9, results.size()); + EXPECT_EQ("The", results[0]); + EXPECT_EQ("quick", results[1]); + EXPECT_EQ("brown", results[2]); + EXPECT_EQ("fox", results[3]); + EXPECT_EQ("jumped", results[4]); + EXPECT_EQ("over", results[5]); + EXPECT_EQ("the", results[6]); + EXPECT_EQ("lazy", results[7]); + EXPECT_EQ("dog", results[8]); + + results.clear(); + Split("a; b; c; d", "; ", testFunc); + EXPECT_EQ(4, results.size()); + EXPECT_EQ("a", results[0]); + EXPECT_EQ("b", results[1]); + EXPECT_EQ("c", results[2]); + EXPECT_EQ("d", results[3]); +} + +TEST(StringUtils, ParseInt32) { + EXPECT_EQ(ParseInt32("0", -1), 0); + EXPECT_EQ(ParseInt32("100", -1), 100); + EXPECT_EQ(ParseInt32("-100", -1), -100); + + EXPECT_EQ(ParseInt32("", -1), -1); + EXPECT_EQ(ParseInt32("a", -1), -1); + EXPECT_EQ(ParseInt32("10x1", -1), 10); + + EXPECT_EQ(ParseInt32("2147483647", -1), 2147483647); + EXPECT_EQ(ParseInt32("2147483648", -1), 2147483647); + + EXPECT_EQ(ParseInt32("-2147483648", -1), -2147483648); + EXPECT_EQ(ParseInt32("-2147483649", -1), -2147483648); +} + +} // namespace base +} // namespace astc_codec diff --git a/src/base/test/type_traits_test.cpp b/src/base/test/type_traits_test.cpp new file mode 100644 index 0000000..b858c01 --- /dev/null +++ b/src/base/test/type_traits_test.cpp @@ -0,0 +1,128 @@ +// 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/base/type_traits.h" + +#include + +#include +#include +#include +#include + +namespace astc_codec { +namespace base { + +TEST(TypeTraits, IsCallable) { + class C; + C* c = nullptr; + + auto lambda = [c](bool) -> C* { return nullptr; }; + + static_assert(is_callable_as::value, "simple function"); + static_assert(is_callable_as::value, + "function reference"); + static_assert(is_callable_as::value, "function pointer"); + static_assert(is_callable_as::value, + "function with arguments and return type"); + static_assert(is_callable_as::value, "lambda"); + static_assert(is_callable_as, bool(int)>::value, + "std::function"); + + static_assert(!is_callable_as::value, + "int should not be callable"); + static_assert(!is_callable_as::value, "incomplete type"); + static_assert(!is_callable_as::value, + "different arguments"); + static_assert(!is_callable_as::value, + "different return types"); + static_assert(!is_callable_as::value, + "slightly different return types"); + static_assert(!is_callable_as::value, + "more arguments"); + static_assert(!is_callable_as::value, + "less arguments"); + + static_assert(!is_callable_as::value, + "bad required signature"); + + static_assert(is_callable_with_args::value, + "simple function"); + static_assert(is_callable_with_args::value, + "function reference"); + static_assert(is_callable_with_args::value, + "function pointer"); + static_assert(is_callable_with_args::value, + "function with arguments and return type"); + static_assert(is_callable_with_args::value, + "lambda"); + static_assert( + is_callable_with_args, bool(int)>::value, + "std::function"); + + static_assert(!is_callable_with_args::value, + "int should not be callable"); + static_assert(!is_callable_with_args::value, "incomplete type"); + static_assert(!is_callable_with_args::value, + "different arguments"); + static_assert(is_callable_with_args::value, + "different return types are ignored"); + static_assert(is_callable_with_args::value, + "slightly different return types are ignored"); + static_assert(!is_callable_with_args::value, + "more arguments"); + static_assert(!is_callable_with_args::value, + "less arguments"); + + static_assert(!is_callable_with_args::value, + "bad required signature"); +} + +TEST(TypeTraits, IsTemplateInstantiation) { + static_assert(!is_template_instantiation_of::value, + "int is not an instance of vector"); + static_assert(!is_template_instantiation_of>, + std::vector>::value, + "list is not an instance of vector"); + + static_assert( + is_template_instantiation_of, std::vector>::value, + "std::vector is an instance of vector"); + static_assert( + is_template_instantiation_of>>, + std::vector>::value, + "nested std::vector<> is an instance of vector"); +} + +TEST(TypeTraits, IsRange) { + static_assert(is_range>::value, + "vector<> should be detected as a range"); + static_assert(is_range>>::value, + "const list<> should be detected as a range"); + static_assert(is_range, 10>>::value, + "array<> should be detected as a range"); + char arr[100]; + static_assert(is_range::value, + "C array should be detected as a range"); + static_assert(is_range::value, + "String literal should be detected as a range"); + + static_assert(!is_range::value, "int shouldn't be a range"); + static_assert(!is_range::value, "int* shouldn't be a range"); + static_assert(!is_range::value, + "even const int* shouldn't be a range"); +} + +} // namespace base +} // namespace astc_codec diff --git a/src/base/test/uint128_test.cpp b/src/base/test/uint128_test.cpp new file mode 100644 index 0000000..0a52244 --- /dev/null +++ b/src/base/test/uint128_test.cpp @@ -0,0 +1,140 @@ +// 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/base/uint128.h" + +#include + +namespace astc_codec { +namespace base { + +TEST(UInt128, Equality) { + const UInt128 zero(0); + const UInt128 max64(~0ULL); + + EXPECT_EQ(zero, zero); + EXPECT_NE(zero, max64); + EXPECT_EQ(zero, UInt128(0)); + EXPECT_NE(zero, UInt128(1)); + EXPECT_EQ(max64, max64); +} + +TEST(UInt128, Shifting) { + const UInt128 max64(~0ULL); + const UInt128 upper64(~0ULL, 0); + EXPECT_EQ(upper64.HighBits(), ~0ULL); + EXPECT_EQ(upper64.LowBits(), 0); + + EXPECT_EQ(upper64 >> 64, max64); + + EXPECT_EQ(UInt128(1) << 1, UInt128(2)); + EXPECT_EQ(UInt128(0) << 0, UInt128(0)); + EXPECT_EQ(max64 << 0, max64); + EXPECT_EQ(max64 >> 0, max64); + EXPECT_EQ(upper64 << 0, upper64); + EXPECT_EQ(upper64 >> 0, upper64); + + { + const UInt128 bit63 = UInt128(1ULL << 62) << 1; + EXPECT_EQ(bit63.LowBits(), 1ULL << 63); + EXPECT_EQ(bit63.HighBits(), 0); + } + + { + const UInt128 bit64 = UInt128(1ULL << 63) << 1; + EXPECT_EQ(bit64.LowBits(), 0); + EXPECT_EQ(bit64.HighBits(), 1); + EXPECT_EQ(bit64 >> 1, UInt128(1ULL << 63)); + } + + { + const UInt128 overshift = max64 << 128; + EXPECT_EQ(overshift.HighBits(), 0); + EXPECT_EQ(overshift.LowBits(), 0); + } + + { + const UInt128 overlap = upper64 >> 32; + EXPECT_EQ(overlap.HighBits(), 0x00000000FFFFFFFF); + EXPECT_EQ(overlap.LowBits(), 0xFFFFFFFF00000000); + } + + { + const UInt128 overlap = max64 << 32; + EXPECT_EQ(overlap.HighBits(), 0x00000000FFFFFFFF); + EXPECT_EQ(overlap.LowBits(), 0xFFFFFFFF00000000); + } +} + +TEST(UInt128, LargeShift) { + const UInt128 base(0xFF); + EXPECT_EQ(base << 64, UInt128(0xFFULL, 0)); + EXPECT_EQ(base << 72, UInt128(0xFF00ULL, 0)); + EXPECT_EQ(base << 80, UInt128(0xFF0000ULL, 0)); + EXPECT_EQ(base << 88, UInt128(0xFF000000ULL, 0)); + EXPECT_EQ(base << 96, UInt128(0xFF00000000ULL, 0)); + EXPECT_EQ(base << 104, UInt128(0xFF0000000000ULL, 0)); + EXPECT_EQ(base << 112, UInt128(0xFF000000000000ULL, 0)); + EXPECT_EQ(base << 120, UInt128(0xFF00000000000000ULL, 0)); + + const UInt128 upper(0xFF00000000000000ULL, 0); + EXPECT_EQ(upper >> 64, UInt128(0, 0xFF00000000000000ULL)); + EXPECT_EQ(upper >> 72, UInt128(0, 0xFF000000000000ULL)); + EXPECT_EQ(upper >> 80, UInt128(0, 0xFF0000000000ULL)); + EXPECT_EQ(upper >> 88, UInt128(0, 0xFF00000000ULL)); + EXPECT_EQ(upper >> 96, UInt128(0, 0xFF000000ULL)); + EXPECT_EQ(upper >> 104, UInt128(0, 0xFF0000ULL)); + EXPECT_EQ(upper >> 112, UInt128(0, 0xFF00ULL)); + EXPECT_EQ(upper >> 120, UInt128(0, 0xFFULL)); +} + +TEST(UInt128, BooleanOperators) { + const UInt128 allOnes(~0ULL, ~0ULL); + EXPECT_EQ(allOnes.HighBits(), ~0ULL); + EXPECT_EQ(allOnes.LowBits(), ~0ULL); + + EXPECT_EQ(~allOnes, UInt128(0)); + EXPECT_EQ(~UInt128(0), allOnes); + + EXPECT_EQ(UInt128(0xFFFF00) & UInt128(0x00FFFF), UInt128(0x00FF00)); + EXPECT_EQ(UInt128(0xFFFF00) | UInt128(0x00FFFF), UInt128(0xFFFFFF)); + EXPECT_EQ(UInt128(0xFFFF00) ^ UInt128(0x00FFFF), UInt128(0xFF00FF)); +} + +TEST(UInt128, Addition) { + const UInt128 bit63(1ULL << 63); + + EXPECT_EQ(UInt128(1) + 1, UInt128(2)); + EXPECT_EQ(bit63 + bit63, UInt128(1) << 64); + + const UInt128 carryUp = UInt128(~0ULL) + 1; + EXPECT_EQ(carryUp.HighBits(), 1); + EXPECT_EQ(carryUp.LowBits(), 0); + + const UInt128 allOnes(~0ULL, ~0ULL); + EXPECT_EQ(allOnes + 1, UInt128(0)); +} + +TEST(UInt128, Subtraction) { + const UInt128 bit64 = UInt128(1) << 64; + EXPECT_EQ(bit64 - 1, UInt128(~0ULL)); + + EXPECT_EQ(UInt128(1) - 1, UInt128(0)); + + const UInt128 allOnes(~0ULL, ~0ULL); + EXPECT_EQ(UInt128(0) - 1, allOnes); +} + +} // namespace base +} // namespace astc_codec diff --git a/src/base/type_traits.h b/src/base/type_traits.h new file mode 100644 index 0000000..917125d --- /dev/null +++ b/src/base/type_traits.h @@ -0,0 +1,172 @@ +// 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. + +#ifndef ASTC_CODEC_BASE_TYPE_TRAITS_H_ +#define ASTC_CODEC_BASE_TYPE_TRAITS_H_ + +#include +#include + +namespace astc_codec { +namespace base { + +namespace details { + +// a simple helper class for SFINAE below. +template +struct dummy { + using type = X; +}; + +} // namespace details + +// add some convenience shortcuts for an overly complex std::enable_if syntax + +// Use 'enable_if' instead of +// 'typename std::enable_if::type' +template +using enable_if = typename std::enable_if::type; + +// Use 'enable_if_c' instead of +// 'typename std::enable_if::type' +template +using enable_if_c = typename std::enable_if::type; + +// Use 'enable_if_convertible' instead of +// 'typename std::enable_if::value, Type>::type' +template +using enable_if_convertible = enable_if>; + +// ----------------------------------------------------------------------------- +// A predicate for checking if some object is callable with a specific +// signature. Examples: +// +// is_callable_as::value == false. +// is_callable_as::value == false. +// is_callable_as::value == true +// +template +struct is_callable_as : std::false_type {}; + +// This specialization is SFINAE-d out if template arguments can't be combined +// into a call expression F(), or if the result of that call is not |R| +template +struct is_callable_as()( + std::declval()...))>::type, + R>::value>::type> : std::true_type {}; + +// +// A similar predicate to only check arguments of the function call and ignore +// the specified return type +// +// is_callable_as::value == true +// is_callable_as::value == false +// is_callable_with_args::value == true +// +template +struct is_callable_with_args : std::false_type {}; + +template +struct is_callable_with_args< + F, R(Args...), + typename std::enable_if< + !std::is_same()(std::declval()...))>::type, + F>::value>::type> : std::true_type {}; + +// ----------------------------------------------------------------------------- +// Check if a type |T| is any instantiation of a template |U|. Examples: +// +// is_template_instantiation_of::value == false +// is_template_instantiation_of< +// std::list>, std::vector>::value == false +// is_template_instantiation_of, std::vector>::value == true +// is_template_instantiation_of< +// std::vector>, std::vector>::value == true +// +template class U> +struct is_template_instantiation_of : std::false_type {}; + +template class U, class... Args> +struct is_template_instantiation_of, U> : std::true_type {}; +// ----------------------------------------------------------------------------- + +// +// is_range - check if type |T| is a range-like type. +// +// It makes sure that expressions std::begin(t) and std::end(t) are well-formed +// and those return the same type. +// +// Note: with expression SFINAE from C++14 is_range_helper<> could be renamed to +// is_range<> with no extra code. C++11 needs an extra level of enable_if<> +// to make it work when the type isn't a range. +// + +namespace details { + +template +using is_range_helper = std::is_same< + decltype(std::begin( + std::declval::type>())), + decltype( + std::end(std::declval::type>()))>; + +} // namespace details + +template +struct is_range : std::false_type {}; + +template +struct is_range< + T, typename std::enable_if::value>::type> + : std::true_type {}; + +//////////////////////////////////////////////////////////////////////////////// +// +// A class to incapsulate integer sequence 0, 1, ..., +// Seq +// Useful to pass function parameters in an array/tuple to call it later. +// + +template +struct Seq {}; + +// A 'maker' class to construct Seq given only +// value. +// MakeSeq works this way, e.g. +// +// MakeSeq<2> inherits MakeSeq<2 - 1, 2 - 1> == MakeSeq<1, 1> +// MakeSeq<1, 1> : MakeSeq<1 - 1, 1 - 1, 1> == MakeSeq<0, 0, 1> +// MakeSeq<0, 0, 1> == MakeSeq<0, S...> and defines |type| = Seq<0, 1> + +template +struct MakeSeq : MakeSeq {}; + +template +struct MakeSeq<0, S...> { + using type = Seq; +}; + +// +// MakeSeqT alias to quickly create Seq<...>: +// MakeSeqT<3> == Seq<0, 1, 2> +template +using MakeSeqT = typename MakeSeq::type; + +} // namespace base +} // namespace astc_codec + +#endif // ASTC_CODEC_BASE_TYPE_TRAITS_H_ diff --git a/src/base/uint128.h b/src/base/uint128.h new file mode 100644 index 0000000..481e4ea --- /dev/null +++ b/src/base/uint128.h @@ -0,0 +1,175 @@ +// 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. + +#ifndef ASTC_CODEC_BASE_UINT128_H_ +#define ASTC_CODEC_BASE_UINT128_H_ + +#include +#include + +namespace astc_codec { +namespace base { + +class UInt128 { + public: + UInt128() = default; + UInt128(uint64_t low) : low_(low) { } + UInt128(uint64_t high, uint64_t low) : low_(low), high_(high) { } + UInt128(const UInt128& other) : low_(other.low_), high_(other.high_) { } + + uint64_t LowBits() const { return low_; } + uint64_t HighBits() const { return high_; } + + // Allow explicit casts to uint64_t. + explicit operator uint64_t() const { return low_; } + + // Copy operators. + UInt128& operator=(const UInt128& other) { + high_ = other.high_; + low_ = other.low_; + return *this; + } + + // Equality operators. + bool operator==(const UInt128& other) const { + return high_ == other.high_ && low_ == other.low_; + } + + bool operator!=(const UInt128& other) const { + return high_ != other.high_ || low_ != other.low_; + } + + // Shifting. + UInt128& operator<<=(int shift) { + high_ = shift >= 64 ? (shift >= 128 ? 0 : low_ << (shift - 64)) + : high_ << shift; + + if (shift > 0 && shift < 64) { + const uint64_t overlappingBits = low_ >> (64 - shift); + high_ |= overlappingBits; + } + + low_ = shift >= 64 ? 0 : low_ << shift; + return *this; + } + + UInt128 operator<<(int shift) const { + UInt128 result = *this; + result <<= shift; + return result; + } + + UInt128& operator>>=(int shift) { + low_ = shift >= 64 ? (shift >= 128 ? 0 : high_ >> (shift - 64)) + : low_ >> shift; + + if (shift > 0 && shift < 64) { + const uint64_t overlappingBits = high_ << (64 - shift); + low_ |= overlappingBits; + } + + high_ = shift >= 64 ? 0 : high_ >> shift; + + return *this; + } + + UInt128 operator>>(int shift) const { + UInt128 result = *this; + result >>= shift; + return result; + } + + // Binary operations. + UInt128& operator|=(const UInt128& other) { + high_ |= other.high_; + low_ |= other.low_; + return *this; + } + + UInt128 operator|(const UInt128& other) const { + UInt128 result = *this; + result |= other; + return result; + } + + UInt128& operator&=(const UInt128& other) { + high_ &= other.high_; + low_ &= other.low_; + return *this; + } + + UInt128 operator&(const UInt128& other) const { + UInt128 result = *this; + result &= other; + return result; + } + + UInt128& operator^=(const UInt128& other) { + high_ ^= other.high_; + low_ ^= other.low_; + return *this; + } + + UInt128 operator^(const UInt128& other) const { + UInt128 result = *this; + result ^= other; + return result; + } + + UInt128 operator~() const { + UInt128 result = *this; + result.high_ = ~high_; + result.low_ = ~low_; + return result; + } + + // Addition/subtraction. + UInt128& operator+=(const UInt128& other) { + const uint64_t carry = + (((low_ & other.low_) & 1) + (low_ >> 1) + (other.low_ >> 1)) >> 63; + high_ += other.high_ + carry; + low_ += other.low_; + return *this; + } + + UInt128 operator+(const UInt128& other) const { + UInt128 result = *this; + result += other; + return result; + } + + UInt128& operator-=(const UInt128& other) { + low_ -= other.low_; + const uint64_t carry = + (((low_ & other.low_) & 1) + (low_ >> 1) + (other.low_ >> 1)) >> 63; + high_ -= other.high_ + carry; + return *this; + } + + UInt128 operator-(const UInt128& other) const { + UInt128 result = *this; + result -= other; + return result; + } + + private: + // TODO(google): Different order for little endian. + uint64_t low_ = 0; + uint64_t high_ = 0; +}; + +} // namespace base +} // namespace astc_codec + +#endif // ASTC_CODEC_BASE_UINT128_H_ diff --git a/src/decoder/BUILD.bazel b/src/decoder/BUILD.bazel new file mode 100644 index 0000000..f2fded5 --- /dev/null +++ b/src/decoder/BUILD.bazel @@ -0,0 +1,238 @@ +# 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. + +cc_library( + name = "footprint", + srcs = ["footprint.cc"], + hdrs = ["footprint.h"], + deps = [ + "//:api", + "//src/base", + ], +) + +cc_library( + name = "astc_utils", + srcs = [ + "astc_file.cc", + "endpoint_codec.cc", + "integer_sequence_codec.cc", + "intermediate_astc_block.cc", + "logical_astc_block.cc", + "partition.cc", + "physical_astc_block.cc", + "quantization.cc", + "weight_infill.cc", + ], + hdrs = [ + "astc_file.h", + "endpoint_codec.h", + "integer_sequence_codec.h", + "intermediate_astc_block.h", + "logical_astc_block.h", + "partition.h", + "physical_astc_block.h", + "quantization.h", + "types.h", + "weight_infill.h", + ], + deps = [ + ":footprint", + "//src/base", + ], +) + +cc_library( + name = "codec", + srcs = ["codec.cc"], + hdrs = ["codec.h"], + deps = [ + ":astc_utils", + ":footprint", + "//src/base", + ], + visibility = ["//:__pkg__"], +) + +cc_binary( + name = "astc_inspector_cli", + srcs = ["tools/astc_inspector_cli.cc"], + deps = [ + ":astc_utils", + "//src/base", + ], +) + +################################################################################ +## +## Testing +## +################################################################################ + +cc_library( + name = "test", + hdrs = ["test/image_utils.h"], + deps = [ + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "physical_astc_block_test", + size = "small", + srcs = ["test/physical_astc_block_test.cc"], + deps = [ + ":astc_utils", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "partition_test", + size = "small", + srcs = ["test/partition_test.cc"], + deps = [ + ":astc_utils", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "integer_sequence_codec_test", + size = "small", + srcs = ["test/integer_sequence_codec_test.cc"], + deps = [ + ":astc_utils", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "intermediate_astc_block_test", + size = "small", + srcs = ["test/intermediate_astc_block_test.cc"], + data = glob([ + "testdata/checkered_*.astc", + ]), + deps = [ + ":astc_utils", + ":test", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "quantization_test", + size = "medium", + srcs = ["test/quantization_test.cc"], + deps = [ + ":astc_utils", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "weight_infill_test", + size = "small", + srcs = ["test/weight_infill_test.cc"], + deps = [ + ":astc_utils", + ":footprint", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "endpoint_codec_test", + size = "small", + srcs = ["test/endpoint_codec_test.cc"], + data = [ + ":testdata/checkerboard.astc", + ], + deps = [ + ":astc_utils", + ":test", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "logical_astc_block_test", + size = "large", + srcs = ["test/logical_astc_block_test.cc"], + data = glob([ + "testdata/atlas_small_*.astc", + "testdata/atlas_small_*.bmp", + "testdata/footprint_*.astc", + "testdata/footprint_*.bmp", + "testdata/rgb_*.astc", + "testdata/rgb_*.bmp", + ]), + deps = [ + ":test", + ":astc_utils", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "codec_test", + size = "large", + srcs = ["test/codec_test.cc"], + data = glob([ + "testdata/atlas_small_*.astc", + "testdata/atlas_small_*.bmp", + ]), + deps = [ + ":test", + ":codec", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "footprint_test", + size = "small", + srcs = ["test/footprint_test.cc"], + deps = [ + ":footprint", + "@gtest//:gtest_main", + ], +) + +cc_test( + name = "astc_fuzzer", + srcs = ["test/astc_fuzzer.cc"], + copts = select({ + # Clang-only flags. TODO: Find a better way to detect GCC/clang. + "@bazel_tools//src/conditions:darwin_x86_64": [ + "-fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp", + "-fsanitize-coverage=bb", + ], + "@bazel_tools//src/conditions:darwin": [ + "-fsanitize-coverage=trace-pc-guard,indirect-calls,trace-cmp", + "-fsanitize-coverage=bb", + ], + # GCC-only flags. + "//conditions:default": [ + "-finstrument-functions" + ], + }), + deps = [ + ":codec", + "@honggfuzz//:honggfuzz", + "@benchmark//:benchmark", + ], + linkstatic = 1, +) diff --git a/src/decoder/astc_file.cc b/src/decoder/astc_file.cc new file mode 100644 index 0000000..4770064 --- /dev/null +++ b/src/decoder/astc_file.cc @@ -0,0 +1,185 @@ +// 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/astc_file.h" + +#include +#include +#include +#include + +namespace astc_codec { + +namespace { +static constexpr size_t kASTCHeaderSize = 16; + +// Reads a value of size T from the buffer at the current offset, then +// increments the offset. +template +inline T ReadVal(const char* file_data, size_t& offset) { + T x; + memcpy(&x, &file_data[offset], sizeof(T)); + offset += sizeof(T); + return x; +} +} // namespace + +ASTCFile::ASTCFile(Header&& header, std::string&& blocks) + : header_(std::move(header)), blocks_(std::move(blocks)) {} + +std::unique_ptr ASTCFile::LoadFromMemory(const char* data, + size_t length, + std::string* error) { + if (length < kASTCHeaderSize) { + *error = "Incomplete header."; + return nullptr; + } + + base::Optional
header_opt = ParseHeader(data); + if (!header_opt) { + *error = "Invalid ASTC header."; + return nullptr; + } + + Header header = header_opt.value(); + + if (header.block_width_ == 0 || header.block_height_ == 0) { + *error = "Invalid block size."; + return nullptr; + } + + std::string blocks(data + kASTCHeaderSize, data + length); + + // Check that this file has the expected number of blocks. + const size_t expected_block_count = + ((header.width_ + header.block_width_ - 1) / header.block_width_) * + ((header.height_ + header.block_height_ - 1) / header.block_height_); + + if (blocks.size() % PhysicalASTCBlock::kSizeInBytes != 0 || + blocks.size() / PhysicalASTCBlock::kSizeInBytes != expected_block_count) { + std::stringstream ss; + ss << "Unexpected file length " << blocks.size() << " expected " + << kASTCHeaderSize + + expected_block_count * PhysicalASTCBlock::kSizeInBytes + << " bytes."; + *error = ss.str(); + return nullptr; + } + + return std::unique_ptr( + new ASTCFile(std::move(header), std::move(blocks))); +} + +std::unique_ptr ASTCFile::LoadFile(const std::string& path, + std::string* error) { + std::ifstream is(path, std::ios::binary); + if (!is) { + *error = "File not found: " + path; + return nullptr; + } + + char header_data[kASTCHeaderSize] = {}; + if (!is.read(header_data, kASTCHeaderSize)) { + *error = "Failed to load ASTC header."; + return nullptr; + } + + base::Optional
header_opt = ParseHeader(header_data); + if (!header_opt) { + *error = "Invalid ASTC header."; + return nullptr; + } + + Header header = header_opt.value(); + + std::string blocks; + { + std::ostringstream ss; + ss << is.rdbuf(); + blocks = ss.str(); + } + + // Check that this file has the expected number of blocks. + const size_t expected_block_count = + ((header.width_ + header.block_width_ - 1) / header.block_width_) * + ((header.height_ + header.block_height_ - 1) / header.block_height_); + + if (blocks.size() % PhysicalASTCBlock::kSizeInBytes != 0 || + blocks.size() / PhysicalASTCBlock::kSizeInBytes != expected_block_count) { + std::stringstream ss; + ss << "Unexpected file length " << blocks.size() << " expected " + << kASTCHeaderSize + + expected_block_count * PhysicalASTCBlock::kSizeInBytes + << " bytes."; + *error = ss.str(); + return nullptr; + } + + return std::unique_ptr( + new ASTCFile(std::move(header), std::move(blocks))); +} + +base::Optional ASTCFile::GetFootprint() const { + return Footprint::FromDimensions(header_.block_width_, header_.block_height_); +} + +std::string ASTCFile::GetFootprintString() const { + std::stringstream footprint; + footprint << header_.block_width_ << "x" << header_.block_height_; + return footprint.str(); +} + +const std::string& ASTCFile::GetRawBlockData() const { + return blocks_; +} + +PhysicalASTCBlock ASTCFile::GetBlock(size_t block_idx) const { + const size_t sz = PhysicalASTCBlock::kSizeInBytes; + const size_t offset = PhysicalASTCBlock::kSizeInBytes * block_idx; + assert(offset <= blocks_.size() - sz); + return PhysicalASTCBlock(blocks_.substr(offset, sz)); +} + +base::Optional ASTCFile::ParseHeader(const char* header) { + size_t offset = 0; + // TODO(google): Handle endianness. + const uint32_t magic = ReadVal(header, offset); + if (magic != 0x5CA1AB13) { + return {}; + } + + const uint32_t block_width = ReadVal(header, offset); + const uint32_t block_height = ReadVal(header, offset); + const uint32_t block_depth = ReadVal(header, offset); + + uint32_t width = 0; + width |= ReadVal(header, offset); + width |= ReadVal(header, offset) << 8; + width |= ReadVal(header, offset) << 16; + + uint32_t height = 0; + height |= ReadVal(header, offset); + height |= ReadVal(header, offset) << 8; + height |= ReadVal(header, offset) << 16; + + uint32_t depth = 0; + depth |= ReadVal(header, offset); + depth |= ReadVal(header, offset) << 8; + depth |= ReadVal(header, offset) << 16; + assert(offset == kASTCHeaderSize); + + return Header(width, height, depth, block_width, block_height, block_depth); +} + +} // namespace astc_codec diff --git a/src/decoder/astc_file.h b/src/decoder/astc_file.h new file mode 100644 index 0000000..c31c2ba --- /dev/null +++ b/src/decoder/astc_file.h @@ -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. + +#ifndef ASTC_CODEC_DECODER_ASTC_FILE_H_ +#define ASTC_CODEC_DECODER_ASTC_FILE_H_ + +#include "src/base/optional.h" +#include "src/decoder/footprint.h" +#include "src/decoder/physical_astc_block.h" + +#include +#include + +namespace astc_codec { + +// A thin wrapper around a .astc file on disk. This class simply reads the ASTC +// header, and stores the block data in memory. +class ASTCFile { + private: + struct Header { + Header(size_t width, size_t height, size_t depth, size_t block_width, + size_t block_height, size_t block_depth) + : width_(width), + height_(height), + depth_(depth), + block_width_(block_width), + block_height_(block_height), + block_depth_(block_depth) {} + + size_t width_; + size_t height_; + size_t depth_; + + size_t block_width_; + size_t block_height_; + size_t block_depth_; + }; + + ASTCFile(ASTCFile::Header&& header, std::string&& blocks); + + public: + // Load an ASTC file from memory. + // If loading failed, nullptr is returned and an error string is populated + // in the error parameter. + static std::unique_ptr LoadFromMemory(const char* data, + size_t length, + std::string* error); + + // Load an ASTC file from file. + // If loading failed, nullptr is returned and an error string is populated + // in the error parameter. + static std::unique_ptr LoadFile(const std::string& path, + std::string* error); + + // Returns the footprint for the file, if it is considered to be a valid + // footprint. + base::Optional GetFootprint() const; + + // Returns the string of the form "NxM" where N and M are the width and height + // of the block footprint, respectively. + std::string GetFootprintString() const; + + // Get the raw block data for the astc file. + const std::string& GetRawBlockData() const; + + // Returns the physical block at the associated block index. + PhysicalASTCBlock GetBlock(size_t block_idx) const; + + size_t GetWidth() const { return header_.width_; } + size_t GetHeight() const { return header_.height_; } + size_t GetDepth() const { return header_.depth_; } + + size_t NumBlocks() const { + return blocks_.size() / PhysicalASTCBlock::kSizeInBytes; + } + + private: + static base::Optional ParseHeader(const char* header); + + const Header header_; + const std::string blocks_; +}; + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_ASTC_FILE_H_ diff --git a/src/decoder/codec.cc b/src/decoder/codec.cc new file mode 100644 index 0000000..c0f8c07 --- /dev/null +++ b/src/decoder/codec.cc @@ -0,0 +1,132 @@ +// 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 "src/base/uint128.h" +#include "src/decoder/logical_astc_block.h" +#include "src/decoder/physical_astc_block.h" + +#include + +namespace astc_codec { + +namespace { +static constexpr size_t kBytesPerPixelUNORM8 = 4; +} + +bool DecompressToImage(const uint8_t* astc_data, size_t astc_data_size, + size_t width, size_t height, Footprint footprint, + uint8_t* out_buffer, size_t out_buffer_size, + size_t out_buffer_stride) { + const size_t block_width = footprint.Width(); + const size_t block_height = footprint.Height(); + assert(block_width != 0); + assert(block_height != 0); + + if (width == 0 || height == 0) { + return false; + } + + const size_t blocks_wide = (width + block_width - 1) / block_width; + assert(blocks_wide != 0); + + // Check that this buffer has the expected number of blocks. + const size_t expected_block_count = + ((width + block_width - 1) / block_width) * + ((height + block_height - 1) / block_height); + + if (astc_data_size % PhysicalASTCBlock::kSizeInBytes != 0 || + astc_data_size / PhysicalASTCBlock::kSizeInBytes != + expected_block_count) { + // TODO(google): Expose error? + return false; + } + + if (kBytesPerPixelUNORM8 * width > out_buffer_stride || + out_buffer_stride * height < out_buffer_size) { + // Output buffer too small. + return false; + } + + base::UInt128 block; + static_assert(sizeof(block) == PhysicalASTCBlock::kSizeInBytes, + "Block size mismatch"); + + for (size_t i = 0; i < astc_data_size; i += PhysicalASTCBlock::kSizeInBytes) { + const size_t block_index = i / PhysicalASTCBlock::kSizeInBytes; + const size_t block_x = block_index % blocks_wide; + const size_t block_y = block_index / blocks_wide; + memcpy(&block, astc_data + i, sizeof(block)); + + PhysicalASTCBlock physical_block(block); + auto lb = UnpackLogicalBlock(footprint, physical_block); + if (!lb) { + return false; + } + + LogicalASTCBlock logical_block = lb.value(); + + for (size_t y = 0; y < block_height; ++y) { + const size_t py = block_height * block_y + y; + uint8_t* out_row = out_buffer + py * out_buffer_stride; + + for (size_t x = 0; x < block_width; ++x) { + const size_t px = block_width * block_x + x; + + // Skip out of bounds. + if (px >= width || py >= height) { + continue; + } + + uint8_t* pixel = out_row + px * kBytesPerPixelUNORM8; + const RgbaColor decoded_color = logical_block.ColorAt(x, y); + for (size_t i = 0; i < kBytesPerPixelUNORM8; ++i) { + pixel[i] = static_cast(decoded_color[i]); + } + } + } + } + + return true; +} + +bool DecompressToImage(const ASTCFile& file, uint8_t* out_buffer, + size_t out_buffer_size, size_t out_buffer_stride) { + base::Optional footprint = file.GetFootprint(); + if (!footprint) { + return false; + } + + return DecompressToImage( + reinterpret_cast(file.GetRawBlockData().c_str()), + file.GetRawBlockData().size(), file.GetWidth(), file.GetHeight(), + footprint.value(), out_buffer, out_buffer_size, out_buffer_stride); +} + +bool ASTCDecompressToRGBA(const uint8_t* astc_data, size_t astc_data_size, + size_t width, size_t height, FootprintType footprint, + uint8_t* out_buffer, size_t out_buffer_size, + size_t out_buffer_stride) { + base::Optional footprint_opt = + Footprint::FromFootprintType(footprint); + if (!footprint_opt) { + return false; + } + + return DecompressToImage(astc_data, astc_data_size, width, height, + footprint_opt.value(), out_buffer, out_buffer_size, + out_buffer_stride); +} + +} // namespace astc_codec diff --git a/src/decoder/codec.h b/src/decoder/codec.h new file mode 100644 index 0000000..eed601a --- /dev/null +++ b/src/decoder/codec.h @@ -0,0 +1,41 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_CODEC_H_ +#define ASTC_CODEC_DECODER_CODEC_H_ + +#include "src/decoder/astc_file.h" +#include "src/decoder/footprint.h" + +#include + +namespace astc_codec { + +// Decompresses ASTC blocks to an image buffer. +// Returns true if the decompression succeeded and the out buffer has been +// filled. +bool DecompressToImage(const uint8_t* astc_data, size_t astc_data_size, + size_t width, size_t height, Footprint footprint, + uint8_t* out_buffer, size_t out_buffer_size, + size_t out_buffer_stride); + +// Decompresses an ASTC file to an image buffer. +// Returns true if the decompression succeeded and the out buffer has been +// filled. +bool DecompressToImage(const ASTCFile& file, uint8_t* out_buffer, + size_t out_buffer_size, size_t out_buffer_stride); + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_CODEC_H_ diff --git a/src/decoder/endpoint_codec.cc b/src/decoder/endpoint_codec.cc new file mode 100644 index 0000000..1513d15 --- /dev/null +++ b/src/decoder/endpoint_codec.cc @@ -0,0 +1,967 @@ +// 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/quantization.h" + +#include +#include +#include +#include + +namespace astc_codec { + +namespace { + +template +T Clamp(T value, T min, T max) { + return value < min ? min : (value > max ? max : value); +} + +// This is the 'blue_contract' function defined in Section C.2.14 of the ASTC +// specification. +template +void BlueContract(ArrayType* const cptr) { + ArrayType& c = *cptr; + c[0] = (c[0] + c[2]) >> 1; + c[1] = (c[1] + c[2]) >> 1; +} + +// Returns the inverse of values in BlueContract, subjected to the constraint +// that the new values are stored in the range [0, 255]. +template +ArrayType InvertBlueContract(const ArrayType& c) { + ArrayType result = c; + result[0] = Clamp(2 * c[0] - c[2], 0, 255); + result[1] = Clamp(2 * c[1] - c[2], 0, 255); + return result; +} + +// This is the 'bit_transfer_signed' function defined in Section C.2.14 of the +// ASTC specification. +void BitTransferSigned(int* const a, int* const b) { + *b >>= 1; + *b |= *a & 0x80; + *a >>= 1; + *a &= 0x3F; + if ((*a & 0x20) != 0) { + *a -= 0x40; + } +} + +// Takes two values, |a| in the range [-32, 31], and |b| in the range [0, 255], +// and returns the two values in [0, 255] that will reconstruct |a| and |b| when +// passed to the BitTransferSigned function. +void InvertBitTransferSigned(int* const a, int* const b) { + assert(*a >= -32); assert(*a < 32); + assert(*b >= 0); assert(*b < 256); + + if (*a < 0) { + *a += 0x40; + } + *a <<= 1; + *a |= (*b & 0x80); + *b <<= 1; + *b &= 0xff; +} + +RgbColor StripAlpha(const RgbaColor& c) { + return RgbColor {{ c[0], c[1], c[2] }}; +} + +template +void Quantize(ContainerType* const c, size_t max_value) { + for (auto& x : *c) { + x = QuantizeCEValueToRange(x, max_value); + } +} + +template +ArrayType QuantizeColor(const ArrayType& c, size_t max_value) { + ArrayType result = c; + Quantize(&result, max_value); + return result; +} + +template +void Unquantize(ContainerType* const c, size_t max_value) { + for (auto& x : *c) { + x = UnquantizeCEValueFromRange(x, max_value); + } +} + +template +ArrayType UnquantizeColor(const ArrayType& c, size_t max_value) { + ArrayType result = c; + Unquantize(&result, max_value); + return result; +} + +// Returns the average of the three RGB channels. +template +const int AverageRGB(const ContainerType& c) { + // Each channel can be in the range [0, 255], and we need to divide by three. + // However, we want to round the error properly. Both (x + 1) / 3 and + // (x + 2) / 3 are relatively imprecise when it comes to rounding, so instead + // we increase the precision by multiplying our numerator by some arbitrary + // number. Here, we choose 256 to get 8 additional bits and maintain + // performance since it turns into a shift rather than a multiply. Our + // denominator then becomes 3 * 256 = 768. + return (std::accumulate(c.begin(), c.begin() + 3, 0) * 256 + 384) / 768; +} + +// Returns the sum of squared differences between each element of |a| and |b|, +// which are assumed to contain the same number of elements. +template +const typename ContainerType::value_type SquaredError( + const ContainerType& a, const ContainerType& b, + size_t num_channels = std::tuple_size::value) { + using ValueTy = typename ContainerType::value_type; + static_assert(std::is_signed::value, + "Value type assumed to be signed to avoid branch below."); + ValueTy result = ValueTy(0); + for (int i = 0; i < num_channels; ++i) { + ValueTy error = a[i] - b[i]; + result += error * error; + } + return result; +} + +constexpr int MaxValuesForModes(ColorEndpointMode mode_a, + ColorEndpointMode mode_b) { + return (NumColorValuesForEndpointMode(mode_a) > + NumColorValuesForEndpointMode(mode_b)) + ? NumColorValuesForEndpointMode(mode_a) + : NumColorValuesForEndpointMode(mode_b); +} + +// This function takes the two colors in |endpoint_low| and |endpoint_high| and +// encodes them into |vals| according to the ASTC spec in section C.2.14. It +// assumes that the two colors are close enough to grayscale that the encoding +// should use the ColorEndpointMode kLDRLumaBaseOffset or kLDRLumaDirect. Which +// one is chosen depends on which produces smaller error for the given +// quantization value stored in |max_value| +bool EncodeColorsLuma(const RgbaColor& endpoint_low, + const RgbaColor& endpoint_high, + int max_value, ColorEndpointMode* const astc_mode, + std::vector* const vals) { + assert(vals->size() == + NumValuesForEncodingMode(EndpointEncodingMode::kDirectLuma)); + int avg1 = AverageRGB(endpoint_low); + int avg2 = AverageRGB(endpoint_high); + + // For the offset mode, L1 is strictly greater than L2, so if we are using + // it to encode the color values, we need to swap the weights and + // endpoints so that the larger of the two is the second endpoint. + bool needs_weight_swap = false; + if (avg1 > avg2) { + needs_weight_swap = true; + std::swap(avg1, avg2); + } + assert(avg1 <= avg2); + + // Now, the first endpoint is based on the low-order six bits of the first + // value, and the high order two bits of the second value. The low order + // six bits of the second value are used as the (strictly positive) offset + // from the first value. + const int offset = std::min(avg2 - avg1, 0x3F); + const int quant_off_low = + QuantizeCEValueToRange((avg1 & 0x3F) << 2, max_value); + const int quant_off_high = + QuantizeCEValueToRange((avg1 & 0xC0) | offset, max_value); + + const int quant_low = QuantizeCEValueToRange(avg1, max_value); + const int quant_high = QuantizeCEValueToRange(avg2, max_value); + + RgbaColor unquant_off_low, unquant_off_high; + RgbaColor unquant_low, unquant_high; + + (*vals)[0] = quant_off_low; + (*vals)[1] = quant_off_high; + DecodeColorsForMode( + *vals, max_value, ColorEndpointMode::kLDRLumaBaseOffset, + &unquant_off_low, &unquant_off_high); + + (*vals)[0] = quant_low; + (*vals)[1] = quant_high; + DecodeColorsForMode(*vals, max_value, ColorEndpointMode::kLDRLumaDirect, + &unquant_low, &unquant_high); + + const auto calculate_error = + [needs_weight_swap, &endpoint_low, &endpoint_high] + (const RgbaColor& low, const RgbaColor& high) { + int error = 0; + if (needs_weight_swap) { + error += SquaredError(low, endpoint_high); + error += SquaredError(high, endpoint_low); + } else { + error += SquaredError(low, endpoint_low); + error += SquaredError(high, endpoint_high); + } + return error; + }; + + const int direct_error = calculate_error(unquant_low, unquant_high); + const int off_error = calculate_error(unquant_off_low, unquant_off_high); + + if (direct_error <= off_error) { + (*vals)[0] = quant_low; + (*vals)[1] = quant_high; + *astc_mode = ColorEndpointMode::kLDRLumaDirect; + } else { + (*vals)[0] = quant_off_low; + (*vals)[1] = quant_off_high; + *astc_mode = ColorEndpointMode::kLDRLumaBaseOffset; + } + + return needs_weight_swap; +} + +class QuantizedEndpointPair { + public: + QuantizedEndpointPair(const RgbaColor& c_low, const RgbaColor& c_high, + int max_value) + : orig_low_(c_low), + orig_high_(c_high), + quant_low_(QuantizeColor(c_low, max_value)), + quant_high_(QuantizeColor(c_high, max_value)), + unquant_low_(UnquantizeColor(quant_low_, max_value)), + unquant_high_(UnquantizeColor(quant_high_, max_value)) { } + + const RgbaColor& QuantizedLow() const { return quant_low_; } + const RgbaColor& QuantizedHigh() const { return quant_high_; } + + const RgbaColor& UnquantizedLow() const { return unquant_low_; } + const RgbaColor& UnquantizedHigh() const { return unquant_high_; } + + const RgbaColor& OriginalLow() const { return orig_low_; } + const RgbaColor& OriginalHigh() const { return orig_high_; } + + private: + RgbaColor orig_low_; + RgbaColor orig_high_; + + RgbaColor quant_low_; + RgbaColor quant_high_; + + RgbaColor unquant_low_; + RgbaColor unquant_high_; +}; + +class CEEncodingOption { + public: + CEEncodingOption() { } + CEEncodingOption( + int squared_error, const QuantizedEndpointPair* quantized_endpoints, + bool swap_endpoints, bool blue_contract, bool use_offset_mode) + : squared_error_(squared_error), + quantized_endpoints_(quantized_endpoints), + swap_endpoints_(swap_endpoints), + blue_contract_(blue_contract), + use_offset_mode_(use_offset_mode) { } + + // Returns true if able to generate valid |astc_mode| and |vals|. In some + // instances, such as if the endpoints reprsent a base/offset pair, we may not + // be able to guarantee blue-contract encoding due to how the base/offset pair + // are represented and the specifics of the decoding procedure. Similarly, + // some direct RGBA encodings also may not be able to emit blue-contract modes + // due to an unlucky combination of channels. In these instances, this + // function will return false, and all pointers will remain unmodified. + bool Pack(bool with_alpha, ColorEndpointMode* const astc_mode, + std::vector* const vals, bool* const needs_weight_swap) const { + auto unquantized_low = quantized_endpoints_->UnquantizedLow(); + auto unquantized_high = quantized_endpoints_->UnquantizedHigh(); + + // In offset mode, we do BitTransferSigned before analyzing the values + // of the endpoints in order to determine whether or not we're going to + // be using blue-contract mode. + if (use_offset_mode_) { + for (int i = 0; i < std::tuple_size::value; ++i) { + BitTransferSigned(&unquantized_high[i], &unquantized_low[i]); + } + } + + // Define variables as outlined in the ASTC spec C.2.14 for the RGB[A] + // direct and base-offset modes + int s0 = 0, s1 = 0; + for (int i = 0; i < 3; ++i) { + s0 += unquantized_low[i]; + s1 += unquantized_high[i]; + } + + // Can we guarantee a blue-contract mode if we want it? In other words, + // if we swap which endpoint is high and which endpoint is low, can we + // guarantee that we will hit the corresponding decode path? + bool swap_vals = false; + if (use_offset_mode_) { + if (blue_contract_) { + swap_vals = s1 >= 0; + } else { + swap_vals = s1 < 0; + } + + // In offset mode, we have two different measurements that swap the + // endpoints prior to encoding, so we don't need to swap them here. + // If we need to swap them to guarantee a blue-contract mode, then + // abort and wait until we get the other error measurement. + if (swap_vals) { + return false; + } + } else { + if (blue_contract_) { + // If we want a blue_contract path, but s1 == s0, then swapping the + // values will have no effect. + if (s1 == s0) { + return false; + } + + swap_vals = s1 > s0; + // If we're encoding blue contract mode directly, then we implicitly + // swap the endpoints during decode, meaning that we need to take + // note of that here. + *needs_weight_swap = !(*needs_weight_swap); + } else { + swap_vals = s1 < s0; + } + } + + const auto* quantized_low = &(quantized_endpoints_->QuantizedLow()); + const auto* quantized_high = &(quantized_endpoints_->QuantizedHigh()); + + if (swap_vals) { + assert(!use_offset_mode_); + std::swap(quantized_low, quantized_high); + *needs_weight_swap = !(*needs_weight_swap); + } + + (*vals)[0] = quantized_low->at(0); + (*vals)[1] = quantized_high->at(0); + (*vals)[2] = quantized_low->at(1); + (*vals)[3] = quantized_high->at(1); + (*vals)[4] = quantized_low->at(2); + (*vals)[5] = quantized_high->at(2); + + if (use_offset_mode_) { + *astc_mode = ColorEndpointMode::kLDRRGBBaseOffset; + } else { + *astc_mode = ColorEndpointMode::kLDRRGBDirect; + } + + if (with_alpha) { + (*vals)[6] = quantized_low->at(3); + (*vals)[7] = quantized_high->at(3); + + if (use_offset_mode_) { + *astc_mode = ColorEndpointMode::kLDRRGBABaseOffset; + } else { + *astc_mode = ColorEndpointMode::kLDRRGBADirect; + } + } + + // If we swapped them to measure, then they need to be swapped after + // decoding + if (swap_endpoints_) { + *needs_weight_swap = !(*needs_weight_swap); + } + + return true; + } + + bool BlueContract() const { return blue_contract_; } + int Error() const { return squared_error_; } + + private: + int squared_error_; + const QuantizedEndpointPair* quantized_endpoints_; + bool swap_endpoints_; + bool blue_contract_; + bool use_offset_mode_; +}; + +bool EncodeColorsRGBA(const RgbaColor& endpoint_low_rgba, + const RgbaColor& endpoint_high_rgba, + int max_value, bool with_alpha, + ColorEndpointMode* const astc_mode, + std::vector* const vals) { + const int num_channels = with_alpha ? std::tuple_size::value : 3; + // The difficulty of encoding into this mode is determining whether or + // not we'd like to use the 'blue contract' function to reconstruct + // the endpoints and whether or not we'll be more accurate by using the + // base/offset color modes instead of quantizing the color channels + // directly. With that in mind, we: + // 1. Generate the inverted values for blue-contract and offset modes. + // 2. Quantize all of the different endpoints. + // 3. Unquantize each sets and decide which one gives least error + // 4. Encode the values correspondingly. + + // 1. Generate the inverted values for blue-contract and offset modes. + const auto inv_bc_low = InvertBlueContract(endpoint_low_rgba); + const auto inv_bc_high = InvertBlueContract(endpoint_high_rgba); + + RgbaColor direct_base, direct_offset; + for (int i = 0; i < std::tuple_size::value; ++i) { + direct_base[i] = endpoint_low_rgba[i]; + direct_offset[i] = + Clamp(endpoint_high_rgba[i] - endpoint_low_rgba[i], -32, 31); + InvertBitTransferSigned(&direct_offset[i], &direct_base[i]); + } + + RgbaColor inv_bc_base, inv_bc_offset; + for (int i = 0; i < std::tuple_size::value; ++i) { + // Remember, for blue-contract'd offset modes, the base is compared + // against the second endpoint and not the first. + inv_bc_base[i] = inv_bc_high[i]; + inv_bc_offset[i] = Clamp(inv_bc_low[i] - inv_bc_high[i], -32, 31); + InvertBitTransferSigned(&inv_bc_offset[i], &inv_bc_base[i]); + } + + // The order of the endpoints for offset modes may determine how well they + // approximate the given endpoints. It may be that the quantization value + // produces more accurate values for the base than the offset or + // vice/versa. For this reason, we need to generate quantized versions of + // the endpoints as if they were swapped to see if we get better error + // out of it. + + RgbaColor direct_base_swapped, direct_offset_swapped; + for (int i = 0; i < std::tuple_size::value; ++i) { + direct_base_swapped[i] = endpoint_high_rgba[i]; + direct_offset_swapped[i] = + Clamp(endpoint_low_rgba[i] - endpoint_high_rgba[i], -32, 31); + InvertBitTransferSigned(&direct_offset_swapped[i], &direct_base_swapped[i]); + } + + RgbaColor inv_bc_base_swapped, inv_bc_offset_swapped; + for (int i = 0; i < std::tuple_size::value; ++i) { + // Remember, for blue-contract'd offset modes, the base is compared + // against the second endpoint and not the first. Hence, the swapped + // version will compare the base against the first endpoint. + inv_bc_base_swapped[i] = inv_bc_low[i]; + inv_bc_offset_swapped[i] = Clamp(inv_bc_high[i] - inv_bc_low[i], -32, 31); + InvertBitTransferSigned(&inv_bc_offset_swapped[i], &inv_bc_base_swapped[i]); + } + + // 2. Quantize the endpoints directly. + const QuantizedEndpointPair direct_quantized( + endpoint_low_rgba, endpoint_high_rgba, max_value); + const QuantizedEndpointPair bc_quantized( + inv_bc_low, inv_bc_high, max_value); + + const QuantizedEndpointPair offset_quantized( + direct_base, direct_offset, max_value); + const QuantizedEndpointPair bc_offset_quantized( + inv_bc_base, inv_bc_offset, max_value); + + const QuantizedEndpointPair offset_swapped_quantized( + direct_base_swapped, direct_offset_swapped, max_value); + const QuantizedEndpointPair bc_offset_swapped_quantized( + inv_bc_base_swapped, inv_bc_offset_swapped, max_value); + + // 3. Unquantize each set and decide which one gives least error. + std::array errors; + auto errors_itr = errors.begin(); + + // 3.1 regular unquantized error + { + const auto rgba_low = direct_quantized.UnquantizedLow(); + const auto rgba_high = direct_quantized.UnquantizedHigh(); + + const int sq_rgb_error = + SquaredError(rgba_low, endpoint_low_rgba, num_channels) + + SquaredError(rgba_high, endpoint_high_rgba, num_channels); + + const bool swap_endpoints = false; + const bool blue_contract = false; + const bool offset_mode = false; + *(errors_itr++) = CEEncodingOption( + sq_rgb_error, &direct_quantized, + swap_endpoints, blue_contract, offset_mode); + } + + // 3.2 Compute blue-contract'd error. + { + auto bc_low = bc_quantized.UnquantizedLow(); + auto bc_high = bc_quantized.UnquantizedHigh(); + BlueContract(&bc_low); + BlueContract(&bc_high); + + const int sq_bc_error = + SquaredError(bc_low, endpoint_low_rgba, num_channels) + + SquaredError(bc_high, endpoint_high_rgba, num_channels); + + const bool swap_endpoints = false; + const bool blue_contract = true; + const bool offset_mode = false; + *(errors_itr++) = CEEncodingOption( + sq_bc_error, &bc_quantized, + swap_endpoints, blue_contract, offset_mode); + } + + // 3.3 Compute base/offset unquantized error. + const auto compute_base_offset_error = + [num_channels, &errors_itr, &endpoint_low_rgba, &endpoint_high_rgba] + (const QuantizedEndpointPair& pair, bool swapped) { + auto base = pair.UnquantizedLow(); + auto offset = pair.UnquantizedHigh(); + + for (int i = 0; i < num_channels; ++i) { + BitTransferSigned(&offset[i], &base[i]); + offset[i] = Clamp(base[i] + offset[i], 0, 255); + } + + int base_offset_error = 0; + // If we swapped the endpoints going in, then without blue contract + // we should be comparing the base against the high endpoint. + if (swapped) { + base_offset_error = + SquaredError(base, endpoint_high_rgba, num_channels) + + SquaredError(offset, endpoint_low_rgba, num_channels); + } else { + base_offset_error = + SquaredError(base, endpoint_low_rgba, num_channels) + + SquaredError(offset, endpoint_high_rgba, num_channels); + } + + const bool blue_contract = false; + const bool offset_mode = true; + *(errors_itr++) = CEEncodingOption( + base_offset_error, &pair, swapped, blue_contract, offset_mode); + }; + + compute_base_offset_error(offset_quantized, false); + + // 3.4 Compute base/offset blue-contract error. + const auto compute_base_offset_blue_contract_error = + [num_channels, &errors_itr, &endpoint_low_rgba, &endpoint_high_rgba] + (const QuantizedEndpointPair& pair, bool swapped) { + auto base = pair.UnquantizedLow(); + auto offset = pair.UnquantizedHigh(); + + for (int i = 0; i < num_channels; ++i) { + BitTransferSigned(&offset[i], &base[i]); + offset[i] = Clamp(base[i] + offset[i], 0, 255); + } + + BlueContract(&base); + BlueContract(&offset); + + int sq_bc_error = 0; + // Remember, for blue-contract'd offset modes, the base is compared + // against the second endpoint and not the first. So, we compare + // against the first if we swapped the endpoints going in. + if (swapped) { + sq_bc_error = + SquaredError(base, endpoint_low_rgba, num_channels) + + SquaredError(offset, endpoint_high_rgba, num_channels); + } else { + sq_bc_error = + SquaredError(base, endpoint_high_rgba, num_channels) + + SquaredError(offset, endpoint_low_rgba, num_channels); + } + + const bool blue_contract = true; + const bool offset_mode = true; + *(errors_itr++) = CEEncodingOption(sq_bc_error, &pair, + swapped, blue_contract, offset_mode); + }; + + compute_base_offset_blue_contract_error(bc_offset_quantized, false); + + // 3.5 Compute swapped base/offset error. + compute_base_offset_error(offset_swapped_quantized, true); + + // 3.6 Compute swapped base/offset blue-contract error. + compute_base_offset_blue_contract_error( + bc_offset_swapped_quantized, true); + + std::sort(errors.begin(), errors.end(), + [](const CEEncodingOption& a, const CEEncodingOption& b) { + return a.Error() < b.Error(); + }); + + // 4. Encode the values correspondingly. + // For this part, we go through each measurement in order of increasing + // error. Based on the properties of each measurement, we decide how to + // best encode the quantized endpoints that produced that error value. If + // for some reason we cannot encode that metric, then we skip it and move + // to the next one. + for (const auto& measurement : errors) { + bool needs_weight_swap = false; + if (measurement.Pack(with_alpha, astc_mode, vals, &needs_weight_swap)) { + // Make sure that if we ask for a blue-contract mode that we get it *and* + // if we don't ask for it then we don't get it. + assert(!(measurement.BlueContract() ^ + UsesBlueContract(max_value, *astc_mode, *vals))); + + // We encoded what we got. + return needs_weight_swap; + } + } + + assert(false && "Shouldn't have reached this point -- some combination of " + "endpoints should be possible to encode!"); + return false; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +bool UsesBlueContract(int max_value, ColorEndpointMode mode, + const std::vector& vals) { + assert(vals.size() >= NumColorValuesForEndpointMode(mode)); + + switch (mode) { + case ColorEndpointMode::kLDRRGBDirect: + case ColorEndpointMode::kLDRRGBADirect: { + constexpr int kNumVals = MaxValuesForModes( + ColorEndpointMode::kLDRRGBDirect, ColorEndpointMode::kLDRRGBADirect); + std::array v {}; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + const int s0 = v[0] + v[2] + v[4]; + const int s1 = v[1] + v[3] + v[5]; + + return s0 > s1; + } + + case ColorEndpointMode::kLDRRGBBaseOffset: + case ColorEndpointMode::kLDRRGBABaseOffset: { + constexpr int kNumVals = MaxValuesForModes( + ColorEndpointMode::kLDRRGBBaseOffset, + ColorEndpointMode::kLDRRGBABaseOffset); + std::array v {}; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + BitTransferSigned(&v[1], &v[0]); + BitTransferSigned(&v[3], &v[2]); + BitTransferSigned(&v[5], &v[4]); + + return v[1] + v[3] + v[5] < 0; + } + + default: + return false; + } +} + +bool EncodeColorsForMode( + const RgbaColor& endpoint_low_rgba, const RgbaColor& endpoint_high_rgba, + int max_value, EndpointEncodingMode encoding_mode, + ColorEndpointMode* const astc_mode, std::vector* const vals) { + bool needs_weight_swap = false; + vals->resize(NumValuesForEncodingMode(encoding_mode)); + + switch (encoding_mode) { + case EndpointEncodingMode::kDirectLuma: + return EncodeColorsLuma( + endpoint_low_rgba, endpoint_high_rgba, max_value, astc_mode, vals); + + case EndpointEncodingMode::kDirectLumaAlpha: { + // TODO(google): See if luma-alpha base-offset is better + const int avg1 = AverageRGB(endpoint_low_rgba); + const int avg2 = AverageRGB(endpoint_high_rgba); + + (*vals)[0] = QuantizeCEValueToRange(avg1, max_value); + (*vals)[1] = QuantizeCEValueToRange(avg2, max_value); + (*vals)[2] = QuantizeCEValueToRange(endpoint_low_rgba[3], max_value); + (*vals)[3] = QuantizeCEValueToRange(endpoint_high_rgba[3], max_value); + *astc_mode = ColorEndpointMode::kLDRLumaAlphaDirect; + } + break; + + case EndpointEncodingMode::kBaseScaleRGB: + case EndpointEncodingMode::kBaseScaleRGBA: { + RgbaColor base = endpoint_high_rgba; + RgbaColor scaled = endpoint_low_rgba; + + // Similar to luma base-offset, the scaled value is strictly less than + // the base value here according to the decode procedure. In this case, + // if the base is larger than the scale then we need to swap. + int num_channels_ge = 0; + for (int i = 0; i < 3; ++i) { + num_channels_ge += + static_cast(endpoint_high_rgba[i] >= endpoint_low_rgba[i]); + } + + if (num_channels_ge < 2) { + needs_weight_swap = true; + std::swap(base, scaled); + } + + // Since the second endpoint is just a direct copy of the RGB values, we + // can start by quantizing them. + const auto q_base = QuantizeColor(base, max_value); + const auto uq_base = UnquantizeColor(q_base, max_value); + + // The first endpoint (scaled) is defined by piecewise multiplying the + // second endpoint (base) by the scale factor and then dividing by 256. + // This means that the inverse operation is to first piecewise multiply + // the first endpoint by 256 and then divide by the unquantized second + // endpoint. We take the average of each of each of these scale values as + // our final scale value. + // TODO(google): Is this the best way to determine the scale factor? + int num_samples = 0; + int scale_sum = 0; + for (int i = 0; i < 3; ++i) { + int x = uq_base[i]; + if (x != 0) { + ++num_samples; + scale_sum += (scaled[i] * 256) / x; + } + } + + (*vals)[0] = q_base[0]; + (*vals)[1] = q_base[1]; + (*vals)[2] = q_base[2]; + if (num_samples > 0) { + const int avg_scale = Clamp(scale_sum / num_samples, 0, 255); + (*vals)[3] = QuantizeCEValueToRange(avg_scale, max_value); + } else { + // In this case, all of the base values are zero, so we can use whatever + // we want as the scale -- it won't affect the outcome. + (*vals)[3] = max_value; + } + *astc_mode = ColorEndpointMode::kLDRRGBBaseScale; + + if (encoding_mode == EndpointEncodingMode::kBaseScaleRGBA) { + (*vals)[4] = QuantizeCEValueToRange(scaled[3], max_value); + (*vals)[5] = QuantizeCEValueToRange(base[3], max_value); + *astc_mode = ColorEndpointMode::kLDRRGBBaseScaleTwoA; + } + } + break; + + case EndpointEncodingMode::kDirectRGB: + case EndpointEncodingMode::kDirectRGBA: + return EncodeColorsRGBA( + endpoint_low_rgba, endpoint_high_rgba, max_value, + encoding_mode == EndpointEncodingMode::kDirectRGBA, astc_mode, vals); + + default: + assert(false && "Unimplemented color encoding."); + } + + return needs_weight_swap; +} + +// These decoding procedures follow the code outlined in Section C.2.14 of +// the ASTC specification. +void DecodeColorsForMode(const std::vector& vals, + int max_value, ColorEndpointMode mode, + RgbaColor* const endpoint_low_rgba, + RgbaColor* const endpoint_high_rgba) { + assert(vals.size() >= NumColorValuesForEndpointMode(mode)); + switch (mode) { + case ColorEndpointMode::kLDRLumaDirect: { + const int l0 = UnquantizeCEValueFromRange(vals[0], max_value); + const int l1 = UnquantizeCEValueFromRange(vals[1], max_value); + + *endpoint_low_rgba = {{ l0, l0, l0, 255 }}; + *endpoint_high_rgba = {{ l1, l1, l1, 255 }}; + } + break; + + case ColorEndpointMode::kLDRLumaBaseOffset: { + const int v0 = UnquantizeCEValueFromRange(vals[0], max_value); + const int v1 = UnquantizeCEValueFromRange(vals[1], max_value); + + const int l0 = (v0 >> 2) | (v1 & 0xC0); + const int l1 = std::min(l0 + (v1 & 0x3F), 0xFF); + + *endpoint_low_rgba = {{ l0, l0, l0, 255 }}; + *endpoint_high_rgba = {{ l1, l1, l1, 255 }}; + } + break; + + case ColorEndpointMode::kLDRLumaAlphaDirect: { + constexpr int kNumVals = + NumColorValuesForEndpointMode(ColorEndpointMode::kLDRLumaAlphaDirect); + + std::array v; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + *endpoint_low_rgba = {{ v[0], v[0], v[0], v[2] }}; + *endpoint_high_rgba = {{ v[1], v[1], v[1], v[3] }}; + } + break; + + case ColorEndpointMode::kLDRLumaAlphaBaseOffset: { + constexpr int kNumVals = NumColorValuesForEndpointMode( + ColorEndpointMode::kLDRLumaAlphaBaseOffset); + + std::array v; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + BitTransferSigned(&v[1], &v[0]); + BitTransferSigned(&v[3], &v[2]); + + *endpoint_low_rgba = {{ v[0], v[0], v[0], v[2] }}; + const int high_luma = v[0] + v[1]; + *endpoint_high_rgba = {{ high_luma, high_luma, high_luma, v[2] + v[3] }}; + + for (auto& c : *endpoint_low_rgba) { c = Clamp(c, 0, 255); } + for (auto& c : *endpoint_high_rgba) { c = Clamp(c, 0, 255); } + } + break; + + case ColorEndpointMode::kLDRRGBBaseScale: { + constexpr int kNumVals = + NumColorValuesForEndpointMode(ColorEndpointMode::kLDRRGBBaseScale); + + std::array v; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + *endpoint_high_rgba = {{ v[0], v[1], v[2], 255 }}; + for (int i = 0; i < 3; ++i) { + const int x = endpoint_high_rgba->at(i); + endpoint_low_rgba->at(i) = (x * v[3]) >> 8; + } + endpoint_low_rgba->at(3) = 255; + } + break; + + case ColorEndpointMode::kLDRRGBDirect: { + constexpr int kNumVals = + NumColorValuesForEndpointMode(ColorEndpointMode::kLDRRGBDirect); + + std::array v; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + const int s0 = v[0] + v[2] + v[4]; + const int s1 = v[1] + v[3] + v[5]; + + *endpoint_low_rgba = {{ v[0], v[2], v[4], 255 }}; + *endpoint_high_rgba = {{ v[1], v[3], v[5], 255 }}; + + if (s1 < s0) { + std::swap(*endpoint_low_rgba, *endpoint_high_rgba); + BlueContract(endpoint_low_rgba); + BlueContract(endpoint_high_rgba); + } + } + break; + + case ColorEndpointMode::kLDRRGBBaseOffset: { + constexpr int kNumVals = + NumColorValuesForEndpointMode(ColorEndpointMode::kLDRRGBBaseOffset); + + std::array v; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + BitTransferSigned(&v[1], &v[0]); + BitTransferSigned(&v[3], &v[2]); + BitTransferSigned(&v[5], &v[4]); + + *endpoint_low_rgba = {{ v[0], v[2], v[4], 255 }}; + *endpoint_high_rgba = {{ v[0] + v[1], v[2] + v[3], v[4] + v[5], 255 }}; + + if (v[1] + v[3] + v[5] < 0) { + std::swap(*endpoint_low_rgba, *endpoint_high_rgba); + BlueContract(endpoint_low_rgba); + BlueContract(endpoint_high_rgba); + } + + for (auto& c : *endpoint_low_rgba) { c = Clamp(c, 0, 255); } + for (auto& c : *endpoint_high_rgba) { c = Clamp(c, 0, 255); } + } + break; + + case ColorEndpointMode::kLDRRGBBaseScaleTwoA: { + constexpr int kNumVals = NumColorValuesForEndpointMode( + ColorEndpointMode::kLDRRGBBaseScaleTwoA); + + std::array v; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + // Base + *endpoint_low_rgba = *endpoint_high_rgba = {{ v[0], v[1], v[2], 255 }}; + + // Scale + for (int i = 0; i < 3; ++i) { + auto& x = endpoint_low_rgba->at(i); + x = (x * v[3]) >> 8; + } + + // Two A + endpoint_low_rgba->at(3) = v[4]; + endpoint_high_rgba->at(3) = v[5]; + } + break; + + case ColorEndpointMode::kLDRRGBADirect: { + constexpr int kNumVals = + NumColorValuesForEndpointMode(ColorEndpointMode::kLDRRGBADirect); + + std::array v; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + const int s0 = v[0] + v[2] + v[4]; + const int s1 = v[1] + v[3] + v[5]; + + *endpoint_low_rgba = {{ v[0], v[2], v[4], v[6] }}; + *endpoint_high_rgba = {{ v[1], v[3], v[5], v[7] }}; + + if (s1 < s0) { + std::swap(*endpoint_low_rgba, *endpoint_high_rgba); + BlueContract(endpoint_low_rgba); + BlueContract(endpoint_high_rgba); + } + } + break; + + case ColorEndpointMode::kLDRRGBABaseOffset: { + constexpr int kNumVals = + NumColorValuesForEndpointMode(ColorEndpointMode::kLDRRGBABaseOffset); + + std::array v; + std::copy(vals.begin(), vals.end(), v.begin()); + Unquantize(&v, max_value); + + BitTransferSigned(&v[1], &v[0]); + BitTransferSigned(&v[3], &v[2]); + BitTransferSigned(&v[5], &v[4]); + BitTransferSigned(&v[7], &v[6]); + + *endpoint_low_rgba = {{ v[0], v[2], v[4], v[6] }}; + *endpoint_high_rgba = {{ + v[0] + v[1], v[2] + v[3], v[4] + v[5], v[6] + v[7] }}; + + if (v[1] + v[3] + v[5] < 0) { + std::swap(*endpoint_low_rgba, *endpoint_high_rgba); + BlueContract(endpoint_low_rgba); + BlueContract(endpoint_high_rgba); + } + + for (auto& c : *endpoint_low_rgba) { c = Clamp(c, 0, 255); } + for (auto& c : *endpoint_high_rgba) { c = Clamp(c, 0, 255); } + } + break; + + default: + // Unimplemented color encoding. + // TODO(google): Is this the correct error handling? + *endpoint_high_rgba = *endpoint_low_rgba = {{ 0, 0, 0, 0 }}; + } +} + +} // namespace astc_codec diff --git a/src/decoder/endpoint_codec.h b/src/decoder/endpoint_codec.h new file mode 100644 index 0000000..a1232d0 --- /dev/null +++ b/src/decoder/endpoint_codec.h @@ -0,0 +1,90 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_ENDPOINT_CODEC_H_ +#define ASTC_CODEC_DECODER_ENDPOINT_CODEC_H_ + +#include "src/decoder/physical_astc_block.h" +#include "src/decoder/types.h" + +#include +#include + +namespace astc_codec { + +// We use a special distinction for encode modes used to pass to the +// EncodeColorsForMode function below. The reason is that some of the color +// modes have sub-modes (like blue-contract) that change whether or not it is +// useful to encode an endpoint pair using one mode versus another. To avoid +// this problem, we approach the problem of encoding by specifying some +// high-level encoding modes. These eventually choose one of the low level +// ColorEndpointModes from Section C.2.14 when used in EncodeColorsForMode. +enum class EndpointEncodingMode { + kDirectLuma, + kDirectLumaAlpha, + kBaseScaleRGB, + kBaseScaleRGBA, + kDirectRGB, + kDirectRGBA +}; + +// Returns the number of values in the encoded endpoint pair after encoding +// to a specific high-level encoding mode. +constexpr int NumValuesForEncodingMode(EndpointEncodingMode mode) { + return + mode == EndpointEncodingMode::kDirectLuma ? 2 : + mode == EndpointEncodingMode::kDirectLumaAlpha ? 4 : + mode == EndpointEncodingMode::kBaseScaleRGB ? 4 : + mode == EndpointEncodingMode::kBaseScaleRGBA ? 6 : + mode == EndpointEncodingMode::kDirectRGB ? 6 : 8; +} + +// Fills |vals| with the quantized endpoint colors values defined in the ASTC +// specification. The values are quantized to the range [0, max_value]. These +// quantization limits can be obtained by querying the associated functions in +// integer_sequence_codec. The returned |astc_mode| will be the ASTC mode used +// to encode the resulting sequence. +// +// The |encoding_mode| is used to determine the way that we encode the values. +// Each encoding mode is used to determine which ASTC mode best corresponds +// to the pair of endpoints. It is a necessary hint to the encoding function +// in order to process the endpoints. Each encoding mode gurantees a certain +// number of values generated per endpoints. +// +// The return value will be true if the endpoints have been switched in order to +// reap the most benefit from the way the hardware decodes the given mode. In +// this case, the associated weights that interpolate this color must also be +// switched. In other words, for each w, it should change to 64 - w. +bool EncodeColorsForMode( + const RgbaColor& endpoint_low_rgba, const RgbaColor& endpoint_high_rgba, + int max_value, EndpointEncodingMode encoding_mode, + ColorEndpointMode* astc_mode, std::vector* vals); + +// Decodes the color values quantized to the range [0, max_value] into RGBA +// endpoints for the given mode. This function is the inverse of +// EncodeColorsForMode -- see that function for details. This function should +// work on all LDR endpoint modes, but no HDR modes. +void DecodeColorsForMode(const std::vector& vals, + int max_value, ColorEndpointMode mode, + RgbaColor* endpoint_low_rgba, + RgbaColor* endpoint_high_rgba); + +// Returns true if the quantized |vals| in the range [0, max_value] use the +// 'blue_contract' modification during decoding for the given |mode|. +bool UsesBlueContract(int max_value, ColorEndpointMode mode, + const std::vector& vals); + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_ENDPOINT_CODEC_H_ diff --git a/src/decoder/footprint.cc b/src/decoder/footprint.cc new file mode 100644 index 0000000..e4f076e --- /dev/null +++ b/src/decoder/footprint.cc @@ -0,0 +1,162 @@ +// 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 "src/base/string_utils.h" + +#include +#include +#include +#include + +namespace astc_codec { + +namespace { + +// Encodes the width and height into an integer so that we can use a switch +// statement instead of a costly lookup map. +constexpr int EncodeDims(int width, int height) { + return (width << 16) | height; +} + +} // namespace + +base::Optional +Footprint::GetValidFootprintForDimensions(int width, int height) { + switch (EncodeDims(width, height)) { + case EncodeDims(4, 4): return FootprintType::k4x4; + case EncodeDims(5, 4): return FootprintType::k5x4; + case EncodeDims(5, 5): return FootprintType::k5x5; + case EncodeDims(6, 5): return FootprintType::k6x5; + case EncodeDims(6, 6): return FootprintType::k6x6; + case EncodeDims(8, 5): return FootprintType::k8x5; + case EncodeDims(8, 6): return FootprintType::k8x6; + case EncodeDims(8, 8): return FootprintType::k8x8; + case EncodeDims(10, 5): return FootprintType::k10x5; + case EncodeDims(10, 6): return FootprintType::k10x6; + case EncodeDims(10, 8): return FootprintType::k10x8; + case EncodeDims(10, 10): return FootprintType::k10x10; + case EncodeDims(12, 10): return FootprintType::k12x10; + case EncodeDims(12, 12): return FootprintType::k12x12; + default: return {}; + } +} + +int Footprint::GetWidthForFootprint(FootprintType footprint) { + switch (footprint) { + case FootprintType::k4x4: return 4; + case FootprintType::k5x4: return 5; + case FootprintType::k5x5: return 5; + case FootprintType::k6x5: return 6; + case FootprintType::k6x6: return 6; + case FootprintType::k8x5: return 8; + case FootprintType::k8x6: return 8; + case FootprintType::k10x5: return 10; + case FootprintType::k10x6: return 10; + case FootprintType::k8x8: return 8; + case FootprintType::k10x8: return 10; + case FootprintType::k10x10: return 10; + case FootprintType::k12x10: return 12; + case FootprintType::k12x12: return 12; + default: + assert(false); + return -1; + } +} + +int Footprint::GetHeightForFootprint(FootprintType footprint) { + switch (footprint) { + case FootprintType::k4x4: return 4; + case FootprintType::k5x4: return 4; + case FootprintType::k5x5: return 5; + case FootprintType::k6x5: return 5; + case FootprintType::k6x6: return 6; + case FootprintType::k8x5: return 5; + case FootprintType::k8x6: return 6; + case FootprintType::k10x5: return 5; + case FootprintType::k10x6: return 6; + case FootprintType::k8x8: return 8; + case FootprintType::k10x8: return 8; + case FootprintType::k10x10: return 10; + case FootprintType::k12x10: return 10; + case FootprintType::k12x12: return 12; + default: + assert(false); + return -1; + } +} + +Footprint::Footprint(FootprintType footprint) + : footprint_(footprint), width_(GetWidthForFootprint(footprint)), + height_(GetHeightForFootprint(footprint)) { } + +//////////////////////////////////////////////////////////////////////////////// + +base::Optional Footprint::Parse(const char* footprint_string) { + assert(footprint_string && footprint_string[0] != '\0'); + + std::vector dimension_strings; + base::Split(footprint_string, "x", [&dimension_strings](std::string&& s) { + dimension_strings.push_back(std::move(s)); + }); + + if (dimension_strings.size() != 2) { + assert(false && "Invalid format for footprint"); + return {}; + } + + const int width = base::ParseInt32(dimension_strings[0].c_str(), 0); + const int height = base::ParseInt32(dimension_strings[1].c_str(), 0); + + assert(width > 0 && height > 0 && "Invalid width or height."); + + return FromDimensions(width, height); +} + +base::Optional Footprint::FromDimensions(int width, int height) { + base::Optional valid_footprint = + GetValidFootprintForDimensions(width, height); + if (valid_footprint) { + return Footprint(valid_footprint.value()); + } else { + return {}; + } +} + +// Returns a Footprint for the given FootprintType. +base::Optional Footprint::FromFootprintType(FootprintType type) { + if (type >= FootprintType::k4x4 && type < FootprintType::kCount) { + return Footprint(type); + } else { + return {}; + } +} + +size_t Footprint::StorageRequirements(int width, int height) const { + const int blocks_wide = (width + width_ - 1) / width_; + const int blocks_high = (height + height_ - 1) / height_; + + constexpr size_t kASTCBlockSizeInBytes = 16; + return blocks_wide * blocks_high * kASTCBlockSizeInBytes; +} + +// Returns bits/pixel for a given footprint. +float Footprint::Bitrate() const { + const int kASTCBlockBitCount = 128; + const int footprint_pixel_count = width_ * height_; + return static_cast(kASTCBlockBitCount) / + static_cast(footprint_pixel_count); +} + +} // namespace astc_codec diff --git a/src/decoder/footprint.h b/src/decoder/footprint.h new file mode 100644 index 0000000..47302cc --- /dev/null +++ b/src/decoder/footprint.h @@ -0,0 +1,106 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_FOOTPRINT_H_ +#define ASTC_CODEC_DECODER_FOOTPRINT_H_ + +#include "include/astc-codec/astc-codec.h" +#include "src/base/optional.h" + +#include + +namespace astc_codec { + +// An ASTC texture can be encoded with varying choices in block size. A set of +// predefined block sizes are specified in the ASTC specification. These are +// referred to in the literature as "footprints" available to an encoder when +// constructing an ASTC bitstream. This class provides various utility functions +// for interacting with these footprints. +class Footprint { + public: + Footprint() = delete; + Footprint(const Footprint& footprint) = default; + + // Return the footprint type. + FootprintType Type() const { return footprint_; } + + // Return logical descriptions of the dimensions. + int Width() const { return width_; } + int Height() const { return height_; } + + // Returns the number of pixels for a block with this footprint. + int NumPixels() const { return width_ * height_; } + + // Returns the number of bytes needed to store an ASTC encoded image with the + // given width and height. + size_t StorageRequirements(int width, int height) const; + + // Returns the number of bits used per pixel. + float Bitrate() const; + + static constexpr int NumValidFootprints() { + return static_cast(FootprintType::kCount); + } + + bool operator==(const Footprint& other) const { + return footprint_ == other.footprint_; + } + + // These are the valid and available ASTC footprints. + static Footprint Get4x4() { return Footprint(FootprintType::k4x4); } + static Footprint Get5x4() { return Footprint(FootprintType::k5x4); } + static Footprint Get5x5() { return Footprint(FootprintType::k5x5); } + static Footprint Get6x5() { return Footprint(FootprintType::k6x5); } + static Footprint Get6x6() { return Footprint(FootprintType::k6x6); } + static Footprint Get8x5() { return Footprint(FootprintType::k8x5); } + static Footprint Get8x6() { return Footprint(FootprintType::k8x6); } + static Footprint Get8x8() { return Footprint(FootprintType::k8x8); } + static Footprint Get10x5() { return Footprint(FootprintType::k10x5); } + static Footprint Get10x6() { return Footprint(FootprintType::k10x6); } + static Footprint Get10x8() { return Footprint(FootprintType::k10x8); } + static Footprint Get10x10() { return Footprint(FootprintType::k10x10); } + static Footprint Get12x10() { return Footprint(FootprintType::k12x10); } + static Footprint Get12x12() { return Footprint(FootprintType::k12x12); } + + // Constructs a footprint from a string of the form "NxM", or no value if + // width and height are not a valid footprint. + static base::Optional Parse(const char* footprint_string); + + // Returns a footprint corresponding to a block of the given width and height, + // or no value if it does not. + static base::Optional FromDimensions(int width, int height); + + // Returns a Footprint for the given FootprintType. + static base::Optional FromFootprintType(FootprintType type); + + private: + // The only constructor. + explicit Footprint(FootprintType footprint); + + // Returns the valid footprint for the width and height if possible. + static base::Optional GetValidFootprintForDimensions( + int width, int height); + + // Returns the associated dimension for the given valid footprint. + static int GetWidthForFootprint(FootprintType footprint); + static int GetHeightForFootprint(FootprintType footprint); + + FootprintType footprint_; + int width_; + int height_; +}; + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_FOOTPRINT_H_ diff --git a/src/decoder/integer_sequence_codec.cc b/src/decoder/integer_sequence_codec.cc new file mode 100644 index 0000000..da7bc56 --- /dev/null +++ b/src/decoder/integer_sequence_codec.cc @@ -0,0 +1,562 @@ +// 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/math_utils.h" + +#include +#include + +namespace astc_codec { + +namespace { + +// Tables of trit and quint encodings generated by the implementation in +// http://cs/aosp-master/external/skia/src/utils/SkTextureCompressor_ASTC.cpp +// +// These tables are used to decode the blocks of values encoded using the ASTC +// integer sequence encoding. The theory is that five trits (values that can +// take any number in the range [0, 2]) can take on a total of 3^5 = 243 total +// values, which can be stored in eight bits. These eight bits are used to +// decode the five trits based on the ASTC specification in Section C.2.12. +// For simplicity, we have stored a look-up table here so that we don't need +// to implement the decoding logic. Similarly, seven bits are used to decode +// three quints (since 5^3 = 125 < 128). +static const std::array kTritEncodings[256] = { + {{ 0, 0, 0, 0, 0 }}, {{ 1, 0, 0, 0, 0 }}, {{ 2, 0, 0, 0, 0 }}, + {{ 0, 0, 2, 0, 0 }}, {{ 0, 1, 0, 0, 0 }}, {{ 1, 1, 0, 0, 0 }}, + {{ 2, 1, 0, 0, 0 }}, {{ 1, 0, 2, 0, 0 }}, {{ 0, 2, 0, 0, 0 }}, + {{ 1, 2, 0, 0, 0 }}, {{ 2, 2, 0, 0, 0 }}, {{ 2, 0, 2, 0, 0 }}, + {{ 0, 2, 2, 0, 0 }}, {{ 1, 2, 2, 0, 0 }}, {{ 2, 2, 2, 0, 0 }}, + {{ 2, 0, 2, 0, 0 }}, {{ 0, 0, 1, 0, 0 }}, {{ 1, 0, 1, 0, 0 }}, + {{ 2, 0, 1, 0, 0 }}, {{ 0, 1, 2, 0, 0 }}, {{ 0, 1, 1, 0, 0 }}, + {{ 1, 1, 1, 0, 0 }}, {{ 2, 1, 1, 0, 0 }}, {{ 1, 1, 2, 0, 0 }}, + {{ 0, 2, 1, 0, 0 }}, {{ 1, 2, 1, 0, 0 }}, {{ 2, 2, 1, 0, 0 }}, + {{ 2, 1, 2, 0, 0 }}, {{ 0, 0, 0, 2, 2 }}, {{ 1, 0, 0, 2, 2 }}, + {{ 2, 0, 0, 2, 2 }}, {{ 0, 0, 2, 2, 2 }}, {{ 0, 0, 0, 1, 0 }}, + {{ 1, 0, 0, 1, 0 }}, {{ 2, 0, 0, 1, 0 }}, {{ 0, 0, 2, 1, 0 }}, + {{ 0, 1, 0, 1, 0 }}, {{ 1, 1, 0, 1, 0 }}, {{ 2, 1, 0, 1, 0 }}, + {{ 1, 0, 2, 1, 0 }}, {{ 0, 2, 0, 1, 0 }}, {{ 1, 2, 0, 1, 0 }}, + {{ 2, 2, 0, 1, 0 }}, {{ 2, 0, 2, 1, 0 }}, {{ 0, 2, 2, 1, 0 }}, + {{ 1, 2, 2, 1, 0 }}, {{ 2, 2, 2, 1, 0 }}, {{ 2, 0, 2, 1, 0 }}, + {{ 0, 0, 1, 1, 0 }}, {{ 1, 0, 1, 1, 0 }}, {{ 2, 0, 1, 1, 0 }}, + {{ 0, 1, 2, 1, 0 }}, {{ 0, 1, 1, 1, 0 }}, {{ 1, 1, 1, 1, 0 }}, + {{ 2, 1, 1, 1, 0 }}, {{ 1, 1, 2, 1, 0 }}, {{ 0, 2, 1, 1, 0 }}, + {{ 1, 2, 1, 1, 0 }}, {{ 2, 2, 1, 1, 0 }}, {{ 2, 1, 2, 1, 0 }}, + {{ 0, 1, 0, 2, 2 }}, {{ 1, 1, 0, 2, 2 }}, {{ 2, 1, 0, 2, 2 }}, + {{ 1, 0, 2, 2, 2 }}, {{ 0, 0, 0, 2, 0 }}, {{ 1, 0, 0, 2, 0 }}, + {{ 2, 0, 0, 2, 0 }}, {{ 0, 0, 2, 2, 0 }}, {{ 0, 1, 0, 2, 0 }}, + {{ 1, 1, 0, 2, 0 }}, {{ 2, 1, 0, 2, 0 }}, {{ 1, 0, 2, 2, 0 }}, + {{ 0, 2, 0, 2, 0 }}, {{ 1, 2, 0, 2, 0 }}, {{ 2, 2, 0, 2, 0 }}, + {{ 2, 0, 2, 2, 0 }}, {{ 0, 2, 2, 2, 0 }}, {{ 1, 2, 2, 2, 0 }}, + {{ 2, 2, 2, 2, 0 }}, {{ 2, 0, 2, 2, 0 }}, {{ 0, 0, 1, 2, 0 }}, + {{ 1, 0, 1, 2, 0 }}, {{ 2, 0, 1, 2, 0 }}, {{ 0, 1, 2, 2, 0 }}, + {{ 0, 1, 1, 2, 0 }}, {{ 1, 1, 1, 2, 0 }}, {{ 2, 1, 1, 2, 0 }}, + {{ 1, 1, 2, 2, 0 }}, {{ 0, 2, 1, 2, 0 }}, {{ 1, 2, 1, 2, 0 }}, + {{ 2, 2, 1, 2, 0 }}, {{ 2, 1, 2, 2, 0 }}, {{ 0, 2, 0, 2, 2 }}, + {{ 1, 2, 0, 2, 2 }}, {{ 2, 2, 0, 2, 2 }}, {{ 2, 0, 2, 2, 2 }}, + {{ 0, 0, 0, 0, 2 }}, {{ 1, 0, 0, 0, 2 }}, {{ 2, 0, 0, 0, 2 }}, + {{ 0, 0, 2, 0, 2 }}, {{ 0, 1, 0, 0, 2 }}, {{ 1, 1, 0, 0, 2 }}, + {{ 2, 1, 0, 0, 2 }}, {{ 1, 0, 2, 0, 2 }}, {{ 0, 2, 0, 0, 2 }}, + {{ 1, 2, 0, 0, 2 }}, {{ 2, 2, 0, 0, 2 }}, {{ 2, 0, 2, 0, 2 }}, + {{ 0, 2, 2, 0, 2 }}, {{ 1, 2, 2, 0, 2 }}, {{ 2, 2, 2, 0, 2 }}, + {{ 2, 0, 2, 0, 2 }}, {{ 0, 0, 1, 0, 2 }}, {{ 1, 0, 1, 0, 2 }}, + {{ 2, 0, 1, 0, 2 }}, {{ 0, 1, 2, 0, 2 }}, {{ 0, 1, 1, 0, 2 }}, + {{ 1, 1, 1, 0, 2 }}, {{ 2, 1, 1, 0, 2 }}, {{ 1, 1, 2, 0, 2 }}, + {{ 0, 2, 1, 0, 2 }}, {{ 1, 2, 1, 0, 2 }}, {{ 2, 2, 1, 0, 2 }}, + {{ 2, 1, 2, 0, 2 }}, {{ 0, 2, 2, 2, 2 }}, {{ 1, 2, 2, 2, 2 }}, + {{ 2, 2, 2, 2, 2 }}, {{ 2, 0, 2, 2, 2 }}, {{ 0, 0, 0, 0, 1 }}, + {{ 1, 0, 0, 0, 1 }}, {{ 2, 0, 0, 0, 1 }}, {{ 0, 0, 2, 0, 1 }}, + {{ 0, 1, 0, 0, 1 }}, {{ 1, 1, 0, 0, 1 }}, {{ 2, 1, 0, 0, 1 }}, + {{ 1, 0, 2, 0, 1 }}, {{ 0, 2, 0, 0, 1 }}, {{ 1, 2, 0, 0, 1 }}, + {{ 2, 2, 0, 0, 1 }}, {{ 2, 0, 2, 0, 1 }}, {{ 0, 2, 2, 0, 1 }}, + {{ 1, 2, 2, 0, 1 }}, {{ 2, 2, 2, 0, 1 }}, {{ 2, 0, 2, 0, 1 }}, + {{ 0, 0, 1, 0, 1 }}, {{ 1, 0, 1, 0, 1 }}, {{ 2, 0, 1, 0, 1 }}, + {{ 0, 1, 2, 0, 1 }}, {{ 0, 1, 1, 0, 1 }}, {{ 1, 1, 1, 0, 1 }}, + {{ 2, 1, 1, 0, 1 }}, {{ 1, 1, 2, 0, 1 }}, {{ 0, 2, 1, 0, 1 }}, + {{ 1, 2, 1, 0, 1 }}, {{ 2, 2, 1, 0, 1 }}, {{ 2, 1, 2, 0, 1 }}, + {{ 0, 0, 1, 2, 2 }}, {{ 1, 0, 1, 2, 2 }}, {{ 2, 0, 1, 2, 2 }}, + {{ 0, 1, 2, 2, 2 }}, {{ 0, 0, 0, 1, 1 }}, {{ 1, 0, 0, 1, 1 }}, + {{ 2, 0, 0, 1, 1 }}, {{ 0, 0, 2, 1, 1 }}, {{ 0, 1, 0, 1, 1 }}, + {{ 1, 1, 0, 1, 1 }}, {{ 2, 1, 0, 1, 1 }}, {{ 1, 0, 2, 1, 1 }}, + {{ 0, 2, 0, 1, 1 }}, {{ 1, 2, 0, 1, 1 }}, {{ 2, 2, 0, 1, 1 }}, + {{ 2, 0, 2, 1, 1 }}, {{ 0, 2, 2, 1, 1 }}, {{ 1, 2, 2, 1, 1 }}, + {{ 2, 2, 2, 1, 1 }}, {{ 2, 0, 2, 1, 1 }}, {{ 0, 0, 1, 1, 1 }}, + {{ 1, 0, 1, 1, 1 }}, {{ 2, 0, 1, 1, 1 }}, {{ 0, 1, 2, 1, 1 }}, + {{ 0, 1, 1, 1, 1 }}, {{ 1, 1, 1, 1, 1 }}, {{ 2, 1, 1, 1, 1 }}, + {{ 1, 1, 2, 1, 1 }}, {{ 0, 2, 1, 1, 1 }}, {{ 1, 2, 1, 1, 1 }}, + {{ 2, 2, 1, 1, 1 }}, {{ 2, 1, 2, 1, 1 }}, {{ 0, 1, 1, 2, 2 }}, + {{ 1, 1, 1, 2, 2 }}, {{ 2, 1, 1, 2, 2 }}, {{ 1, 1, 2, 2, 2 }}, + {{ 0, 0, 0, 2, 1 }}, {{ 1, 0, 0, 2, 1 }}, {{ 2, 0, 0, 2, 1 }}, + {{ 0, 0, 2, 2, 1 }}, {{ 0, 1, 0, 2, 1 }}, {{ 1, 1, 0, 2, 1 }}, + {{ 2, 1, 0, 2, 1 }}, {{ 1, 0, 2, 2, 1 }}, {{ 0, 2, 0, 2, 1 }}, + {{ 1, 2, 0, 2, 1 }}, {{ 2, 2, 0, 2, 1 }}, {{ 2, 0, 2, 2, 1 }}, + {{ 0, 2, 2, 2, 1 }}, {{ 1, 2, 2, 2, 1 }}, {{ 2, 2, 2, 2, 1 }}, + {{ 2, 0, 2, 2, 1 }}, {{ 0, 0, 1, 2, 1 }}, {{ 1, 0, 1, 2, 1 }}, + {{ 2, 0, 1, 2, 1 }}, {{ 0, 1, 2, 2, 1 }}, {{ 0, 1, 1, 2, 1 }}, + {{ 1, 1, 1, 2, 1 }}, {{ 2, 1, 1, 2, 1 }}, {{ 1, 1, 2, 2, 1 }}, + {{ 0, 2, 1, 2, 1 }}, {{ 1, 2, 1, 2, 1 }}, {{ 2, 2, 1, 2, 1 }}, + {{ 2, 1, 2, 2, 1 }}, {{ 0, 2, 1, 2, 2 }}, {{ 1, 2, 1, 2, 2 }}, + {{ 2, 2, 1, 2, 2 }}, {{ 2, 1, 2, 2, 2 }}, {{ 0, 0, 0, 1, 2 }}, + {{ 1, 0, 0, 1, 2 }}, {{ 2, 0, 0, 1, 2 }}, {{ 0, 0, 2, 1, 2 }}, + {{ 0, 1, 0, 1, 2 }}, {{ 1, 1, 0, 1, 2 }}, {{ 2, 1, 0, 1, 2 }}, + {{ 1, 0, 2, 1, 2 }}, {{ 0, 2, 0, 1, 2 }}, {{ 1, 2, 0, 1, 2 }}, + {{ 2, 2, 0, 1, 2 }}, {{ 2, 0, 2, 1, 2 }}, {{ 0, 2, 2, 1, 2 }}, + {{ 1, 2, 2, 1, 2 }}, {{ 2, 2, 2, 1, 2 }}, {{ 2, 0, 2, 1, 2 }}, + {{ 0, 0, 1, 1, 2 }}, {{ 1, 0, 1, 1, 2 }}, {{ 2, 0, 1, 1, 2 }}, + {{ 0, 1, 2, 1, 2 }}, {{ 0, 1, 1, 1, 2 }}, {{ 1, 1, 1, 1, 2 }}, + {{ 2, 1, 1, 1, 2 }}, {{ 1, 1, 2, 1, 2 }}, {{ 0, 2, 1, 1, 2 }}, + {{ 1, 2, 1, 1, 2 }}, {{ 2, 2, 1, 1, 2 }}, {{ 2, 1, 2, 1, 2 }}, + {{ 0, 2, 2, 2, 2 }}, {{ 1, 2, 2, 2, 2 }}, {{ 2, 2, 2, 2, 2 }}, + {{ 2, 1, 2, 2, 2 }} +}; + +static const std::array kQuintEncodings[128] = { + {{ 0, 0, 0 }}, {{ 1, 0, 0 }}, {{ 2, 0, 0 }}, {{ 3, 0, 0 }}, {{ 4, 0, 0 }}, + {{ 0, 4, 0 }}, {{ 4, 4, 0 }}, {{ 4, 4, 4 }}, {{ 0, 1, 0 }}, {{ 1, 1, 0 }}, + {{ 2, 1, 0 }}, {{ 3, 1, 0 }}, {{ 4, 1, 0 }}, {{ 1, 4, 0 }}, {{ 4, 4, 1 }}, + {{ 4, 4, 4 }}, {{ 0, 2, 0 }}, {{ 1, 2, 0 }}, {{ 2, 2, 0 }}, {{ 3, 2, 0 }}, + {{ 4, 2, 0 }}, {{ 2, 4, 0 }}, {{ 4, 4, 2 }}, {{ 4, 4, 4 }}, {{ 0, 3, 0 }}, + {{ 1, 3, 0 }}, {{ 2, 3, 0 }}, {{ 3, 3, 0 }}, {{ 4, 3, 0 }}, {{ 3, 4, 0 }}, + {{ 4, 4, 3 }}, {{ 4, 4, 4 }}, {{ 0, 0, 1 }}, {{ 1, 0, 1 }}, {{ 2, 0, 1 }}, + {{ 3, 0, 1 }}, {{ 4, 0, 1 }}, {{ 0, 4, 1 }}, {{ 4, 0, 4 }}, {{ 0, 4, 4 }}, + {{ 0, 1, 1 }}, {{ 1, 1, 1 }}, {{ 2, 1, 1 }}, {{ 3, 1, 1 }}, {{ 4, 1, 1 }}, + {{ 1, 4, 1 }}, {{ 4, 1, 4 }}, {{ 1, 4, 4 }}, {{ 0, 2, 1 }}, {{ 1, 2, 1 }}, + {{ 2, 2, 1 }}, {{ 3, 2, 1 }}, {{ 4, 2, 1 }}, {{ 2, 4, 1 }}, {{ 4, 2, 4 }}, + {{ 2, 4, 4 }}, {{ 0, 3, 1 }}, {{ 1, 3, 1 }}, {{ 2, 3, 1 }}, {{ 3, 3, 1 }}, + {{ 4, 3, 1 }}, {{ 3, 4, 1 }}, {{ 4, 3, 4 }}, {{ 3, 4, 4 }}, {{ 0, 0, 2 }}, + {{ 1, 0, 2 }}, {{ 2, 0, 2 }}, {{ 3, 0, 2 }}, {{ 4, 0, 2 }}, {{ 0, 4, 2 }}, + {{ 2, 0, 4 }}, {{ 3, 0, 4 }}, {{ 0, 1, 2 }}, {{ 1, 1, 2 }}, {{ 2, 1, 2 }}, + {{ 3, 1, 2 }}, {{ 4, 1, 2 }}, {{ 1, 4, 2 }}, {{ 2, 1, 4 }}, {{ 3, 1, 4 }}, + {{ 0, 2, 2 }}, {{ 1, 2, 2 }}, {{ 2, 2, 2 }}, {{ 3, 2, 2 }}, {{ 4, 2, 2 }}, + {{ 2, 4, 2 }}, {{ 2, 2, 4 }}, {{ 3, 2, 4 }}, {{ 0, 3, 2 }}, {{ 1, 3, 2 }}, + {{ 2, 3, 2 }}, {{ 3, 3, 2 }}, {{ 4, 3, 2 }}, {{ 3, 4, 2 }}, {{ 2, 3, 4 }}, + {{ 3, 3, 4 }}, {{ 0, 0, 3 }}, {{ 1, 0, 3 }}, {{ 2, 0, 3 }}, {{ 3, 0, 3 }}, + {{ 4, 0, 3 }}, {{ 0, 4, 3 }}, {{ 0, 0, 4 }}, {{ 1, 0, 4 }}, {{ 0, 1, 3 }}, + {{ 1, 1, 3 }}, {{ 2, 1, 3 }}, {{ 3, 1, 3 }}, {{ 4, 1, 3 }}, {{ 1, 4, 3 }}, + {{ 0, 1, 4 }}, {{ 1, 1, 4 }}, {{ 0, 2, 3 }}, {{ 1, 2, 3 }}, {{ 2, 2, 3 }}, + {{ 3, 2, 3 }}, {{ 4, 2, 3 }}, {{ 2, 4, 3 }}, {{ 0, 2, 4 }}, {{ 1, 2, 4 }}, + {{ 0, 3, 3 }}, {{ 1, 3, 3 }}, {{ 2, 3, 3 }}, {{ 3, 3, 3 }}, {{ 4, 3, 3 }}, + {{ 3, 4, 3 }}, {{ 0, 3, 4 }}, {{ 1, 3, 4 }} +}; + +// A cached table containing the max ranges for values encoded using ASTC's +// Bounded Integer Sequence Encoding. These are the numbers between 1 and 255 +// that can be represented exactly as a number in the ranges +// [0, 2^k), [0, 3 * 2^k), and [0, 5 * 2^k). +static const std::array kMaxRanges = []() { + std::array ranges; + + // Initialize the table that we need for determining value encodings. + auto next_max_range = ranges.begin(); + auto add_val = [&next_max_range](int val) { + if (val <= 0 || (1 << kLog2MaxRangeForBits) <= val) { + return; + } + + *(next_max_range++) = val; + }; + + for (int i = 0; i <= kLog2MaxRangeForBits; ++i) { + add_val(3 * (1 << i) - 1); + add_val(5 * (1 << i) - 1); + add_val((1 << i) - 1); + } + + assert(std::distance(next_max_range, ranges.end()) == 0); + std::sort(ranges.begin(), ranges.end()); + return ranges; +}(); + +// Returns true if x == 0 or if x is a power of two. This function is only used +// in the GetCountsForRange function, where we need to have it return true +// on zero since we can have single trit/quint ISE encodings according to +// Table C.2.7. +template::value, T>::type = 0> +inline constexpr bool IsPow2(T x) { return (x & (x - 1)) == 0; } + +// For the ISE block encoding, these arrays determine how many bits are +// used after each value to store the interleaved quint/trit block. +const int kInterleavedQuintBits[3] = { 3, 2, 2 }; +const int kInterleavedTritBits[5] = { 2, 2, 1, 2, 1 }; + +// Decodes either a trit or quint block using the BISE (Bounded Integer Sequence +// Encoding) defined in Section C.2.12 of the ASTC specification. ValRange is +// expected to be either 3 or 5 depending on whether or not we're encoding trits +// or quints respectively. In other words, it is the remaining factor in whether +// the passed blocks contain encoded values of the form 3*2^k or 5*2^k. +template +std::array DecodeISEBlock( + uint64_t block_bits, int num_bits) { + static_assert(ValRange == 3 || ValRange == 5, + "We only know about trits and quints"); + + // We either have three quints or five trits + constexpr const int kNumVals = (ValRange == 5) ? 3 : 5; + + // Depending on whether or not we're using quints or trits will determine + // the positions of the interleaved bits in the encoded block. + constexpr const int* const kInterleavedBits = + (ValRange == 5) ? kInterleavedQuintBits : kInterleavedTritBits; + + // Set up the bits for reading + base::BitStream block_bit_src(block_bits, sizeof(block_bits) * 8); + + // Decode the block + std::array m; + uint64_t encoded = 0; + uint32_t encoded_bits_read = 0; + for (int i = 0; i < kNumVals; ++i) { + { + uint64_t bits = 0; + const bool result = block_bit_src.GetBits(num_bits, &bits); + assert(result); + + m[i] = static_cast(bits); + } + + uint64_t encoded_bits; + { + const bool result = block_bit_src.GetBits(kInterleavedBits[i], &encoded_bits); + assert(result); + } + encoded |= encoded_bits << encoded_bits_read; + encoded_bits_read += kInterleavedBits[i]; + } + + // Make sure that our encoded trit/quint doesn't exceed its bounds + assert(ValRange != 3 || encoded < 256); + assert(ValRange != 5 || encoded < 128); + + const int* const kEncodings = (ValRange == 5) ? + kQuintEncodings[encoded].data() : kTritEncodings[encoded].data(); + + std::array result; + for (int i = 0; i < kNumVals; ++i) { + assert(m[i] < 1 << num_bits); + result[i] = kEncodings[i] << num_bits | m[i]; + } + return result; +} + +// Encode a single trit or quint block using the BISE (Bounded Integer Sequence +// Encoding) defined in Section C.2.12 of the ASTC specification. ValRange is +// expected to be either 3 or 5 depending on whether or not we're encoding trits +// or quints respectively. In other words, it is the remaining factor in whether +// the passed blocks contain encoded values of the form 3*2^k or 5*2^k. +template +void EncodeISEBlock(const std::vector& vals, int bits_per_val, + base::BitStream* bit_sink) { + static_assert(ValRange == 3 || ValRange == 5, + "We only know about trits and quints"); + + // We either have three quints or five trits + constexpr const int kNumVals = (ValRange == 5) ? 3 : 5; + + // Three quints in seven bits or five trits in eight bits + constexpr const int kNumEncodedBitsPerBlock = (ValRange == 5) ? 7 : 8; + + // Depending on whether or not we're using quints or trits will determine + // the positions of the interleaved bits in the encoding + constexpr const int* const kInterleavedBits = + (ValRange == 5) ? kInterleavedQuintBits : kInterleavedTritBits; + + // ISE blocks can only have up to a specific number of values... + assert(vals.size() <= kNumVals); + + // Split up into bits and non bits. Non bits are used to find the quint/trit + // encoding that we need. + std::array non_bits = {{ 0 }}; + std::array bits = {{ 0 }}; + for (size_t i = 0; i < vals.size(); ++i) { + bits[i] = vals[i] & ((1 << bits_per_val) - 1); + non_bits[i] = vals[i] >> bits_per_val; + assert(non_bits[i] < ValRange); + } + + // We only need to add as many bits as necessary, so let's limit it based + // on the computation described in Section C.2.22 of the ASTC specification + const int total_num_bits = + ((vals.size() * kNumEncodedBitsPerBlock + kNumVals - 1) / kNumVals) + + vals.size() * bits_per_val; + int bits_added = 0; + + // The number of bits used for the quint/trit encoding is necessary to know + // in order to properly select the encoding we need to represent. + int num_encoded_bits = 0; + for (int i = 0; i < kNumVals; ++i) { + bits_added += bits_per_val; + if (bits_added >= total_num_bits) { + break; + } + + num_encoded_bits += kInterleavedBits[i]; + bits_added += kInterleavedBits[i]; + if (bits_added >= total_num_bits) { + break; + } + } + bits_added = 0; + assert(num_encoded_bits <= kNumEncodedBitsPerBlock); + + // TODO(google): The faster way to do this would be to construct trees out + // of the quint/trit encoding patterns, or just invert the decoding logic. + // Here we go from the end backwards because it makes our tests are more + // deterministic. + int non_bit_encoding = -1; + for (int j = (1 << num_encoded_bits) - 1; j >= 0; --j) { + bool matches = true; + + // We don't need to match all trits here, just the ones that correspond + // to the values that we passed in + for (size_t i = 0; i < kNumVals; ++i) { + if ((ValRange == 5 && kQuintEncodings[j][i] != non_bits[i]) || + (ValRange == 3 && kTritEncodings[j][i] != non_bits[i])) { + matches = false; + break; + } + } + + if (matches) { + non_bit_encoding = j; + break; + } + } + + assert(non_bit_encoding >= 0); + + // Now pack the bits into the block + for (int i = 0; i < vals.size(); ++i) { + // First add the base bits for this value + if (bits_added + bits_per_val <= total_num_bits) { + bit_sink->PutBits(bits[i], bits_per_val); + bits_added += bits_per_val; + } + + // Now add the interleaved bits from the quint/trit + int num_int_bits = kInterleavedBits[i]; + int int_bits = non_bit_encoding & ((1 << num_int_bits) - 1); + if (bits_added + num_int_bits <= total_num_bits) { + bit_sink->PutBits(int_bits, num_int_bits); + bits_added += num_int_bits; + non_bit_encoding >>= num_int_bits; + } + } +} + +inline void CHECK_COUNTS(int trits, int quints) { + assert(trits == 0 || quints == 0); // Either trits or quints + assert(trits == 0 || trits == 1); // At most one trit + assert(quints == 0 || quints == 1); // At most one quint +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +std::array::const_iterator ISERangeBegin() { + return kMaxRanges.cbegin(); +} + +std::array::const_iterator ISERangeEnd() { + return kMaxRanges.cend(); +} + +void IntegerSequenceCodec::GetCountsForRange( + int range, int* const trits, int* const quints, int* const bits) { + // Make sure the passed pointers are valid + assert(trits != nullptr); + assert(quints != nullptr); + assert(bits != nullptr); + + // These are generally errors -- there should never be any ASTC values + // outside of this range + assert(range > 0); + assert(range < 1 << kLog2MaxRangeForBits); + + *bits = 0; + *trits = 0; + *quints = 0; + + // Search through the numbers of the form 2^n, 3 * 2^n and 5 * 2^n + const int max_vals_for_range = + *std::lower_bound(kMaxRanges.begin(), kMaxRanges.end(), range) + 1; + + // Make sure we found something + assert(max_vals_for_range > 1); + + // Find out what kind of range it is + if ((max_vals_for_range % 3 == 0) && IsPow2(max_vals_for_range / 3)) { + *bits = base::Log2Floor(max_vals_for_range / 3); + *trits = 1; + *quints = 0; + } else if ((max_vals_for_range % 5 == 0) && IsPow2(max_vals_for_range / 5)) { + *bits = base::Log2Floor(max_vals_for_range / 5); + *trits = 0; + *quints = 1; + } else if (IsPow2(max_vals_for_range)) { + *bits = base::Log2Floor(max_vals_for_range); + *trits = 0; + *quints = 0; + } + + // If we set any of these values then we're done. + if ((*bits | *trits | *quints) != 0) { + CHECK_COUNTS(*trits, *quints); + } +} + +// Returns the overall bit count for a range of val_count values encoded +// using the specified number of trits, quints and straight bits (respectively) +int IntegerSequenceCodec::GetBitCount(int num_vals, + int trits, int quints, int bits) { + CHECK_COUNTS(trits, quints); + + // See section C.2.22 for the formula used here. + const int trit_bit_count = ((num_vals * 8 * trits) + 4) / 5; + const int quint_bit_count = ((num_vals * 7 * quints) + 2) / 3; + const int base_bit_count = num_vals * bits; + return trit_bit_count + quint_bit_count + base_bit_count; +} + +IntegerSequenceCodec::IntegerSequenceCodec(int range) { + int trits, quints, bits; + GetCountsForRange(range, &trits, &quints, &bits); + InitializeWithCounts(trits, quints, bits); +} + +IntegerSequenceCodec::IntegerSequenceCodec( + int trits, int quints, int bits) { + InitializeWithCounts(trits, quints, bits); +} + +void IntegerSequenceCodec::InitializeWithCounts( + int trits, int quints, int bits) { + CHECK_COUNTS(trits, quints); + + if (trits > 0) { + encoding_ = EncodingMode::kTritEncoding; + } else if (quints > 0) { + encoding_ = EncodingMode::kQuintEncoding; + } else { + encoding_ = EncodingMode::kBitEncoding; + } + + bits_ = bits; +} + +int IntegerSequenceCodec::NumValsPerBlock() const { + const std::array kNumValsByEncoding = {{ 5, 3, 1 }}; + return kNumValsByEncoding[static_cast(encoding_)]; +} + +int IntegerSequenceCodec::EncodedBlockSize() const { + const std::array kExtraBlockSizeByEncoding = {{ 8, 7, 0 }}; + const int num_vals = NumValsPerBlock(); + return kExtraBlockSizeByEncoding[static_cast(encoding_)] + + num_vals * bits_; +} + +std::vector IntegerSequenceDecoder::Decode( + int num_vals, base::BitStream *bit_src) const { + int trits = (encoding_ == kTritEncoding)? 1 : 0; + int quints = (encoding_ == kQuintEncoding)? 1 : 0; + const int total_num_bits = GetBitCount(num_vals, trits, quints, bits_); + const int bits_per_block = EncodedBlockSize(); + assert(bits_per_block < 64); + + int bits_left = total_num_bits; + std::vector result; + while (bits_left > 0) { + uint64_t block_bits; + { + const bool result = bit_src->GetBits(std::min(bits_left, bits_per_block), &block_bits); + assert(result); + } + + switch (encoding_) { + case kTritEncoding: { + auto trit_vals = DecodeISEBlock<3>(block_bits, bits_); + result.insert(result.end(), trit_vals.begin(), trit_vals.end()); + } + break; + + case kQuintEncoding: { + auto quint_vals = DecodeISEBlock<5>(block_bits, bits_); + result.insert(result.end(), quint_vals.begin(), quint_vals.end()); + } + break; + + case kBitEncoding: + result.push_back(static_cast(block_bits)); + break; + } + + bits_left -= bits_per_block; + } + + // Resize result to only contain as many values as requested + assert(result.size() >= static_cast(num_vals)); + result.resize(num_vals); + + // Encoded all the values + return result; +} + +void IntegerSequenceEncoder::Encode(base::BitStream* bit_sink) const { + // Go through all of the values and chop them up into blocks. The properties + // of the trit and quint encodings mean that if we need to encode fewer values + // in a block than the number of values encoded in the block then we need to + // consider the last few values to be zero. + + auto next_val = vals_.begin(); + while (next_val != vals_.end()) { + switch (encoding_) { + case kTritEncoding: { + std::vector trit_vals; + for (int i = 0; i < 5; ++i) { + if (next_val != vals_.end()) { + trit_vals.push_back(*next_val); + ++next_val; + } + } + + EncodeISEBlock<3>(trit_vals, bits_, bit_sink); + } + break; + + case kQuintEncoding: { + std::vector quint_vals; + for (int i = 0; i < 3; ++i) { + if (next_val != vals_.end()) { + quint_vals.push_back(*next_val); + ++next_val; + } + } + + EncodeISEBlock<5>(quint_vals, bits_, bit_sink); + } + break; + + case kBitEncoding: { + bit_sink->PutBits(*next_val, EncodedBlockSize()); + ++next_val; + } + break; + } + } +} + +} // namespace astc_codec diff --git a/src/decoder/integer_sequence_codec.h b/src/decoder/integer_sequence_codec.h new file mode 100644 index 0000000..a815e09 --- /dev/null +++ b/src/decoder/integer_sequence_codec.h @@ -0,0 +1,169 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_INTEGER_SEQUENCE_CODEC_H_ +#define ASTC_CODEC_DECODER_INTEGER_SEQUENCE_CODEC_H_ + +#include "src/base/bit_stream.h" +#include "src/base/uint128.h" + +#include +#include +#include + +namespace astc_codec { + +// The maximum number of bits that we would need to encode an ISE value. The +// ASTC specification does not give a maximum number, however unquantized color +// values have a maximum range of 255, meaning that we can't feasibly have more +// than eight bits per value. +constexpr int kLog2MaxRangeForBits = 8; + +// Ranges can take any of the the forms 2^k, 3*2^k, or 5*2^k for k up to +// kLog2MaxRangeForBits. Hence we have three types of ranges. Since the +// maximum encoded value is 255, k won't go larger than 8. We don't have quints +// that accompany [6, 8]-bits, as (5 * 2^6 = 320 > 255) and we don't have trits +// that accompany [7, 8]-bits, as (3 * 2^7 = 384 > 255). But we do have trits +// and quints that accompany no bits. Hence we have a total of +// 3 * kLog2MaxRangeForBits - 3 - 2 + 2 total ranges. +constexpr int kNumPossibleRanges = 3 * kLog2MaxRangeForBits - 3; + +// Returns an iterator through the available ASTC ranges. +std::array::const_iterator ISERangeBegin(); +std::array::const_iterator ISERangeEnd(); + +// Base class for ASTC integer sequence encoders and decoders. These codecs +// operate on sequences of integers and produce bit patterns that pack the +// integers based on the encoding scheme specified in the ASTC specification +// Section C.2.12. The resulting bit pattern is a sequence of encoded blocks. +// All blocks in a sequence are one of the following encodings: +// +// (1 -- bit encoding) one encoded value of the form 2^k +// (2 -- trit encoding) five encoded values of the form 3*2^k +// (3 -- quint encoding) three encoded values of the form 5*2^k +// +// The layouts of each block are designed such that the blocks can be truncated +// during encoding in order to support variable length input sequences (i.e. a +// sequence of values that are encoded using trit encoded blocks does not +// need to have a multiple-of-five length). +class IntegerSequenceCodec { + public: + // Returns the number of trits, quints, and bits needed to encode values in + // [0, range]. This is used to determine the layout of ISE encoded bit + // streams. The returned array holds the number of trits, quints, and bits + // respectively. range is expected to be within the interval [1, 5242879] + static void GetCountsForRange(int range, int* trits, int* quints, int* bits); + + // Returns the number of bits needed to encode the given number of values with + // respect to the number of trits, quints, and bits specified in ise_counts + // (in that order). It is expected that either trits or quints can be + // nonzero, but not both, and neither can be larger than one. Anything else is + // undefined. + static int GetBitCount(int num_vals, int trits, int quints, int bits); + + // Convenience function that returns the number of bits needed to encoded + // num_vals within the range [0, range] (inclusive). + static inline int GetBitCountForRange(int num_vals, int range) { + int trits, quints, bits; + GetCountsForRange(range, &trits, &quints, &bits); + return GetBitCount(num_vals, trits, quints, bits); + } + + protected: + explicit IntegerSequenceCodec(int range); + IntegerSequenceCodec(int trits, int quints, int bits); + + // The encoding mode -- since having trits and quints are mutually exclusive, + // we can store the encoding we decide on in this enum. + enum EncodingMode { + kTritEncoding = 0, + kQuintEncoding, + kBitEncoding, + }; + + EncodingMode encoding_; + int bits_; + + // Returns the number of values stored in a single ISE block. Since quints and + // trits are packed three/five to a bit pattern (respectively), each sequence + // is chunked into blocks in order to encode it. For only bit-encodings, the + // block size is one. + int NumValsPerBlock() const; + + // Returns the size of a single ISE block in bits (see NumValsPerBlock). + int EncodedBlockSize() const; + + private: + // Determines the encoding mode. + void InitializeWithCounts(int trits, int quints, int bits); +}; + +// The integer sequence decoder. The decoder only remembers the given encoding +// but each invocation of Decode operates independently on the input bits. +class IntegerSequenceDecoder : public IntegerSequenceCodec { + public: + // Creates a decoder that decodes values within [0, range] (inclusive). + explicit IntegerSequenceDecoder(int range) + : IntegerSequenceCodec(range) { } + + // Creates a decoder based on the number of trits, quints, and bits expected + // in the bit stream passed to Decode. + IntegerSequenceDecoder(int trits, int quints, int bits) + : IntegerSequenceCodec(trits, quints, bits) { } + + // Decodes num_vals from the bit_src. The number of bits read is dependent + // on the number of bits required to encode num_vals based on the calculation + // provided in Section C.2.22 of the ASTC specification. The return value + // always contains exactly num_vals. + std::vector Decode(int num_vals, + base::BitStream* bit_src) const; +}; + +// The integer sequence encoder. The encoder accepts values one by one and +// places them into a temporary array that it holds. When needed the user +// may call Encode to produce an encoded bit stream of the associated values. +class IntegerSequenceEncoder : public IntegerSequenceCodec { + public: + // Creates an encoder that encodes values within [0, range] (inclusive). + explicit IntegerSequenceEncoder(int range) + : IntegerSequenceCodec(range) { } + + // Creates an encoder based on the number of trits, quints, and bits for + // the bit stream produced by Encode. + IntegerSequenceEncoder(int trits, int quints, int bits) + : IntegerSequenceCodec(trits, quints, bits) { } + + // Adds a value to the encoding sequence. + void AddValue(int val) { + // Make sure it's within bounds + assert(encoding_ != EncodingMode::kTritEncoding || val < 3 * (1 << bits_)); + assert(encoding_ != EncodingMode::kQuintEncoding || val < 5 * (1 << bits_)); + assert(encoding_ != EncodingMode::kBitEncoding || val < (1 << bits_)); + vals_.push_back(val); + } + + // Writes the encoding for vals_ to the bit_sink. Multiple calls to Encode + // will produce the same result. + void Encode(base::BitStream* bit_sink) const; + + // Removes all of the previously added values to the encoder. + void Reset() { vals_.clear(); } + + private: + std::vector vals_; +}; + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_INTEGER_SEQUENCE_CODEC_H_ diff --git a/src/decoder/intermediate_astc_block.cc b/src/decoder/intermediate_astc_block.cc new file mode 100644 index 0000000..e03af1e --- /dev/null +++ b/src/decoder/intermediate_astc_block.cc @@ -0,0 +1,591 @@ +// 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/integer_sequence_codec.h" +#include "src/base/bit_stream.h" +#include "src/base/math_utils.h" +#include "src/base/optional.h" +#include "src/base/uint128.h" + +#include +#include +#include + +namespace astc_codec { + +namespace { + +constexpr int kEndpointRange_ReturnInvalidWeightDims = -1; +constexpr int kEndpointRange_ReturnNotEnoughColorBits = -2; + +base::UInt128 PackVoidExtentBlock(uint16_t r, uint16_t g, uint16_t b, + uint16_t a, std::array coords) { + base::BitStream bit_sink; + + // Put void extent mode... + bit_sink.PutBits(0xDFC, 12); + + // Each of the coordinates goes in 13 bits at a time. + for (auto coord : coords) { + assert(coord < 1 << 13); + bit_sink.PutBits(coord, 13); + } + assert(bit_sink.Bits() == 64); + + // Then we add R, G, B, and A in order + bit_sink.PutBits(r, 16); + bit_sink.PutBits(g, 16); + bit_sink.PutBits(b, 16); + bit_sink.PutBits(a, 16); + + assert(bit_sink.Bits() == 128); + + base::UInt128 result; + bit_sink.GetBits(128, &result); + return result; +} + +base::Optional GetEncodedWeightRange(int range, + std::array* const r) { + const std::array, 12> kValidRangeEncodings = + {{ {{ 0, 1, 0 }}, {{ 1, 1, 0 }}, {{ 0, 0, 1 }}, + {{ 1, 0, 1 }}, {{ 0, 1, 1 }}, {{ 1, 1, 1 }}, + {{ 0, 1, 0 }}, {{ 1, 1, 0 }}, {{ 0, 0, 1 }}, + {{ 1, 0, 1 }}, {{ 0, 1, 1 }}, {{ 1, 1, 1 }} }}; + + // If our range is larger than all available ranges, this is an error. + const int smallest_range = kValidWeightRanges.front(); + const int largest_range = kValidWeightRanges.back(); + if (range < smallest_range || largest_range < range) { + std::stringstream strm; + strm << "Could not find block mode. Invalid weight range: " + << range << " not in [" << smallest_range << ", " + << largest_range << std::endl; + return strm.str(); + } + + // Find the upper bound on the range, otherwise. + const auto range_iter = std::lower_bound( + kValidWeightRanges.cbegin(), kValidWeightRanges.cend(), range); + auto enc_iter = kValidRangeEncodings.cbegin(); + enc_iter += std::distance(kValidWeightRanges.cbegin(), range_iter); + *r = *enc_iter; + return {}; +} + +struct BlockModeInfo { + int min_weight_grid_dim_x; + int max_weight_grid_dim_x; + int min_weight_grid_dim_y; + int max_weight_grid_dim_y; + int r0_bit_pos; + int r1_bit_pos; + int r2_bit_pos; + int weight_grid_x_offset_bit_pos; + int weight_grid_y_offset_bit_pos; + bool require_single_plane_low_prec; +}; + +constexpr int kNumBlockModes = 10; +const std::array kBlockModeInfo {{ + { 4, 7, 2, 5, 4, 0, 1, 7, 5, false }, // B+4 A+2 + { 8, 11, 2, 5, 4, 0, 1, 7, 5, false }, // B+8 A+2 + { 2, 5, 8, 11, 4, 0, 1, 5, 7, false }, // A+2 B+8 + { 2, 5, 6, 7, 4, 0, 1, 5, 7, false }, // A+2 B+6 + { 2, 3, 2, 5, 4, 0, 1, 7, 5, false }, // B+2 A+2 + { 12, 12, 2, 5, 4, 2, 3, -1, 5, false }, // 12 A+2 + { 2, 5, 12, 12, 4, 2, 3, 5, -1, false }, // A+2 12 + { 6, 6, 10, 10, 4, 2, 3, -1, -1, false }, // 6 10 + { 10, 10, 6, 6, 4, 2, 3, -1, -1, false }, // 10 6 + { 6, 9, 6, 9, 4, 2, 3, 5, 9, true } // A+6 B+6 +}}; + +// These are the bits that must be set for ASTC to recognize a given +// block mode. They are the 1's set in table C.2.8 of the spec. +const std::array kBlockModeMask = {{ + 0x0, 0x4, 0x8, 0xC, 0x10C, 0x0, 0x80, 0x180, 0x1A0, 0x100 +}}; + +static base::Optional PackBlockMode(int dim_x, int dim_y, int range, + bool dual_plane, + base::BitStream* const bit_sink) { + // We need to set the high precision bit if our range is too high... + bool high_prec = range > 7; + + std::array r; + const auto result = GetEncodedWeightRange(range, &r); + if (result) { + return result; + } + + // The high two bits of R must not be zero. If this happens then it's + // an illegal encoding according to Table C.2.7 that should have gotten + // caught in GetEncodedWeightRange + assert((r[1] | r[2]) > 0); + + // Just go through the table and see if any of the modes can handle + // the given dimensions. + for (int mode = 0; mode < kNumBlockModes; ++mode) { + const BlockModeInfo& block_mode = kBlockModeInfo[mode]; + + bool is_valid_mode = true; + is_valid_mode &= block_mode.min_weight_grid_dim_x <= dim_x; + is_valid_mode &= dim_x <= block_mode.max_weight_grid_dim_x; + is_valid_mode &= block_mode.min_weight_grid_dim_y <= dim_y; + is_valid_mode &= dim_y <= block_mode.max_weight_grid_dim_y; + is_valid_mode &= !(block_mode.require_single_plane_low_prec && dual_plane); + is_valid_mode &= !(block_mode.require_single_plane_low_prec && high_prec); + + if (!is_valid_mode) { + continue; + } + + // Initialize to the bits we must set. + uint32_t encoded_mode = kBlockModeMask[mode]; + auto setBit = [&encoded_mode](const uint32_t value, const uint32_t offset) { + encoded_mode = (encoded_mode & ~(1 << offset)) | ((value & 1) << offset); + }; + + // Set all the bits we need to set + setBit(r[0], block_mode.r0_bit_pos); + setBit(r[1], block_mode.r1_bit_pos); + setBit(r[2], block_mode.r2_bit_pos); + + // Find our width and height offset from the base width and height weight + // grid dimension for the given block mode. These are the 1-2 bits that + // get encoded in the block mode used to calculate the final weight grid + // width and height. + const int offset_x = dim_x - block_mode.min_weight_grid_dim_x; + const int offset_y = dim_y - block_mode.min_weight_grid_dim_y; + + // If we don't have an offset position then our offset better be zero. + // If this isn't the case, then this isn't a viable block mode and we + // should have caught this sooner. + assert(block_mode.weight_grid_x_offset_bit_pos >= 0 || offset_x == 0); + assert(block_mode.weight_grid_y_offset_bit_pos >= 0 || offset_y == 0); + + encoded_mode |= offset_x << block_mode.weight_grid_x_offset_bit_pos; + encoded_mode |= offset_y << block_mode.weight_grid_y_offset_bit_pos; + + if (!block_mode.require_single_plane_low_prec) { + setBit(high_prec, 9); + setBit(dual_plane, 10); + } + + // Make sure that the mode is the first thing the bit sink is writing to + assert(bit_sink->Bits() == 0); + bit_sink->PutBits(encoded_mode, 11); + + return {}; + } + + return std::string("Could not find viable block mode"); +} + +// Returns true if all endpoint modes are equal. +bool SharedEndpointModes(const IntermediateBlockData& data) { + return std::accumulate( + data.endpoints.begin(), data.endpoints.end(), true, + [&data](const bool& a, const IntermediateEndpointData& b) { + return a && b.mode == data.endpoints[0].mode; + }); +} + +// Returns the starting bit (between 0 and 128) where the extra CEM and +// dual plane info is stored in the ASTC block. +int ExtraConfigBitPosition(const IntermediateBlockData& data) { + const bool has_dual_channel = data.dual_plane_channel.hasValue(); + const int num_weights = data.weight_grid_dim_x * data.weight_grid_dim_y * + (has_dual_channel ? 2 : 1); + const int num_weight_bits = + IntegerSequenceCodec::GetBitCountForRange(num_weights, data.weight_range); + + int extra_config_bits = 0; + if (!SharedEndpointModes(data)) { + const int num_encoded_cem_bits = 2 + data.endpoints.size() * 3; + extra_config_bits = num_encoded_cem_bits - 6; + } + + if (has_dual_channel) { + extra_config_bits += 2; + } + + return 128 - num_weight_bits - extra_config_bits; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +base::Optional UnpackIntermediateBlock( + const PhysicalASTCBlock& pb) { + if (pb.IsIllegalEncoding()) { + return {}; + } + + if (pb.IsVoidExtent()) { + return {}; + } + + // Non void extent? Then let's try to decode everything else. + IntermediateBlockData data; + + // All blocks have color values... + const base::UInt128 color_bits_mask = + (base::UInt128(1) << pb.NumColorBits().value()) - 1; + const base::UInt128 color_bits = + (pb.GetBlockBits() >> pb.ColorStartBit().value()) & color_bits_mask; + base::BitStream bit_src(color_bits, 128); + + IntegerSequenceDecoder color_decoder(pb.ColorValuesRange().value()); + const int num_colors_in_block = pb.NumColorValues().value(); + std::vector colors = color_decoder.Decode(num_colors_in_block, &bit_src); + + // Decode simple info + const auto weight_dims = pb.WeightGridDims(); + data.weight_grid_dim_x = weight_dims->at(0); + data.weight_grid_dim_y = weight_dims->at(1); + data.weight_range = pb.WeightRange().value(); + + data.partition_id = pb.PartitionID(); + data.dual_plane_channel = pb.DualPlaneChannel(); + + auto colors_iter = colors.begin(); + for (int i = 0; i < pb.NumPartitions().value(); ++i) { + IntermediateEndpointData ep_data; + ep_data.mode = pb.GetEndpointMode(i).value(); + + const int num_colors = NumColorValuesForEndpointMode(ep_data.mode); + ep_data.colors.insert(ep_data.colors.end(), colors_iter, + colors_iter + num_colors); + colors_iter += num_colors; + + data.endpoints.push_back(ep_data); + } + assert(colors_iter == colors.end()); + data.endpoint_range = pb.ColorValuesRange().value(); + + // Finally decode the weights + const base::UInt128 weight_bits_mask = + (base::UInt128(1) << pb.NumWeightBits().value()) - 1; + const base::UInt128 weight_bits = + base::ReverseBits(pb.GetBlockBits()) & weight_bits_mask; + bit_src = base::BitStream(weight_bits, 128); + + IntegerSequenceDecoder weight_decoder(data.weight_range); + int num_weights = data.weight_grid_dim_x * data.weight_grid_dim_y; + num_weights *= pb.IsDualPlane() ? 2 : 1; + data.weights = weight_decoder.Decode(num_weights, &bit_src); + + return data; +} + +int EndpointRangeForBlock(const IntermediateBlockData& data) { + // First check to see if we exceed the number of bits allotted for weights, as + // specified in C.2.24. If so, then the endpoint range is meaningless, but not + // because we had an overzealous color endpoint mode, so return a different + // error code. + if (IntegerSequenceCodec::GetBitCountForRange( + data.weight_grid_dim_x * data.weight_grid_dim_y * + (data.dual_plane_channel.hasValue() ? 2 : 1), + data.weight_range) > 96) { + return kEndpointRange_ReturnInvalidWeightDims; + } + + const int num_partitions = data.endpoints.size(); + + // Calculate the number of bits that we would write prior to getting to the + // color endpoint data + const int bits_written = + 11 // Block mode + + 2 // Num partitions + + ((num_partitions > 1) ? 10 : 0) // Partition ID + + ((num_partitions == 1) ? 4 : 6); // Shared CEM bits + + // We can determine the range based on how many bits we have between the start + // of the color endpoint data and the next section, which is the extra config + // bit position + const int color_bits_available = ExtraConfigBitPosition(data) - bits_written; + + int num_color_values = 0; + for (const auto& ep_data : data.endpoints) { + num_color_values += NumColorValuesForEndpointMode(ep_data.mode); + } + + // There's no way any valid ASTC encoding has no room left for any color + // values. If we hit this then something is wrong in the caller -- abort. + // According to section C.2.24, the smallest number of bits available is + // ceil(13*C/5), where C is the number of color endpoint integers needed. + const int bits_needed = (13 * num_color_values + 4) / 5; + if (color_bits_available < bits_needed) { + return kEndpointRange_ReturnNotEnoughColorBits; + } + + int color_value_range = 255; + for (; color_value_range > 1; --color_value_range) { + const int bits_for_range = IntegerSequenceCodec::GetBitCountForRange( + num_color_values, color_value_range); + if (bits_for_range <= color_bits_available) { + break; + } + } + + return color_value_range; +} + +base::Optional UnpackVoidExtent(const PhysicalASTCBlock& pb) { + if (pb.IsIllegalEncoding()) { + return {}; + } + + if (!pb.IsVoidExtent()) { + return {}; + } + + // All blocks have color values... + const base::UInt128 color_bits_mask = + (base::UInt128(1) << pb.NumColorBits().value()) - 1; + const uint64_t color_bits = ( + (pb.GetBlockBits() >> pb.ColorStartBit().value()) & color_bits_mask).LowBits(); + + assert(pb.NumColorValues().value() == 4); + VoidExtentData data; + data.r = static_cast((color_bits >> 0) & 0xFFFF); + data.g = static_cast((color_bits >> 16) & 0xFFFF); + data.b = static_cast((color_bits >> 32) & 0xFFFF); + data.a = static_cast((color_bits >> 48) & 0xFFFF); + + const auto void_extent_coords = pb.VoidExtentCoords(); + if (void_extent_coords) { + data.coords[0] = void_extent_coords->at(0); + data.coords[1] = void_extent_coords->at(1); + data.coords[2] = void_extent_coords->at(2); + data.coords[3] = void_extent_coords->at(3); + } else { + uint16_t all_ones = (1 << 13) - 1; + for (auto& coord : data.coords) { + coord = all_ones; + } + } + + return data; +} + +// Packs the given intermediate block into a physical block. Returns false if +// the provided values in the intermediate block emit an illegal ASTC +// encoding. +base::Optional Pack(const IntermediateBlockData& data, + base::UInt128* pb) { + if (data.weights.size() != + data.weight_grid_dim_x * data.weight_grid_dim_y * + (data.dual_plane_channel.hasValue() ? 2 : 1)) { + return std::string("Incorrect number of weights!"); + } + + // If it's not a void extent block, then it gets a bit more tricky... + base::BitStream bit_sink; + + // First we need to encode the block mode. + const auto error_string = PackBlockMode( + data.weight_grid_dim_x, data.weight_grid_dim_y, data.weight_range, + data.dual_plane_channel.hasValue(), &bit_sink); + if (error_string) { + return error_string; + } + + // Next, we place the number of partitions minus one. + const int num_partitions = data.endpoints.size(); + bit_sink.PutBits(num_partitions - 1, 2); + + // If we have more than one partition, then we also have a partition ID. + if (num_partitions > 1) { + const int id = data.partition_id.value(); + assert(id >= 0); + bit_sink.PutBits(id, 10); + } + + // Take a detour, let's encode the weights so that we know how many bits they + // consume. + base::BitStream weight_sink; + + IntegerSequenceEncoder weight_enc(data.weight_range); + for (auto weight : data.weights) { + weight_enc.AddValue(weight); + } + weight_enc.Encode(&weight_sink); + + const int num_weight_bits = weight_sink.Bits(); + assert(num_weight_bits == + IntegerSequenceCodec::GetBitCountForRange( + data.weights.size(), data.weight_range)); + + // Let's continue... how much after the color data do we need to write? + int extra_config = 0; + + // Determine if all endpoint pairs share the same endpoint mode + assert(data.endpoints.size() > 0); + bool shared_endpoint_mode = SharedEndpointModes(data); + + // The first part of the endpoint mode (CEM) comes directly after the + // partition info, if it exists. If there is no partition info, the CEM comes + // right after the block mode. In the single-partition case, we just write out + // the entire singular CEM, but in the multi-partition case, if all CEMs are + // the same then their shared CEM is specified directly here, too. In both + // cases, shared_endpoint_mode is true (in the singular case, + // shared_endpoint_mode is trivially true). + if (shared_endpoint_mode) { + if (num_partitions > 1) { + bit_sink.PutBits(0, 2); + } + bit_sink.PutBits(static_cast(data.endpoints[0].mode), 4); + } else { + // Here, the CEM is not shared across all endpoint pairs, and we need to + // figure out what to place here, and what to place in the extra config + // bits before the weight data... + + // Non-shared config modes must all be within the same class (out of four) + // See Section C.2.11 + int min_class = 2; // We start with 2 here instead of three because it's + // the highest that can be encoded -- even if all modes + // are class 3. + int max_class = 0; + for (const auto& ep_data : data.endpoints) { + const int ep_mode_class = static_cast(ep_data.mode) >> 2; + min_class = std::min(min_class, ep_mode_class); + max_class = std::max(max_class, ep_mode_class); + } + + assert(max_class >= min_class); + + if (max_class - min_class > 1) { + return std::string("Endpoint modes are invalid"); + } + + // Construct the CEM mode -- six of its bits will fit here, but otherwise + // the rest will go in the extra configuration bits. + base::BitStream cem_encoder; + + // First encode the base class + assert(min_class >= 0); + assert(min_class < 3); + cem_encoder.PutBits(min_class + 1, 2); + + // Next, encode the class selector bits -- this is simply the offset + // from the base class + for (const auto& ep_data : data.endpoints) { + const int ep_mode_class = static_cast(ep_data.mode) >> 2; + const int class_selector_bit = ep_mode_class - min_class; + assert(class_selector_bit == 0 || class_selector_bit == 1); + cem_encoder.PutBits(class_selector_bit, 1); + } + + // Finally, we need to choose from each class which actual mode + // we belong to and encode those. + for (const auto& ep_data : data.endpoints) { + const int ep_mode = static_cast(ep_data.mode) & 3; + assert(ep_mode < 4); + cem_encoder.PutBits(ep_mode, 2); + } + assert(cem_encoder.Bits() == 2 + num_partitions * 3); + + uint32_t encoded_cem; + cem_encoder.GetBits(2 + num_partitions * 3, &encoded_cem); + + // Since only six bits fit here before the color endpoint data, the rest + // need to go in the extra config data. + extra_config = encoded_cem >> 6; + + // Write out the six bits we had + bit_sink.PutBits(encoded_cem, 6); + } + + // If we have a dual-plane channel, we can tack that onto our extra config + // data + if (data.dual_plane_channel.hasValue()) { + const int channel = data.dual_plane_channel.value(); + assert(channel < 4); + extra_config <<= 2; + extra_config |= channel; + } + + // Get the range of endpoint values. It can't be -1 because we should have + // checked for that much earlier. + const int color_value_range = data.endpoint_range + ? data.endpoint_range.value() + : EndpointRangeForBlock(data); + + assert(color_value_range != kEndpointRange_ReturnInvalidWeightDims); + if (color_value_range == kEndpointRange_ReturnNotEnoughColorBits) { + return { "Intermediate block emits illegal color range" }; + } + + IntegerSequenceEncoder color_enc(color_value_range); + for (const auto& ep_data : data.endpoints) { + for (int color : ep_data.colors) { + if (color > color_value_range) { + return { "Color outside available color range!" }; + } + + color_enc.AddValue(color); + } + } + color_enc.Encode(&bit_sink); + + // Now we need to skip some bits to get to the extra configuration bits. The + // number of bits we need to skip depends on where we are in the stream and + // where we need to get to. + const int extra_config_bit_position = ExtraConfigBitPosition(data); + const int extra_config_bits = + 128 - num_weight_bits - extra_config_bit_position; + assert(extra_config_bits >= 0); + assert(extra_config < 1 << extra_config_bits); + + // Make sure the color encoder didn't write more than we thought it would. + int bits_to_skip = extra_config_bit_position - bit_sink.Bits(); + assert(bits_to_skip >= 0); + + while (bits_to_skip > 0) { + const int skipping = std::min(32, bits_to_skip); + bit_sink.PutBits(0, skipping); + bits_to_skip -= skipping; + } + + // Finally, write out the rest of the config bits. + bit_sink.PutBits(extra_config, extra_config_bits); + + // We should be right up to the weight bits... + assert(bit_sink.Bits() == 128 - num_weight_bits); + + // Flush out our bit writer and write out the weight bits + base::UInt128 astc_bits; + bit_sink.GetBits(128 - num_weight_bits, &astc_bits); + + base::UInt128 rev_weight_bits; + weight_sink.GetBits(weight_sink.Bits(), &rev_weight_bits); + + astc_bits |= base::ReverseBits(rev_weight_bits); + + // And we're done! Whew! + *pb = astc_bits; + return PhysicalASTCBlock(*pb).IsIllegalEncoding(); +} + +base::Optional Pack(const VoidExtentData& data, + base::UInt128* pb) { + *pb = PackVoidExtentBlock(data.r, data.g, data.b, data.a, data.coords); + return PhysicalASTCBlock(*pb).IsIllegalEncoding(); +} + +} // namespace astc_codec diff --git a/src/decoder/intermediate_astc_block.h b/src/decoder/intermediate_astc_block.h new file mode 100644 index 0000000..ec6eb3e --- /dev/null +++ b/src/decoder/intermediate_astc_block.h @@ -0,0 +1,128 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_INTERMEDIATE_ASTC_BLOCK_H_ +#define ASTC_CODEC_DECODER_INTERMEDIATE_ASTC_BLOCK_H_ + +#include "src/base/optional.h" +#include "src/base/uint128.h" +#include "src/decoder/physical_astc_block.h" + +#include +#include + +namespace astc_codec { + +// From Table C.2.7 -- These are the only valid ranges that weight +// values can take. +constexpr std::array kValidWeightRanges = +{{ 1, 2, 3, 4, 5, 7, 9, 11, 15, 19, 23, 31 }}; + +// Void extent data are all the ASTC blocks that are labeled for having a +// constant color. In the ASTC spec, some of these blocks may optionally +// have "void extent coordinates" that describe how far in texture space +// the constant color should span. If these coordinates are not valid, +// then the coordinates are all set to a fully saturated bit mask +// ((1 << 13) - 1) and the block is treated as a singular constant color. +// We call both types of these blocks "void extent" to remove confusion +// in our code. +struct VoidExtentData { + uint16_t r; + uint16_t g; + uint16_t b; + uint16_t a; + std::array coords; +}; + +// Intermediate endpoint data. Really this is just an endpoint mode +// and a couple of values that represent the data used to decode the +// RGB values from the color endpoint mode. +struct IntermediateEndpointData { + ColorEndpointMode mode; + std::vector colors; +}; + +// This is an unpacked physical ASTC block, but it does not have enough +// information to be worked with logically. It is simply a container of +// all of the unpacked ASTC information. It is used as a staging area +// for the information that is later Pack()'d into a PhysicalASTCBlock. +struct IntermediateBlockData { + int weight_grid_dim_x; + int weight_grid_dim_y; + int weight_range; + + // Quantized, non-interpolated weights + std::vector weights; + + // The 10-bit partition ID if we need one + base::Optional partition_id; + + // The dual-plane channel in [0, 3] if it exists. + base::Optional dual_plane_channel; + + // The quantized/encoded endpoint values for this block. The range of each + // endpoint value is specified by |endpoint_range|, if it exists. If not, the + // range can be queried by calling EndpointRangeForBlock + std::vector endpoints; + + // The range [0, endpoint_range] that any one endpoint value can take. Users + // should not write to this value themselves. If it is empty at the time + // someone calls Pack(), it will be automatically inferred. Otherwise, it is + // set by Unpack() based on what the underlying encoding specified. + base::Optional endpoint_range; +}; + +// Returns the maximum value that a given endpoint value can take according to +// the other settings in the block. Ignores the |endpoint_range| member +// variable. Returns negative values on error: +// -1 : Too many bits required to store weight grid +// -2 : There are too few bits allocated for color endpoint data according to +// C.2.24 in the ASTC spec +int EndpointRangeForBlock(const IntermediateBlockData& data); +inline int EndpointRangeForBlock(const VoidExtentData& data); + +// Unpacks the physical ASTC block into the intermediate block. Returns false +// if the physical block is an error encoded block, or if the physical block +// is a void extent block. On error the contents of ib are undefined. +base::Optional UnpackIntermediateBlock( + const PhysicalASTCBlock& pb); + +// Unpacks the physical ASTC block into a void extent block. Returns false +// if the physical block is an error encoded block, or if the physical block +// is an intermediate block. On error the contents of ib are undefined. +base::Optional UnpackVoidExtent(const PhysicalASTCBlock& pb); + +// Packs the given intermediate block into a physical block. Returns an error +// string if the provided values in the intermediate block emit an illegal ASTC +// encoding. In this case the results in the physical block are undefined. +base::Optional Pack(const IntermediateBlockData& data, + base::UInt128* pb); + +// Packs the given void extent block into a physical block. Returns an error +// string if the provided values in the void extent block emit an illegal ASTC +// encoding. In this case the results in the physical block are undefined. +base::Optional Pack(const VoidExtentData& data, base::UInt128* pb); + +//////////////////////////////////////////////////////////////////////////////// +// +// Impl + +inline int EndpointRangeForBlock(const VoidExtentData& data) { + // Void extent blocks use 16-bit ARGB definitions + return (1 << 16) - 1; +} + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_INTERMEDIATE_ASTC_BLOCK_H_ diff --git a/src/decoder/logical_astc_block.cc b/src/decoder/logical_astc_block.cc new file mode 100644 index 0000000..9271e18 --- /dev/null +++ b/src/decoder/logical_astc_block.cc @@ -0,0 +1,262 @@ +// 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/endpoint_codec.h" +#include "src/decoder/footprint.h" +#include "src/decoder/integer_sequence_codec.h" +#include "src/decoder/quantization.h" +#include "src/decoder/weight_infill.h" + +namespace astc_codec { + +namespace { + +Partition GenerateSinglePartition(Footprint footprint) { + return Partition { footprint, /* num_parts = */ 1, /* partition_id = */ 0, + std::vector(footprint.NumPixels(), 0) }; +} + +static std::vector DecodeEndpoints(const IntermediateBlockData& block) { + const int endpoint_range = block.endpoint_range + ? block.endpoint_range.value() + : EndpointRangeForBlock(block); + assert(endpoint_range > 0); + + std::vector endpoints; + for (const auto& eps : block.endpoints) { + RgbaColor decmp_one_rgba, decmp_two_rgba; + DecodeColorsForMode(eps.colors, endpoint_range, eps.mode, + &decmp_one_rgba, &decmp_two_rgba); + endpoints.emplace_back(decmp_one_rgba, decmp_two_rgba); + } + return endpoints; +} + +static std::vector DecodeEndpoints(const VoidExtentData& block) { + EndpointPair eps; + eps.first[0] = eps.second[0] = (block.r * 255) / 65535; + eps.first[1] = eps.second[1] = (block.g * 255) / 65535; + eps.first[2] = eps.second[2] = (block.b * 255) / 65535; + eps.first[3] = eps.second[3] = (block.a * 255) / 65535; + + std::vector endpoints; + endpoints.emplace_back(eps); + return endpoints; +} + +Partition ComputePartition(const Footprint& footprint, + const IntermediateBlockData& block) { + if (block.partition_id) { + const int part_id = block.partition_id.value(); + const size_t num_parts = block.endpoints.size(); + return GetASTCPartition(footprint, num_parts, part_id); + } else { + return GenerateSinglePartition(footprint); + } +} + +Partition ComputePartition(const Footprint& footprint, const VoidExtentData&) { + return GenerateSinglePartition(footprint); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +LogicalASTCBlock::LogicalASTCBlock(const Footprint& footprint) + : endpoints_(1), + weights_(footprint.NumPixels(), 0), + partition_(GenerateSinglePartition(footprint)) { } + +LogicalASTCBlock::LogicalASTCBlock(const Footprint& footprint, + const IntermediateBlockData& block) + : endpoints_(DecodeEndpoints(block)), + partition_(ComputePartition(footprint, block)) { + CalculateWeights(footprint, block); +} + +LogicalASTCBlock::LogicalASTCBlock(const Footprint& footprint, + const VoidExtentData& block) + : endpoints_(DecodeEndpoints(block)), + partition_(ComputePartition(footprint, block)) { + CalculateWeights(footprint, block); +} + +void LogicalASTCBlock::CalculateWeights(const Footprint& footprint, + const IntermediateBlockData& block) { + const int grid_size_x = block.weight_grid_dim_x; + const int grid_size_y = block.weight_grid_dim_y; + const int weight_grid_size = grid_size_x * grid_size_y; + + // Either we have a dual plane and we have twice as many weights as + // specified or we don't + assert(block.dual_plane_channel + ? block.weights.size() == weight_grid_size * 2 + : block.weights.size() == weight_grid_size); + + std::vector unquantized; + unquantized.reserve(weight_grid_size); + + // According to C.2.16, if we have dual-plane weights, then we have two + // weights per texel -- one adjacent to the other. Hence, we have to skip + // some when we decode the separate weight values. + const int weight_frequency = (block.dual_plane_channel) ? 2 : 1; + const int weight_range = block.weight_range; + + for (int i = 0; i < weight_grid_size; ++i) { + const int weight = block.weights[i * weight_frequency]; + unquantized.push_back(UnquantizeWeightFromRange(weight, weight_range)); + } + weights_ = InfillWeights(unquantized, footprint, grid_size_x, grid_size_y); + + if (block.dual_plane_channel) { + SetDualPlaneChannel(block.dual_plane_channel.value()); + for (int i = 0; i < weight_grid_size; ++i) { + const int weight = block.weights[i * weight_frequency + 1]; + unquantized[i] = UnquantizeWeightFromRange(weight, weight_range); + } + dual_plane_->weights = + InfillWeights(unquantized, footprint, grid_size_x, grid_size_y); + } +} + +void LogicalASTCBlock::CalculateWeights(const Footprint& footprint, + const VoidExtentData&) { + weights_ = std::vector(footprint.NumPixels(), 0); +} + +void LogicalASTCBlock::SetWeightAt(int x, int y, int weight) { + assert(weight >= 0); + assert(weight <= 64); + weights_.at(y * GetFootprint().Width() + x) = weight; +} + +int LogicalASTCBlock::WeightAt(int x, int y) const { + return weights_.at(y * GetFootprint().Width() + x); +} + +void LogicalASTCBlock::SetDualPlaneWeightAt(int channel, int x, int y, + int weight) { + assert(weight >= 0); + assert(weight <= 64); + + // If it's not a dual plane, then this has no logical meaning + assert(IsDualPlane()); + + // If it is a dual plane and the passed channel matches the query, then we + // return the specialized weights + if (dual_plane_->channel == channel) { + dual_plane_->weights.at(y * GetFootprint().Width() + x) = weight; + } else { + // If the channel is not the special channel, then return the general weight + SetWeightAt(x, y, weight); + } +} + +int LogicalASTCBlock::DualPlaneWeightAt(int channel, int x, int y) const { + // If it's not a dual plane, then we just return the weight for all channels + if (!IsDualPlane()) { + // TODO(google): Log warning, Requesting dual-plane channel for a non + // dual-plane block! + return WeightAt(x, y); + } + + // If it is a dual plane and the passed channel matches the query, then we + // return the specialized weights + if (dual_plane_->channel == channel) { + return dual_plane_->weights.at(y * GetFootprint().Width() + x); + } + + // If the channel is not the special channel, then return the general weight + return WeightAt(x, y); +} + +void LogicalASTCBlock::SetDualPlaneChannel(int channel) { + if (channel < 0) { + dual_plane_.clear(); + } else if (dual_plane_) { + dual_plane_->channel = channel; + } else { + dual_plane_ = DualPlaneData {channel, weights_}; + } +} + +RgbaColor LogicalASTCBlock::ColorAt(int x, int y) const { + const auto footprint = GetFootprint(); + assert(x >= 0); assert(x < footprint.Width()); + assert(y >= 0); assert(y < footprint.Height()); + + const int texel_idx = y * footprint.Width() + x; + const int part = partition_.assignment[texel_idx]; + const auto& endpoints = endpoints_[part]; + + RgbaColor result; + for (int channel = 0; channel < 4; ++channel) { + const int weight = (dual_plane_ && dual_plane_->channel == channel) + ? dual_plane_->weights[texel_idx] + : weights_[texel_idx]; + const int p0 = endpoints.first[channel]; + const int p1 = endpoints.second[channel]; + + assert(p0 >= 0); assert(p0 < 256); + assert(p1 >= 0); assert(p1 < 256); + + // According to C.2.19 + const int c0 = (p0 << 8) | p0; + const int c1 = (p1 << 8) | p1; + const int c = (c0 * (64 - weight) + c1 * weight + 32) / 64; + // TODO(google): Handle conversion to sRGB or FP16 per C.2.19. + const int quantized = ((c * 255) + 32767) / 65536; + assert(quantized < 256); + result[channel] = quantized; + } + + return result; +} + +void LogicalASTCBlock::SetPartition(const Partition& p) { + assert(p.footprint == partition_.footprint && + "New partitions may not be for a different footprint"); + partition_ = p; + endpoints_.resize(p.num_parts); +} + +void LogicalASTCBlock::SetEndpoints(const EndpointPair& eps, int subset) { + assert(subset < partition_.num_parts); + assert(subset < endpoints_.size()); + + endpoints_[subset] = eps; +} + +base::Optional UnpackLogicalBlock( + const Footprint& footprint, const PhysicalASTCBlock& pb) { + if (pb.IsVoidExtent()) { + base::Optional ve = UnpackVoidExtent(pb); + if (!ve) { + return {}; + } + + return LogicalASTCBlock(footprint, ve.value()); + } else { + base::Optional ib = UnpackIntermediateBlock(pb); + if (!ib) { + return {}; + } + + return LogicalASTCBlock(footprint, ib.value()); + } +} + +} // namespace astc_codec diff --git a/src/decoder/logical_astc_block.h b/src/decoder/logical_astc_block.h new file mode 100644 index 0000000..2243eb4 --- /dev/null +++ b/src/decoder/logical_astc_block.h @@ -0,0 +1,127 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_LOGICAL_ASTC_BLOCK_H_ +#define ASTC_CODEC_DECODER_LOGICAL_ASTC_BLOCK_H_ + +#include "src/base/optional.h" +#include "src/decoder/footprint.h" +#include "src/decoder/intermediate_astc_block.h" +#include "src/decoder/partition.h" +#include "src/decoder/physical_astc_block.h" + +#include +#include +#include + +namespace astc_codec { + +// A logical ASTC block holds the endpoints, indices, and partition information +// of a compressed block. These values generally do not adhere to any +// quality-for-bitrate-imposed limits and are solely logical entities for +// determining the best representation of a given block. +class LogicalASTCBlock { + public: + LogicalASTCBlock(const LogicalASTCBlock&) = default; + LogicalASTCBlock(LogicalASTCBlock&&) = default; + + // Unpack an intermediate block into a logical one. + LogicalASTCBlock(const Footprint& footprint, + const IntermediateBlockData& block); + + // Unpack a void extent intermediate block into a logical one. + LogicalASTCBlock(const Footprint& footprint, const VoidExtentData& block); + + // Create a new, empty ASTC block + explicit LogicalASTCBlock(const Footprint& footprint); + + // Returns the footprint associated with this block. The footprint is defined + // via the partition, because the partition definition is dependent on the + // footprint. + const Footprint& GetFootprint() const { return partition_.footprint; } + + // Returns the unquantized and infilled weight in the range [0, 64] for the + // given texel location. Assumes that the block is a single-plane block, + // meaning that weights are used equally across all channels. + void SetWeightAt(int x, int y, int weight); + int WeightAt(int x, int y) const; + + // Returns the unquantized and infilled weight in the range [0, 64] for the + // given channel at the given texel location. If the block does not have a + // dual-plane channel then the reference-returning version will fail, as it + // cannot return a reference to a value that (potentially) doesn't exist. + void SetDualPlaneWeightAt(int channel, int x, int y, int weight); + int DualPlaneWeightAt(int channel, int x, int y) const; + + // Returns the color as it would be in the given pixel coordinates of the + // block. Fails if the coordinates are outside of the range of the block + // footprint + RgbaColor ColorAt(int x, int y) const; + + // Sets the current partition for the block. |p|'s footprint must match the + // return value of GetFootprint() or else this call will fail. + void SetPartition(const Partition& p); + + // Sets the endpoints for the given subset. + void SetEndpoints(const EndpointPair& eps, int subset); + void SetEndpoints(const Endpoint& ep1, const Endpoint& ep2, int subset) { + SetEndpoints(std::make_pair(ep1, ep2), subset); + } + + // Sets the dual plane channel for the block. Value must be within the range + // [0, 3]. If a negative value is passed, then the dual-plane data for the + // block is removed, and the block is treated as a single-plane block. + void SetDualPlaneChannel(int channel); + bool IsDualPlane() const { return dual_plane_.hasValue(); } + + private: + // A block may have up to four endpoint pairs. + std::vector endpoints_; + + // Weights are stored as values in the interval [0, 64]. + std::vector weights_; + + // The partition information for this block. This determines the + // appropriate subsets that each pixel should belong to. + Partition partition_; + + // Dual plane data holds both the channel and the weights that describe + // the dual plane data for the given block. If a block has a dual plane, then + // we need to know both the channel and the weights associated with it. + struct DualPlaneData { + int channel; + std::vector weights; + }; + + // The dual-plane data is optional from a logical representation of the block. + base::Optional dual_plane_; + + // Calculates the unquantized and interpolated weights from the encoded weight + // values and possibly dual-plane weights specified in the passed ASTC block. + void CalculateWeights(const Footprint& footprint, + const IntermediateBlockData& block); + + // Calculates the weights for a VoidExtentBlock. + void CalculateWeights(const Footprint& footprint, + const VoidExtentData& block); +}; + +// Unpacks the physical ASTC block into a logical block. Returns false if the +// physical block is an error encoded block. +base::Optional UnpackLogicalBlock( + const Footprint& footprint, const PhysicalASTCBlock& pb); + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_LOGICAL_ASTC_BLOCK_H_ diff --git a/src/decoder/partition.cc b/src/decoder/partition.cc new file mode 100644 index 0000000..90d39fd --- /dev/null +++ b/src/decoder/partition.cc @@ -0,0 +1,600 @@ +// 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 "src/base/bottom_n.h" +#include "src/decoder/footprint.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace astc_codec { + +namespace { + +// The maximum number of partitions supported by ASTC is four. +constexpr int kMaxNumSubsets = 4; + +// Partition selection function based on the ASTC specification. +// See section C.2.21 +int SelectASTCPartition(int seed, int x, int y, int z, int partitioncount, + int num_pixels) { + if (partitioncount <= 1) { + return 0; + } + + if (num_pixels < 31) { + x <<= 1; + y <<= 1; + z <<= 1; + } + + seed += (partitioncount - 1) * 1024; + + uint32_t rnum = seed; + rnum ^= rnum >> 15; + rnum -= rnum << 17; + rnum += rnum << 7; + rnum += rnum << 4; + rnum ^= rnum >> 5; + rnum += rnum << 16; + rnum ^= rnum >> 7; + rnum ^= rnum >> 3; + rnum ^= rnum << 6; + rnum ^= rnum >> 17; + + uint8_t seed1 = rnum & 0xF; + uint8_t seed2 = (rnum >> 4) & 0xF; + uint8_t seed3 = (rnum >> 8) & 0xF; + uint8_t seed4 = (rnum >> 12) & 0xF; + uint8_t seed5 = (rnum >> 16) & 0xF; + uint8_t seed6 = (rnum >> 20) & 0xF; + uint8_t seed7 = (rnum >> 24) & 0xF; + uint8_t seed8 = (rnum >> 28) & 0xF; + uint8_t seed9 = (rnum >> 18) & 0xF; + uint8_t seed10 = (rnum >> 22) & 0xF; + uint8_t seed11 = (rnum >> 26) & 0xF; + uint8_t seed12 = ((rnum >> 30) | (rnum << 2)) & 0xF; + + seed1 *= seed1; + seed2 *= seed2; + seed3 *= seed3; + seed4 *= seed4; + seed5 *= seed5; + seed6 *= seed6; + seed7 *= seed7; + seed8 *= seed8; + seed9 *= seed9; + seed10 *= seed10; + seed11 *= seed11; + seed12 *= seed12; + + int sh1, sh2, sh3; + if (seed & 1) { + sh1 = (seed & 2 ? 4 : 5); + sh2 = (partitioncount == 3 ? 6 : 5); + } else { + sh1 = (partitioncount == 3 ? 6 : 5); + sh2 = (seed & 2 ? 4 : 5); + } + sh3 = (seed & 0x10) ? sh1 : sh2; + + seed1 >>= sh1; + seed2 >>= sh2; + seed3 >>= sh1; + seed4 >>= sh2; + seed5 >>= sh1; + seed6 >>= sh2; + seed7 >>= sh1; + seed8 >>= sh2; + + seed9 >>= sh3; + seed10 >>= sh3; + seed11 >>= sh3; + seed12 >>= sh3; + + int a = seed1 * x + seed2 * y + seed11 * z + (rnum >> 14); + int b = seed3 * x + seed4 * y + seed12 * z + (rnum >> 10); + int c = seed5 * x + seed6 * y + seed9 * z + (rnum >> 06); + int d = seed7 * x + seed8 * y + seed10 * z + (rnum >> 02); + + a &= 0x3F; + b &= 0x3F; + c &= 0x3F; + d &= 0x3F; + + if (partitioncount <= 3) { + d = 0; + } + if (partitioncount <= 2) { + c = 0; + } + + if (a >= b && a >= c && a >= d) { + return 0; + } else if (b >= c && b >= d) { + return 1; + } else if (c >= d) { + return 2; + } else { + return 3; + } +} + +// A partition hash that we can pass to containers like std::unordered_set +struct PartitionHasher { + size_t operator()(const Partition& part) const { + // The issue here is that if we have two different partitions, A and B, then + // their hash should be equal if A and B are equal. We define the distance + // between A and B using PartitionMetric, but internally that finds a 1-1 + // mapping from labels in A to labels in B. + // + // With that in mind, when we define a hash for partitions, we need to find + // a 1-1 mapping to a 'universal' labeling scheme. Here we define that as + // the heuristic: the first encountered label will be 0, the second will be + // 1, etc. This creates a unique 1-1 mapping scheme from any partition. + // + // Note, we can't use this heuristic for the PartitionMetric, as it will + // generate very large discrepancies between similar labellings (for example + // 000...001 vs 011...111). We are just looking for a boolean distinction + // whether or not two partitions are different and don't care how different + // they are. + std::array mapping {{ -1, -1, -1, -1 }}; + int next_subset = 0; + for (int subset : part.assignment) { + if (mapping[subset] < 0) { + mapping[subset] = next_subset++; + } + } + assert(next_subset <= kMaxNumSubsets); + + // The return value will be the hash of the assignment according to this + // mapping + const auto seed = 0; + return std::accumulate(part.assignment.begin(), part.assignment.end(), seed, + [&mapping](size_t seed, const int& subset) { + std::hash hasher; + const int s = mapping[subset]; + return hasher(seed) ^ hasher(static_cast(s)); + }); + } +}; + +// Construct a VP-Tree of partitions. Since our PartitionMetric satisfies +// the triangle inequality, we can use this general higher-dimensional space +// partitioning tree to organize our partitions. +// +// TODO(google): !SPEED! Right now this tree stores an actual linked +// structure of pointers which is likely very slow during construction and +// very not cache-coherent during traversal, so it'd probably be good to +// switch to a flattened binary tree structure if performance becomes an +// issue. +class PartitionTree { + public: + // Unclear what it means to have an uninitialized tree, so delete default + // constructors, but allow the tree to be moved + PartitionTree() = delete; + PartitionTree(const PartitionTree&) = delete; + PartitionTree(PartitionTree&& t) = default; + + // Generate a PartitionTree from iterators over |Partition|s + template + PartitionTree(Itr begin, Itr end) : parts_(begin, end) { + std::vector part_indices(parts_.size()); + std::iota(part_indices.begin(), part_indices.end(), 0); + root_ = std::unique_ptr( + new PartitionTreeNode(parts_, part_indices)); + } + + // Search for the k-nearest partitions that are closest to part based on + // the result of PartitionMetric + void Search(const Partition& part, int k, + std::vector* const results, + std::vector* const distances) const { + ResultHeap heap(k); + SearchNode(root_, part, &heap); + + results->clear(); + if (nullptr != distances) { + distances->clear(); + } + + std::vector search_results = heap.Pop(); + for (const auto& result : search_results) { + results->push_back(&parts_[result.part_idx]); + if (nullptr != distances) { + distances->push_back(result.distance); + } + } + + assert(results->size() == k); + } + + private: + // Heap elements to be stored while searching the tree. The two relevant + // pieces of information are the partition index and it's distance from the + // queried partition. + struct ResultNode { + int part_idx; + int distance; + + // Heap based on distance from query point. + bool operator<(const ResultNode& other) const { + return distance < other.distance; + } + }; + + using ResultHeap = base::BottomN; + + struct PartitionTreeNode { + int part_idx; + int split_dist; + + std::unique_ptr left; + std::unique_ptr right; + + PartitionTreeNode(const std::vector &parts, + const std::vector &part_indices) + : split_dist(-1) { + assert(part_indices.size() > 0); + + right.reset(nullptr); + left.reset(nullptr); + + // Store the first node as our vantage point + part_idx = part_indices[0]; + const Partition& vantage_point = parts[part_indices[0]]; + + // Calculate the distances of the remaining nodes against the vantage + // point. + std::vector> part_dists; + for (int i = 1; i < part_indices.size(); ++i) { + const int idx = part_indices[i]; + const int dist = PartitionMetric(vantage_point, parts[idx]); + if (dist > 0) { + part_dists.push_back(std::make_pair(idx, dist)); + } + } + + // If there are no more different parts, then this is a leaf node + if (part_dists.empty()) { + return; + } + + struct OrderBySecond { + typedef std::pair PairType; + bool operator()(const PairType& lhs, const PairType& rhs) { + return lhs.second < rhs.second; + } + }; + + // We want to partition the set such that the points are ordered + // based on their distances from the vantage point. We can do this + // using the partial sort of nth element. + std::nth_element( + part_dists.begin(), part_dists.begin() + part_dists.size() / 2, + part_dists.end(), OrderBySecond()); + + // Once that's done, our split position is in the middle + const auto split_iter = part_dists.begin() + part_dists.size() / 2; + split_dist = split_iter->second; + + // Recurse down the right and left sub-trees with the indices of the + // parts that are farther and closer respectively + std::vector right_indices; + for (auto itr = split_iter; itr != part_dists.end(); ++itr) { + right_indices.push_back(itr->first); + } + + if (!right_indices.empty()) { + right.reset(new PartitionTreeNode(parts, right_indices)); + } + + std::vector left_indices; + for (auto itr = part_dists.begin(); itr != split_iter; ++itr) { + left_indices.push_back(itr->first); + } + + if (!left_indices.empty()) { + left.reset(new PartitionTreeNode(parts, left_indices)); + } + } + }; + + void SearchNode(const std::unique_ptr& node, + const Partition& p, ResultHeap* const heap) const { + if (nullptr == node) { + return; + } + + // Calculate distance against current node + const int dist = PartitionMetric(parts_[node->part_idx], p); + + // Push it onto the heap and remove the top-most nodes to maintain + // closest k indices. + ResultNode result; + result.part_idx = node->part_idx; + result.distance = dist; + heap->Push(result); + + // If the split distance is uninitialized, it means we have no children. + if (node->split_dist < 0) { + assert(nullptr == node->left); + assert(nullptr == node->right); + return; + } + + // Next we need to check the left and right trees if their distance + // is closer/farther than the farthest element on the heap + const int tau = heap->Top().distance; + if (dist + tau < node->split_dist || dist - tau < node->split_dist) { + SearchNode(node->left, p, heap); + } + + if (dist + tau > node->split_dist || dist - tau > node->split_dist) { + SearchNode(node->right, p, heap); + } + } + + std::vector parts_; + std::unique_ptr root_; +}; + +// A helper function that generates all of the partitions for each number of +// subsets in ASTC blocks and stores them in a PartitionTree for fast retrieval. +const int kNumASTCPartitionIDBits = 10; +PartitionTree GenerateASTCPartitionTree(Footprint footprint) { + std::unordered_set parts; + for (int num_parts = 2; num_parts <= kMaxNumSubsets; ++num_parts) { + for (int id = 0; id < (1 << kNumASTCPartitionIDBits); ++id) { + Partition part = GetASTCPartition(footprint, num_parts, id); + + // Make sure we're not using a degenerate partition assignment that wastes + // an endpoint pair... + bool valid_part = true; + for (int i = 0; i < num_parts; ++i) { + if (std::find(part.assignment.begin(), part.assignment.end(), i) == + part.assignment.end()) { + valid_part = false; + break; + } + } + + if (valid_part) { + parts.insert(std::move(part)); + } + } + } + + return PartitionTree(parts.begin(), parts.end()); +} + +// To avoid needing any fancy boilerplate for mapping from a width, height +// tuple, we can define a simple encoding for the block mode: +constexpr int EncodeDims(int width, int height) { + return (width << 16) | height; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +int PartitionMetric(const Partition& a, const Partition& b) { + // Make sure that one partition is at least a subset of the other... + assert(a.footprint == b.footprint); + + // Make sure that the number of parts is within our limits. ASTC has a maximum + // of four subsets per block according to the specification. + assert(a.num_parts <= kMaxNumSubsets); + assert(b.num_parts <= kMaxNumSubsets); + + const int w = a.footprint.Width(); + const int h = b.footprint.Height(); + + struct PairCount { + int a; + int b; + int count; + + // Comparison needed for sort below. + bool operator>(const PairCount& other) const { + return count > other.count; + } + }; + + // Since we need to find the smallest mapping from labels in A to labels in B, + // we need to store each label pair in a structure that can later be sorted. + // The maximum number of subsets in an ASTC block is four, meaning that + // between the two partitions, we can have up to sixteen different pairs. + std::array pair_counts; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + const int idx = y * 4 + x; + pair_counts[idx].a = x; + pair_counts[idx].b = y; + pair_counts[idx].count = 0; + } + } + + // Count how many times we see each pair of assigned values (order matters!) + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + const int idx = y * w + x; + + const int a_val = a.assignment[idx]; + const int b_val = b.assignment[idx]; + + assert(a_val >= 0); + assert(b_val >= 0); + + assert(a_val < 4); + assert(b_val < 4); + + ++(pair_counts[b_val * 4 + a_val].count); + } + } + + // Sort the pairs in descending order based on their count + std::sort(pair_counts.begin(), pair_counts.end(), std::greater()); + + // Now assign pairs one by one until we have no more pairs to assign. Once + // a value from A is assigned to a value in B, it can no longer be reassigned, + // so we can keep track of this in a matrix. Similarly, to keep the assignment + // one-to-one, once a value in B has been assigned to, it cannot be assigned + // to again. + std::array, kMaxNumSubsets> assigned { }; + + int pixels_matched = 0; + for (const auto& pair_count : pair_counts) { + bool is_assigned = false; + for (int i = 0; i < kMaxNumSubsets; ++i) { + is_assigned |= assigned.at(pair_count.a).at(i); + is_assigned |= assigned.at(i).at(pair_count.b); + } + + if (!is_assigned) { + assigned.at(pair_count.a).at(pair_count.b) = true; + pixels_matched += pair_count.count; + } + } + + // The difference is the number of pixels that had an assignment versus the + // total number of pixels. + return w * h - pixels_matched; +} + +// Generates the partition assignment for the given block attributes. +Partition GetASTCPartition(const Footprint& footprint, int num_parts, + int partition_id) { + // Partitions must have at least one subset but may have at most four + assert(num_parts >= 0); + assert(num_parts <= kMaxNumSubsets); + + // Partition ID can be no more than 10 bits. + assert(partition_id >= 0); + assert(partition_id < 1 << 10); + + Partition part = {footprint, num_parts, partition_id, /* assignment = */ {}}; + part.assignment.reserve(footprint.NumPixels()); + + // Maintain column-major order so that we match all of the image processing + // algorithms that depend on this class. + for (int y = 0; y < footprint.Height(); ++y) { + for (int x = 0; x < footprint.Width(); ++x) { + const int p = SelectASTCPartition(partition_id, x, y, 0, num_parts, + footprint.NumPixels()); + part.assignment.push_back(p); + } + } + + return part; +} + +const std::vector FindKClosestASTCPartitions( + const Partition& candidate, int k) { + const int encoded_dims = EncodeDims(candidate.footprint.Width(), + candidate.footprint.Height()); + + int index = 0; + switch (encoded_dims) { + case EncodeDims(4, 4): index = 0; break; + case EncodeDims(5, 4): index = 1; break; + case EncodeDims(5, 5): index = 2; break; + case EncodeDims(6, 5): index = 3; break; + case EncodeDims(6, 6): index = 4; break; + case EncodeDims(8, 5): index = 5; break; + case EncodeDims(8, 6): index = 6; break; + case EncodeDims(8, 8): index = 7; break; + case EncodeDims(10, 5): index = 8; break; + case EncodeDims(10, 6): index = 9; break; + case EncodeDims(10, 8): index = 10; break; + case EncodeDims(10, 10): index = 11; break; + case EncodeDims(12, 10): index = 12; break; + case EncodeDims(12, 12): index = 13; break; + default: + assert(false && "Unknown footprint dimensions. This should have been caught sooner."); + break; + } + + static const auto* const kASTCPartitionTrees = + new std::array {{ + GenerateASTCPartitionTree(Footprint::Get4x4()), + GenerateASTCPartitionTree(Footprint::Get5x4()), + GenerateASTCPartitionTree(Footprint::Get5x5()), + GenerateASTCPartitionTree(Footprint::Get6x5()), + GenerateASTCPartitionTree(Footprint::Get6x6()), + GenerateASTCPartitionTree(Footprint::Get8x5()), + GenerateASTCPartitionTree(Footprint::Get8x6()), + GenerateASTCPartitionTree(Footprint::Get8x8()), + GenerateASTCPartitionTree(Footprint::Get10x5()), + GenerateASTCPartitionTree(Footprint::Get10x6()), + GenerateASTCPartitionTree(Footprint::Get10x8()), + GenerateASTCPartitionTree(Footprint::Get10x10()), + GenerateASTCPartitionTree(Footprint::Get12x10()), + GenerateASTCPartitionTree(Footprint::Get12x12()), + }}; + + const PartitionTree& parts_vptree = kASTCPartitionTrees->at(index); + std::vector results; + parts_vptree.Search(candidate, k, &results, nullptr); + return results; +} + +// Returns the valid ASTC partition that is closest to the candidate based on +// the PartitionMetric defined above. +const Partition& FindClosestASTCPartition(const Partition& candidate) { + // Given a candidate, the closest valid partition will likely not be an exact + // match. Consider all of the texels for which this valid partition differs + // with the candidate. + // + // If the valid partition has more subsets than the candidate, then all of the + // highest subset will be included in the mismatched texels. Since the number + // of possible partitions with increasing subsets grows exponentially, the + // chance that a valid partition with fewer subsets appears within the first + // few closest partitions is relatively high. Empirically, we can usually find + // a partition with at most |candidate.num_parts| number of subsets within the + // first four closest partitions. + constexpr int kSearchItems = 4; + + const std::vector results = + FindKClosestASTCPartitions(candidate, kSearchItems); + + // Optimistically, look for result with the same number of subsets. + for (const auto& result : results) { + if (result->num_parts == candidate.num_parts) { + return *result; + } + } + + // If all else fails, then at least find the result with fewer subsets than + // we asked for. + for (const auto& result : results) { + if (result->num_parts < candidate.num_parts) { + return *result; + } + } + + assert(false && + "Could not find partition with acceptable number of subsets!"); + return *(results[0]); +} + +} // namespace astc_codec diff --git a/src/decoder/partition.h b/src/decoder/partition.h new file mode 100644 index 0000000..4f64acb --- /dev/null +++ b/src/decoder/partition.h @@ -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. + +#ifndef ASTC_CODEC_DECODER_PARTITION_H_ +#define ASTC_CODEC_DECODER_PARTITION_H_ + +#include "src/base/optional.h" +#include "src/decoder/footprint.h" + +#include + +namespace astc_codec { + +struct Partition; + +// Determines the "difference" between any two partitions of the same size. +// This metric attempts to find the best one to one mapping from the labels in +// partition a against the labels in partition b. Once that mapping is found, it +// returns the number of pixels that are mismatched between the two. Each +// partition is expected to start in the upper left corner of the block and +// proceed in raster-scan order. Two partitions are equal if the mapping is +// bijective. This metric is a metric in the mathematical sense. In other words +// it has the following properties: +// +// 1) PartitionMetric(a, b) >= 0 +// 2) PartitionMetric(a, b) == PartitionMetric(b, a) +// 3) PartitionMetric(a, b) == 0 iff a == b +// 4) PartitionMetric(a, b) + PartitionMetric(b, c) >= PartitionMetric(a, c) +// +// Throws an error if one partition's footprint is not equal to the other. +int PartitionMetric(const Partition& a, const Partition& b); + +// A partition is a way to divide up an ASTC block into disjoint subsets such +// that each subset uses a different set of endpoints. This is used to increase +// the compression quality of blocks. One way to store such a partition is to +// assign an ID to use with a predetermined decoding method. Here we store the +// logical representation of partitions by keeping a per-pixel label. All pixels +// that share a label belong to the same subset. +struct Partition { + // The footprint width and height of this partition. This determines the size + // of the assignment array. + Footprint footprint; + + // The number of subsets in this partition. The values in the partition + // assignment fall within the range [0, num_parts). The maximum number of + // parts supported is four. + int num_parts; + + // The 10-bit partition ID as stored in bits 13-22 of multi-part ASTC blocks. + // (See Section C.2.9) If there is no guarantee that this partition is a valid + // ASTC partition, this should be set to absl::nullopt. + base::Optional partition_id; + + // A value in the range [0, num_parts) corresponding to the label for + // the given texel (x, y) in [0, footprint_width) x [0, footprint_height) + // using a raster-order layout. + std::vector assignment; + + // Returns true only if their "distance" is zero, i.e. if they have compatible + // subset assignments. + bool operator==(const Partition& other) const { + return PartitionMetric(*this, other) == 0; + } +}; + +// Generates the ASTC partition assignment for the given block attributes. +Partition GetASTCPartition(const Footprint& footprint, int num_parts, + int partition_id); + +// Returns the |k| valid ASTC partitions that are closest to the candidate based +// on the PartitionMetric defined above. +const std::vector FindKClosestASTCPartitions( + const Partition& candidate, int k); + +// Returns the valid ASTC partition closest to the candidate with at most as +// many subsets as the |candidate|. Note: this is not a deterministic function, +// as the underlying valid partitions are sorted using a hash map and a distance +// function whose range is the natural numbers. The chances that two or more +// partitions are equally 'closest' is possible, in which case this function +// makes no guarantees about which one it will return. For more control, use +// FindKClosestASTCPartitions above. +const Partition& FindClosestASTCPartition(const Partition& candidate); + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_PARTITION_H_ diff --git a/src/decoder/physical_astc_block.cc b/src/decoder/physical_astc_block.cc new file mode 100644 index 0000000..7cc4d8e --- /dev/null +++ b/src/decoder/physical_astc_block.cc @@ -0,0 +1,761 @@ +// 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/math_utils.h" +#include "src/base/optional.h" +#include "src/base/uint128.h" +#include "src/decoder/integer_sequence_codec.h" + +#include +#include + +namespace astc_codec { + +namespace { + +static_assert(static_cast(ColorEndpointMode::kNumColorEndpointModes) == 16, + "There are only sixteen color endpoint modes defined in the " + "ASTC specification. If this is false, then the enum may be " + "incorrect."); + +constexpr int kASTCBlockSizeBits = 128; +constexpr int kASTCBlockSizeBytes = kASTCBlockSizeBits / 8; +constexpr uint32_t kVoidExtentMaskBits = 9; +constexpr uint32_t kVoidExtentMask = 0x1FC; +constexpr int kWeightGridMinBitLength = 24; +constexpr int kWeightGridMaxBitLength = 96; +constexpr int kMaxNumPartitions = 4; +constexpr int kMaxNumWeights = 64; + +// These are the overall block modes defined in table C.2.8. There are 10 +// weight grid encoding schemes + void extent. +enum class BlockMode { + kB4_A2, + kB8_A2, + kA2_B8, + kA2_B6, + kB2_A2, + k12_A2, + kA2_12, + k6_10, + k10_6, + kA6_B6, + kVoidExtent, +}; + +struct WeightGridProperties { + int width; + int height; + int range; +}; + +// Local function prototypes +base::Optional DecodeBlockMode(const base::UInt128 astc_bits); +base::Optional DecodeWeightProps( + const base::UInt128 astc_bits, std::string* error); +std::array DecodeVoidExtentCoords(const base::UInt128 astc_bits); +bool DecodeDualPlaneBit(const base::UInt128 astc_bits); +int DecodeNumPartitions(const base::UInt128 astc_bits); +int DecodeNumWeightBits(const base::UInt128 astc_bits); +int DecodeDualPlaneBitStartPos(const base::UInt128 astc_bits); +ColorEndpointMode DecodeEndpointMode(const base::UInt128 astc_bits, + int partition); +int DecodeNumColorValues(const base::UInt128 astc_bits); + +// Returns the block mode, if it's valid. +base::Optional DecodeBlockMode(const base::UInt128 astc_bits) { + using Result = base::Optional; + const uint64_t low_bits = astc_bits.LowBits(); + if (base::GetBits(low_bits, 0, kVoidExtentMaskBits) == kVoidExtentMask) { + return Result(BlockMode::kVoidExtent); + } + + if (base::GetBits(low_bits, 0, 2) != 0) { + const uint64_t mode_bits = base::GetBits(low_bits, 2, 2); + switch (mode_bits) { + case 0: return Result(BlockMode::kB4_A2); + case 1: return Result(BlockMode::kB8_A2); + case 2: return Result(BlockMode::kA2_B8); + case 3: return base::GetBits(low_bits, 8, 1) ? + Result(BlockMode::kB2_A2) : Result(BlockMode::kA2_B6); + } + } else { + const uint64_t mode_bits = base::GetBits(low_bits, 5, 4); + if ((mode_bits & 0xC) == 0x0) { + if (base::GetBits(low_bits, 0, 4) == 0) { + // Reserved. + return Result(); + } else { + return Result(BlockMode::k12_A2); + } + } else if ((mode_bits & 0xC) == 0x4) { + return Result(BlockMode::kA2_12); + } else if (mode_bits == 0xC) { + return Result(BlockMode::k6_10); + } else if (mode_bits == 0xD) { + return Result(BlockMode::k10_6); + } else if ((mode_bits & 0xC) == 0x8) { + return Result(BlockMode::kA6_B6); + } + } + + return Result(); +} + +base::Optional DecodeWeightProps( + const base::UInt128 astc_bits, std::string* error) { + auto block_mode = DecodeBlockMode(astc_bits); + if (!block_mode) { + *error = "Reserved block mode"; + return {}; + } + + // The dimensions of the weight grid and their range + WeightGridProperties props; + + // Determine the weight extents based on the block mode + const uint32_t low_bits = + static_cast(astc_bits.LowBits() & 0xFFFFFFFF); + switch (block_mode.value()) { + case BlockMode::kB4_A2: { + int a = base::GetBits(low_bits, 5, 2); + int b = base::GetBits(low_bits, 7, 2); + props.width = b + 4; + props.height = a + 2; + } + break; + + case BlockMode::kB8_A2: { + int a = base::GetBits(low_bits, 5, 2); + int b = base::GetBits(low_bits, 7, 2); + props.width = b + 8; + props.height = a + 2; + } + break; + + case BlockMode::kA2_B8: { + int a = base::GetBits(low_bits, 5, 2); + int b = base::GetBits(low_bits, 7, 2); + props.width = a + 2; + props.height = b + 8; + } + break; + + case BlockMode::kA2_B6: { + int a = base::GetBits(low_bits, 5, 2); + int b = base::GetBits(low_bits, 7, 1); + props.width = a + 2; + props.height = b + 6; + } + break; + + case BlockMode::kB2_A2: { + int a = base::GetBits(low_bits, 5, 2); + int b = base::GetBits(low_bits, 7, 1); + props.width = b + 2; + props.height = a + 2; + } + break; + + case BlockMode::k12_A2: { + int a = base::GetBits(low_bits, 5, 2); + props.width = 12; + props.height = a + 2; + } + break; + + case BlockMode::kA2_12: { + int a = base::GetBits(low_bits, 5, 2); + props.width = a + 2; + props.height = 12; + } + break; + + case BlockMode::k6_10: { + props.width = 6; + props.height = 10; + } + break; + + case BlockMode::k10_6: { + props.width = 10; + props.height = 6; + } + break; + + case BlockMode::kA6_B6: { + int a = base::GetBits(low_bits, 5, 2); + int b = base::GetBits(low_bits, 9, 2); + props.width = a + 6; + props.height = b + 6; + } + break; + + // Void extent blocks have no weight grid. + case BlockMode::kVoidExtent: + *error = "Void extent block has no weight grid"; + return {}; + + // We have a valid block mode which isn't a void extent? We + // should be able to decode the weight grid dimensions. + default: + assert(false && "Error decoding weight grid"); + *error = "Internal error"; + return {}; + } + + // Determine the weight range based on the block mode + uint32_t r = base::GetBits(low_bits, 4, 1); + switch (block_mode.value()) { + case BlockMode::kB4_A2: + case BlockMode::kB8_A2: + case BlockMode::kA2_B8: + case BlockMode::kA2_B6: + case BlockMode::kB2_A2: { + r |= base::GetBits(low_bits, 0, 2) << 1; + } + break; + + case BlockMode::k12_A2: + case BlockMode::kA2_12: + case BlockMode::k6_10: + case BlockMode::k10_6: + case BlockMode::kA6_B6: { + r |= base::GetBits(low_bits, 2, 2) << 1; + } + break; + + // We have a valid block mode which doesn't have weights? We + // should have caught this earlier. + case BlockMode::kVoidExtent: + default: + assert(false && "Error decoding weight grid"); + *error = "Internal error"; + return {}; + } + + // Decode the range... + // High bit is in bit 9 unless we're using a particular block mode + uint32_t h = base::GetBits(low_bits, 9, 1); + if (block_mode == BlockMode::kA6_B6) { + h = 0; + } + + // Figure out the range of the weights (Table C.2.7) + constexpr std::array kWeightRanges = {{ + -1, -1, 1, 2, 3, 4, 5, 7, -1, -1, 9, 11, 15, 19, 23, 31 + }}; + + assert(((h << 3) | r) < kWeightRanges.size()); + + props.range = kWeightRanges.at((h << 3) | r); + if (props.range < 0) { + *error = "Reserved range for weight bits"; + return {}; + } + + // Error checking -- do we have too many weights? + int num_weights = props.width * props.height; + if (DecodeDualPlaneBit(astc_bits)) { + num_weights *= 2; + } + + if (kMaxNumWeights < num_weights) { + *error = "Too many weights specified"; + return {}; + } + + // Do we have too many weight bits? + const int bit_count = + IntegerSequenceCodec::GetBitCountForRange(num_weights, props.range); + + if (bit_count < kWeightGridMinBitLength) { + *error = "Too few bits required for weight grid"; + return {}; + } + + if (kWeightGridMaxBitLength < bit_count) { + *error = "Too many bits required for weight grid"; + return {}; + } + + return props; +} + +// Returns the four 13-bit integers that define the range of texture +// coordinates present in a void extent block as defined in Section +// C.2.23 of the specification. The coordinates returned are of +// the form (min_s, max_s, min_t, max_t) +std::array DecodeVoidExtentCoords(const base::UInt128 astc_bits) { + const uint64_t low_bits = astc_bits.LowBits(); + + std::array coords; + for (int i = 0; i < 4; ++i) { + coords[i] = static_cast(base::GetBits(low_bits, 12 + 13 * i, 13)); + } + + return coords; +} + +bool DecodeDualPlaneBit(const base::UInt128 astc_bits) { + base::Optional block_mode = DecodeBlockMode(astc_bits); + + // Void extent blocks certainly aren't dual-plane. + if (block_mode == BlockMode::kVoidExtent) { + return false; + } + + // One special block mode doesn't have any dual plane bit + if (block_mode == BlockMode::kA6_B6) { + return false; + } + + // Otherwise, dual plane is determined by the 10th bit. + constexpr int kDualPlaneBitPosition = 10; + return base::GetBits(astc_bits, kDualPlaneBitPosition, 1) != 0; +} + +int DecodeNumPartitions(const base::UInt128 astc_bits) { + constexpr int kNumPartitionsBitPosition = 11; + constexpr int kNumPartitionsBitLength = 2; + + // Non-void extent blocks + const uint64_t low_bits = astc_bits.LowBits(); + const int num_partitions = 1 + static_cast( + base::GetBits(low_bits, + kNumPartitionsBitPosition, + kNumPartitionsBitLength)); + assert(num_partitions > 0); + assert(num_partitions <= kMaxNumPartitions); + + return num_partitions; +} + +int DecodeNumWeightBits(const base::UInt128 astc_bits) { + std::string error; + auto maybe_weight_props = DecodeWeightProps(astc_bits, &error); + if (!maybe_weight_props.hasValue()) { + return 0; // No weights? No weight bits... + } + + const auto weight_props = maybe_weight_props.value(); + + // Figure out the number of weights + int num_weights = weight_props.width * weight_props.height; + if (DecodeDualPlaneBit(astc_bits)) { + num_weights *= 2; + } + + // The number of bits is determined by the number of values + // that are going to be encoded using the given ise_counts. + return IntegerSequenceCodec::GetBitCountForRange( + num_weights, weight_props.range); +} + +// Returns the number of bits after the weight data used to +// store additional CEM bits. +int DecodeNumExtraCEMBits(const base::UInt128 astc_bits) { + const int num_partitions = DecodeNumPartitions(astc_bits); + + // Do we only have one partition? + if (num_partitions == 1) { + return 0; + } + + // Do we have a shared CEM? + constexpr int kSharedCEMBitPosition = 23; + constexpr int kSharedCEMBitLength = 2; + const base::UInt128 shared_cem = + base::GetBits(astc_bits, kSharedCEMBitPosition, kSharedCEMBitLength); + if (shared_cem == 0) { + return 0; + } + + const std::array extra_cem_bits_for_partition = {{ 0, 2, 5, 8 }}; + return extra_cem_bits_for_partition[num_partitions - 1]; +} + +// Returns the starting position of the dual plane channel. This comes +// before the weight data and extra CEM bits. +int DecodeDualPlaneBitStartPos(const base::UInt128 astc_bits) { + const int start_pos = kASTCBlockSizeBits + - DecodeNumWeightBits(astc_bits) + - DecodeNumExtraCEMBits(astc_bits); + + if (DecodeDualPlaneBit(astc_bits)) { + return start_pos - 2; + } else { + return start_pos; + } +} + +// Decodes a CEM mode based on the partition number. +ColorEndpointMode DecodeEndpointMode(const base::UInt128 astc_bits, + int partition) { + int num_partitions = DecodeNumPartitions(astc_bits); + assert(partition >= 0); + assert(partition < num_partitions); + + // Do we only have one partition? + uint64_t low_bits = astc_bits.LowBits(); + if (num_partitions == 1) { + uint64_t cem = base::GetBits(low_bits, 13, 4); + return static_cast(cem); + } + + // More than one partition ... do we have a shared CEM? + if (DecodeNumExtraCEMBits(astc_bits) == 0) { + const uint64_t shared_cem = base::GetBits(low_bits, 25, 4); + return static_cast(shared_cem); + } + + // More than one partition and no shared CEM... + uint64_t cem = base::GetBits(low_bits, 23, 6); + const int base_cem = static_cast(((cem & 0x3) - 1) * 4); + cem >>= 2; // Skip the base CEM bits + + // The number of extra CEM bits at the end of the weight grid is + // determined by the number of partitions and what the base cem mode is... + const int num_extra_cem_bits = DecodeNumExtraCEMBits(astc_bits); + const int extra_cem_start_pos = kASTCBlockSizeBits + - num_extra_cem_bits + - DecodeNumWeightBits(astc_bits); + + base::UInt128 extra_cem = + base::GetBits(astc_bits, extra_cem_start_pos, num_extra_cem_bits); + cem |= extra_cem.LowBits() << 4; + + // Decode C and M per Figure C.4 + int c = -1, m = -1; + for (int i = 0; i < num_partitions; ++i) { + if (i == partition) { + c = cem & 0x1; + } + cem >>= 1; + } + + for (int i = 0; i < num_partitions; ++i) { + if (i == partition) { + m = cem & 0x3; + } + cem >>= 2; + } + + assert(c >= 0); + assert(m >= 0); + + // Compute the mode based on C and M + const int mode = base_cem + 4 * c + m; + assert(mode < static_cast(ColorEndpointMode::kNumColorEndpointModes)); + return static_cast(mode); +} + +int DecodeNumColorValues(const base::UInt128 astc_bits) { + int num_color_values = 0; + auto num_partitions = DecodeNumPartitions(astc_bits); + for (int i = 0; i < num_partitions; ++i) { + ColorEndpointMode endpoint_mode = DecodeEndpointMode(astc_bits, i); + num_color_values += NumColorValuesForEndpointMode(endpoint_mode); + } + + return num_color_values; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +static_assert(sizeof(PhysicalASTCBlock) == PhysicalASTCBlock::kSizeInBytes, + "The size of the struct should be the size of the block so that" + "we can effectively use them contiguously in memory."); + +PhysicalASTCBlock::PhysicalASTCBlock(const base::UInt128 astc_block) + : astc_bits_(astc_block) {} + +PhysicalASTCBlock::PhysicalASTCBlock(const std::string& encoded_block) + : astc_bits_([&encoded_block]() { + assert(encoded_block.size() == PhysicalASTCBlock::kSizeInBytes); + base::UInt128 astc_bits = 0; + int shift = 0; + for (const unsigned char c : encoded_block) { + astc_bits |= base::UInt128(static_cast(c)) << shift; + shift += 8; + } + return astc_bits; + }()) +{ } + +base::Optional PhysicalASTCBlock::IsIllegalEncoding() const { + // If the block is not a void extent block, then it must have + // weights specified. DecodeWeightProps will return the weight specifications + // if they exist and are legal according to C.2.24, and will otherwise be + // empty. + base::Optional block_mode = DecodeBlockMode(astc_bits_); + if (block_mode != BlockMode::kVoidExtent) { + std::string error; + auto maybe_weight_props = DecodeWeightProps(astc_bits_, &error); + if (!maybe_weight_props.hasValue()) { + return error; + } + } + + // Check void extent blocks... + if (block_mode == BlockMode::kVoidExtent) { + // ... for reserved bits incorrectly set + if (base::GetBits(astc_bits_, 10, 2) != 0x3) { + return std::string("Reserved bits set for void extent block"); + } + + // ... for incorrectly defined texture coordinates + std::array coords = DecodeVoidExtentCoords(astc_bits_); + + bool coords_all_1s = true; + for (const auto coord : coords) { + coords_all_1s &= coord == ((1 << 13) - 1); + } + + if (!coords_all_1s && (coords[0] >= coords[1] || coords[2] >= coords[3])) { + return std::string("Void extent texture coordinates are invalid"); + } + } + + // If the number of color values exceeds a threshold and it isn't a void + // extent block then we've run into an error + if (block_mode != BlockMode::kVoidExtent) { + int num_color_vals = DecodeNumColorValues(astc_bits_); + if (num_color_vals > 18) { + return std::string("Too many color values"); + } + + // The maximum number of available color bits is the number of + // bits between the dual plane bits and the base CEM. This must + // be larger than a threshold defined in C.2.24. + + // Dual plane bit starts after weight bits and CEM + const int num_partitions = DecodeNumPartitions(astc_bits_); + const int dual_plane_start_pos = DecodeDualPlaneBitStartPos(astc_bits_); + const int color_start_bit = (num_partitions == 1) ? 17 : 29; + + const int required_color_bits = ((13 * num_color_vals) + 4) / 5; + const int available_color_bits = dual_plane_start_pos - color_start_bit; + if (available_color_bits < required_color_bits) { + return std::string("Not enough color bits"); + } + + // If we have four partitions and a dual plane then we have a problem. + if (num_partitions == 4 && DecodeDualPlaneBit(astc_bits_)) { + return std::string("Both four partitions and dual plane specified"); + } + } + + // Otherwise we're OK + return { }; +} + +bool PhysicalASTCBlock::IsVoidExtent() const { + // If it's an error block, it's not a void extent block. + if (IsIllegalEncoding()) { + return false; + } + + return DecodeBlockMode(astc_bits_) == BlockMode::kVoidExtent; +} + +base::Optional> PhysicalASTCBlock::VoidExtentCoords() const { + if (IsIllegalEncoding() || !IsVoidExtent()) { + return { }; + } + + // If void extent coords are all 1's then these are not valid void extent + // coords + const uint64_t ve_mask = 0xFFFFFFFFFFFFFDFFULL; + const uint64_t const_blk_mode = 0xFFFFFFFFFFFFFDFCULL; + if ((ve_mask & astc_bits_.LowBits()) == const_blk_mode) { + return {}; + } + + return DecodeVoidExtentCoords(astc_bits_); +} + +bool PhysicalASTCBlock::IsDualPlane() const { + // If it's an error block, then we aren't a dual plane block + if (IsIllegalEncoding()) { + return false; + } + + return DecodeDualPlaneBit(astc_bits_); +} + +// Returns the number of weight bits present in this block +base::Optional PhysicalASTCBlock::NumWeightBits() const { + // If it's an error block, then we have no weight bits. + if (IsIllegalEncoding()) return { }; + + // If it's a void extent block, we have no weight bits + if (IsVoidExtent()) return { }; + + return DecodeNumWeightBits(astc_bits_); +} + +base::Optional PhysicalASTCBlock::WeightStartBit() const { + if (IsIllegalEncoding()) return { }; + if (IsVoidExtent()) return { }; + + return kASTCBlockSizeBits - DecodeNumWeightBits(astc_bits_); +} + +base::Optional> PhysicalASTCBlock::WeightGridDims() const { + std::string error; + auto weight_props = DecodeWeightProps(astc_bits_, &error); + + if (!weight_props.hasValue()) return { }; + if (IsIllegalEncoding()) return { }; + + const auto props = weight_props.value(); + return {{{ props.width, props.height }}}; +} + +base::Optional PhysicalASTCBlock::WeightRange() const { + std::string error; + auto weight_props = DecodeWeightProps(astc_bits_, &error); + + if (!weight_props.hasValue()) return { }; + if (IsIllegalEncoding()) return { }; + + return weight_props.value().range; +} + +base::Optional PhysicalASTCBlock::DualPlaneChannel() const { + if (!IsDualPlane()) return { }; + + int dual_plane_start_pos = DecodeDualPlaneBitStartPos(astc_bits_); + auto plane_bits = base::GetBits(astc_bits_, dual_plane_start_pos, 2); + return base::Optional(static_cast(plane_bits.LowBits())); +} + +base::Optional PhysicalASTCBlock::ColorStartBit() const { + if (IsVoidExtent()) { + return 64; + } + + auto num_partitions = NumPartitions(); + if (!num_partitions) return { }; + + return (num_partitions == 1) ? 17 : 29; +} + +base::Optional PhysicalASTCBlock::NumColorValues() const { + // If we have a void extent block, then we have four color values + if (IsVoidExtent()) { + return 4; + } + + // If we have an illegal encoding, then we have no color values + if (IsIllegalEncoding()) return { }; + + return DecodeNumColorValues(astc_bits_); +} + +void PhysicalASTCBlock::GetColorValuesInfo(int* const color_bits, + int* const color_range) const { + // Figure out the range possible for the number of values we have... + const int dual_plane_start_pos = DecodeDualPlaneBitStartPos(astc_bits_); + const int max_color_bits = dual_plane_start_pos - ColorStartBit().value(); + const int num_color_values = NumColorValues().value(); + for (int range = 255; range > 0; --range) { + const int bitcount = + IntegerSequenceCodec::GetBitCountForRange(num_color_values, range); + if (bitcount <= max_color_bits) { + if (color_bits != nullptr) { + *color_bits = bitcount; + } + + if (color_range != nullptr) { + *color_range = range; + } + return; + } + } + + assert(false && + "This means that even if we have a range of one there aren't " + "enough bits to store the color values, and our encoding is " + "illegal."); +} + +base::Optional PhysicalASTCBlock::NumColorBits() const { + if (IsIllegalEncoding()) return { }; + + if (IsVoidExtent()) { + return 64; + } + + int color_bits; + GetColorValuesInfo(&color_bits, nullptr); + return color_bits; +} + +base::Optional PhysicalASTCBlock::ColorValuesRange() const { + if (IsIllegalEncoding()) return { }; + + if (IsVoidExtent()) { + return (1 << 16) - 1; + } + + int color_range; + GetColorValuesInfo(nullptr, &color_range); + return color_range; +} + +base::Optional PhysicalASTCBlock::NumPartitions() const { + // Error blocks have no partitions + if (IsIllegalEncoding()) return { }; + + // Void extent blocks have no partitions either + if (DecodeBlockMode(astc_bits_) == BlockMode::kVoidExtent) { + return { }; + } + + // All others have some number of partitions + return DecodeNumPartitions(astc_bits_); +} + +base::Optional PhysicalASTCBlock::PartitionID() const { + auto num_partitions = NumPartitions(); + if (!num_partitions || num_partitions == 1) return { }; + + const uint64_t low_bits = astc_bits_.LowBits(); + return static_cast(base::GetBits(low_bits, 13, 10)); +} + +base::Optional PhysicalASTCBlock::GetEndpointMode( + int partition) const { + // Error block? + if (IsIllegalEncoding()) return { }; + + // Void extent blocks have no endpoint modes + if (DecodeBlockMode(astc_bits_) == BlockMode::kVoidExtent) { + return { }; + } + + // Do we even have a CEM for this partition? + if (partition < 0 || DecodeNumPartitions(astc_bits_) <= partition) { + return { }; + } + + return DecodeEndpointMode(astc_bits_, partition); +} + +} // namespace astc_codec diff --git a/src/decoder/physical_astc_block.h b/src/decoder/physical_astc_block.h new file mode 100644 index 0000000..1b04bdd --- /dev/null +++ b/src/decoder/physical_astc_block.h @@ -0,0 +1,128 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_PHYSICAL_ASTC_BLOCK_H_ +#define ASTC_CODEC_DECODER_PHYSICAL_ASTC_BLOCK_H_ + +// The logic in this file is based on the ASTC specification, which can be +// found here: +// https://www.opengl.org/registry/specs/KHR/texture_compression_astc_hdr.txt + +#include "src/base/optional.h" +#include "src/base/uint128.h" +#include "src/decoder/types.h" + +#include + +namespace astc_codec { + +// A PhysicalASTCBlock contains all 128 bits and the logic for decoding the +// various internals of an ASTC block. +class PhysicalASTCBlock { + public: + // The physical size in bytes of an ASTC block + static const size_t kSizeInBytes = 16; + + // Initializes an ASTC block based on the encoded string. + explicit PhysicalASTCBlock(const std::string& encoded_block); + explicit PhysicalASTCBlock(const base::UInt128 astc_block); + + // Returns the 128 bits of this ASTC block. + base::UInt128 GetBlockBits() const { return astc_bits_; } + + // Weights are stored in a grid that may not have the same dimensions + // as the block dimensions. This allows us to see what the physical + // dimensions are of the grid. + base::Optional> WeightGridDims() const; + + // The weight range is the maximum value a weight can take in the + // weight grid. + base::Optional WeightRange() const; + + // Returns true if the block encoding specifies a void-extent block. This + // kind of block stores a single color to be used for every pixel in the + // block. + bool IsVoidExtent() const; + + // Returns the values (min_s, max_s, min_t, max_t) as defined in the void + // extent block as the range of texture coordinates for which this block is + // defined. (See Section C.2.23) + base::Optional> VoidExtentCoords() const; + + // Returns true if the block contains two separate weight grids. One used + // for the channel returned by DualPlaneChannel() and one used by the other + // channels. + bool IsDualPlane() const; + + // Returns the channel used as the "dual plane". The return value is only + // meaningful if IsDualPlane() returns true... + base::Optional DualPlaneChannel() const; + + // Returns a reason that the encoding doesn't adhere to the specification. + // If the encoding is legal, then this returns a nullptr. This allows us to + // still use code of the form: + // + // if (IsIllegalEncoding()) { + // ... error ... + // } + // ... no error ... + // + // However, it also helps with debugging since we can find problems with + // encodings a lot faster. + base::Optional IsIllegalEncoding() const; + + // Returns the number of weight bits present in this block. + base::Optional NumWeightBits() const; + + // Returns the starting position within the range [0, 127] of the + // weight data within the block. + base::Optional WeightStartBit() const; + + // Returns the number of endpoint pairs used in this block. + base::Optional NumPartitions() const; + + // Returns the seed used to determine the partition for a given + // (x, y) coordinate within the block. Determined using the + // block size and the function as described in the specification. + base::Optional PartitionID() const; + + // Returns the color endpoint mode for the given partition index. + base::Optional GetEndpointMode(int partition) const; + + // Returns the starting position within the range [0, 127] of the + // color data within the block. + base::Optional ColorStartBit() const; + + // Returns the number of integers used to represent the color endpoints. + base::Optional NumColorValues() const; + + // Returns the number of bits used to represent the color endpoints. + base::Optional NumColorBits() const; + + // Returns the maximum value that each of the encoded integers used to + // represent the color endpoints can take. + base::Optional ColorValuesRange() const; + + private: + const base::UInt128 astc_bits_; + + // The logic to return the number of color bits and the color values range + // is very similar, so it's probably best to abstract it away into its own + // function. + void GetColorValuesInfo(int* color_bits, int* color_range) const; +}; + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_PHYSICAL_ASTC_BLOCK_H_ diff --git a/src/decoder/quantization.cc b/src/decoder/quantization.cc new file mode 100644 index 0000000..db99682 --- /dev/null +++ b/src/decoder/quantization.cc @@ -0,0 +1,462 @@ +// 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/base/math_utils.h" + +#include +#include +#include +#include +#include +#include + +namespace astc_codec { + +namespace { + +// Trit unquantization procedure as described in Section C.2.13 +int GetUnquantizedTritValue(int trit, int bits, int range) { + int a = (bits & 1) ? 0x1FF : 0; + int b = 0, c = 0; + switch (range) { + case 5: { + b = 0; + c = 204; + } + break; + + case 11: { + int x = (bits >> 1) & 0x1; + b = (x << 1) | (x << 2) | (x << 4) | (x << 8); + c = 93; + } + break; + + case 23: { + int x = (bits >> 1) & 0x3; + b = x | (x << 2) | (x << 7); + c = 44; + } + break; + + case 47: { + int x = (bits >> 1) & 0x7; + b = x | (x << 6); + c = 22; + } + break; + + case 95: { + int x = (bits >> 1) & 0xF; + b = (x >> 2) | (x << 5); + c = 11; + } + break; + + case 191: { + int x = (bits >> 1) & 0x1F; + b = (x >> 4) | (x << 4); + c = 5; + } + break; + + default: + assert(false && "Illegal trit encoding"); + break; + } + + int t = trit * c + b; + t ^= a; + t = (a & 0x80) | (t >> 2); + return t; +} + +// Quint unquantization procedure as described in Section C.2.13 +int GetUnquantizedQuintValue(int quint, int bits, int range) { + int a = (bits & 1) ? 0x1FF : 0; + int b = 0, c = 0; + switch (range) { + case 9: { + b = 0; + c = 113; + } + break; + + case 19: { + int x = (bits >> 1) & 0x1; + b = (x << 2) | (x << 3) | (x << 8); + c = 54; + } + break; + + case 39: { + int x = (bits >> 1) & 0x3; + b = (x >> 1) | (x << 1) | (x << 7); + c = 26; + } + break; + + case 79: { + int x = (bits >> 1) & 0x7; + b = (x >> 1) | (x << 6); + c = 13; + } + break; + + case 159: { + int x = (bits >> 1) & 0xF; + b = (x >> 3) | (x << 5); + c = 6; + } + break; + + default: + assert(false && "Illegal quint encoding"); + break; + } + + int t = quint * c + b; + t ^= a; + t = (a & 0x80) | (t >> 2); + return t; +} + +// Trit unquantization procedure as described in Section C.2.17. In the code +// below, the variables a, b, and c correspond to the columns A, B, and C in +// the specification. +int GetUnquantizedTritWeight(int trit, int bits, int range) { + int a = (bits & 1) ? 0x7F : 0; + int b = 0, c = 0; + switch (range) { + case 2: + return (std::array {{ 0, 32, 63 }})[trit]; + + case 5: + c = 50; + b = 0; + break; + + case 11: { + c = 23; + b = (bits >> 1) & 1; + b |= (b << 2) | (b << 6); + } + break; + + case 23: { + c = 11; + b = (bits >> 1) & 0x3; + b |= (b << 5); + } + break; + + default: + assert(false && "Illegal trit encoding"); + break; + } + + int t = trit * c + b; + t ^= a; + t = (a & 0x20) | (t >> 2); + return t; +} + +// Quint unquantization procedure as described in Section C.2.17. In the code +// below, the variables a, b, and c correspond to the columns A, B, and C in +// the specification. +int GetUnquantizedQuintWeight(int quint, int bits, int range) { + int a = (bits & 1) ? 0x7F : 0; + int b = 0, c = 0; + switch (range) { + case 4: + return (std::array {{ 0, 16, 32, 47, 63 }})[quint]; + + case 9: + c = 28; + b = 0; + break; + + case 19: { + c = 13; + b = (bits >> 1) & 0x1; + b = (b << 1) | (b << 6); + } + break; + + default: + assert(false && "Illegal quint encoding"); + break; + } + + int t = quint * c + b; + t ^= a; + t = (a & 0x20) | (t >> 2); + return t; +} + +// A Quantization map allows us to convert to/from values that are quantized +// according to the ASTC spec. +class QuantizationMap { + public: + int Quantize(int x) const { + return x < quantization_map_.size() ? quantization_map_.at(x) : 0; + } + + int Unquantize(int x) const { + return x < unquantization_map_.size() ? unquantization_map_.at(x) : 0; + } + + protected: + QuantizationMap() { } + std::vector quantization_map_; + std::vector unquantization_map_; + + void GenerateQuantizationMap() { + assert(unquantization_map_.size() > 1); + quantization_map_.clear(); + + // TODO(google) For weights, we don't need quantization values all the + // way up to 256, but it doesn't hurt -- just wastes memory, but the code + // is much cleaner this way + for (int i = 0; i < 256; ++i) { + int best_idx = 0; + int best_idx_score = 256; + int idx = 0; + for (int unquantized_val : unquantization_map_) { + const int diff = i - unquantized_val; + const int idx_score = diff * diff; + if (idx_score < best_idx_score) { + best_idx = idx; + best_idx_score = idx_score; + } + idx++; + } + + quantization_map_.push_back(best_idx); + } + } +}; + +template +class TritQuantizationMap : public QuantizationMap { + public: + explicit TritQuantizationMap(int range) : QuantizationMap() { + assert((range + 1) % 3 == 0); + const int num_bits_pow_2 = (range + 1) / 3; + const int num_bits = + num_bits_pow_2 == 0 ? 0 : base::Log2Floor(num_bits_pow_2); + + for (int trit = 0; trit < 3; ++trit) { + for (int bits = 0; bits < (1 << num_bits); ++bits) { + unquantization_map_.push_back(UnquantizationFunc(trit, bits, range)); + } + } + + GenerateQuantizationMap(); + } +}; + +template +class QuintQuantizationMap : public QuantizationMap { + public: + explicit QuintQuantizationMap(int range) : QuantizationMap() { + assert((range + 1) % 5 == 0); + const int num_bits_pow_2 = (range + 1) / 5; + const int num_bits = + num_bits_pow_2 == 0 ? 0 : base::Log2Floor(num_bits_pow_2); + + for (int quint = 0; quint < 5; ++quint) { + for (int bits = 0; bits < (1 << num_bits); ++bits) { + unquantization_map_.push_back(UnquantizationFunc(quint, bits, range)); + } + } + + GenerateQuantizationMap(); + } +}; + +template +class BitQuantizationMap : public QuantizationMap { + public: + explicit BitQuantizationMap(int range) + : QuantizationMap() { + // Make sure that if we're using bits then we have a positive power of two. + assert(base::CountOnes(range + 1) == 1); + + const int num_bits = base::Log2Floor(range + 1); + for (int bits = 0; bits <= range; ++bits) { + // Need to replicate bits until we fill up the bits + int unquantized = bits; + int num_unquantized_bits = num_bits; + while (num_unquantized_bits < TotalUnquantizedBits) { + const int num_dst_bits_to_shift_up = + std::min(num_bits, TotalUnquantizedBits - num_unquantized_bits); + const int num_src_bits_to_shift_down = + num_bits - num_dst_bits_to_shift_up; + unquantized <<= num_dst_bits_to_shift_up; + unquantized |= bits >> num_src_bits_to_shift_down; + num_unquantized_bits += num_dst_bits_to_shift_up; + } + assert(num_unquantized_bits == TotalUnquantizedBits); + + unquantization_map_.push_back(unquantized); + + // Fill half of the quantization map with the previous value for bits + // and the other half with the current value for bits + if (bits > 0) { + const int prev_unquant = unquantization_map_.at(bits - 1); + while (quantization_map_.size() <= (prev_unquant + unquantized) / 2) { + quantization_map_.push_back(bits - 1); + } + } + while (quantization_map_.size() <= unquantized) { + quantization_map_.push_back(bits); + } + } + + assert(quantization_map_.size() == 1 << TotalUnquantizedBits); + } +}; + +using QMap = std::shared_ptr; + +// Returns the quantization map for quantizing color values in [0, 255] with the +// smallest range that can accommodate |r| +static const QuantizationMap* GetQuantMapForValueRange(int r) { + // Endpoint values can be quantized using bits, trits, or quints. Here we + // store the quantization maps for each of the ranges that are supported by + // such an encoding. That way we can choose the proper quantization procedure + // based on the range of values rather than by having complicated switches and + // logic. We must use a std::map here instead of a std::unordered_map because + // of the assumption made in std::upper_bound about the iterators being from a + // poset. + static const auto* const kASTCEndpointQuantization = new std::map { + { 5, QMap(new TritQuantizationMap(5)) }, + { 7, QMap(new BitQuantizationMap<8>(7)) }, + { 9, QMap(new QuintQuantizationMap(9)) }, + { 11, QMap(new TritQuantizationMap(11)) }, + { 15, QMap(new BitQuantizationMap<8>(15)) }, + { 19, QMap(new QuintQuantizationMap(19)) }, + { 23, QMap(new TritQuantizationMap(23)) }, + { 31, QMap(new BitQuantizationMap<8>(31)) }, + { 39, QMap(new QuintQuantizationMap(39)) }, + { 47, QMap(new TritQuantizationMap(47)) }, + { 63, QMap(new BitQuantizationMap<8>(63)) }, + { 79, QMap(new QuintQuantizationMap(79)) }, + { 95, QMap(new TritQuantizationMap(95)) }, + { 127, QMap(new BitQuantizationMap<8>(127)) }, + { 159, QMap(new QuintQuantizationMap(159)) }, + { 191, QMap(new TritQuantizationMap(191)) }, + { 255, QMap(new BitQuantizationMap<8>(255)) }, + }; + + assert(r < 256); + auto itr = kASTCEndpointQuantization->upper_bound(r); + if (itr != kASTCEndpointQuantization->begin()) { + return (--itr)->second.get(); + } + return nullptr; +} + +// Returns the quantization map for weight values in [0, 63] with the smallest +// range that can accommodate |r| +static const QuantizationMap* GetQuantMapForWeightRange(int r) { + // Similar to endpoint quantization, weights can also be stored using trits, + // quints, or bits. Here we store the quantization maps for each of the ranges + // that are supported by such an encoding. + static const auto* const kASTCWeightQuantization = new std::map { + { 1, QMap(new BitQuantizationMap<6>(1)) }, + { 2, QMap(new TritQuantizationMap(2)) }, + { 3, QMap(new BitQuantizationMap<6>(3)) }, + { 4, QMap(new QuintQuantizationMap(4)) }, + { 5, QMap(new TritQuantizationMap(5)) }, + { 7, QMap(new BitQuantizationMap<6>(7)) }, + { 9, QMap(new QuintQuantizationMap(9)) }, + { 11, QMap(new TritQuantizationMap(11)) }, + { 15, QMap(new BitQuantizationMap<6>(15)) }, + { 19, QMap(new QuintQuantizationMap(19)) }, + { 23, QMap(new TritQuantizationMap(23)) }, + { 31, QMap(new BitQuantizationMap<6>(31)) }, + }; + + assert(r < 32); + auto itr = kASTCWeightQuantization->upper_bound(r); + if (itr != kASTCWeightQuantization->begin()) { + return (--itr)->second.get(); + } + return nullptr; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +int QuantizeCEValueToRange(int value, int range_max_value) { + assert(range_max_value >= kEndpointRangeMinValue); + assert(range_max_value <= 255); + assert(value >= 0); + assert(value <= 255); + + const QuantizationMap* map = GetQuantMapForValueRange(range_max_value); + return map ? map->Quantize(value) : 0; +} + +int UnquantizeCEValueFromRange(int value, int range_max_value) { + assert(range_max_value >= kEndpointRangeMinValue); + assert(range_max_value <= 255); + assert(value >= 0); + assert(value <= range_max_value); + + const QuantizationMap* map = GetQuantMapForValueRange(range_max_value); + return map ? map->Unquantize(value) : 0; +} + +int QuantizeWeightToRange(int weight, int range_max_value) { + assert(range_max_value >= 1); + assert(range_max_value <= kWeightRangeMaxValue); + assert(weight >= 0); + assert(weight <= 64); + + // The quantization maps that define weight unquantization expect values in + // the range [0, 64), but the specification quantizes them to the range + // [0, 64] according to C.2.17. This is a slight hack similar to the one in + // the unquantization procedure to return the passed in unquantized value to + // [0, 64) prior to running it through the quantization procedure. + if (weight > 33) { + weight -= 1; + } + const QuantizationMap* map = GetQuantMapForWeightRange(range_max_value); + return map ? map->Quantize(weight) : 0; +} + +int UnquantizeWeightFromRange(int weight, int range_max_value) { + assert(range_max_value >= 1); + assert(range_max_value <= kWeightRangeMaxValue); + assert(weight >= 0); + assert(weight <= range_max_value); + const QuantizationMap* map = GetQuantMapForWeightRange(range_max_value); + int dq = map ? map->Unquantize(weight) : 0; + + // Quantized weights are returned in the range [0, 64), but they should be + // returned in the range [0, 64], so according to C.2.17 we need to add one + // to the result. + assert(dq < 64); + if (dq > 32) { + dq += 1; + } + return dq; +} + +} // namespace astc_codec diff --git a/src/decoder/quantization.h b/src/decoder/quantization.h new file mode 100644 index 0000000..5f7239f --- /dev/null +++ b/src/decoder/quantization.h @@ -0,0 +1,65 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_QUANTIZATION_H_ +#define ASTC_CODEC_DECODER_QUANTIZATION_H_ + +//////////////////////////////////////////////////////////////////////////////// +// +// ASTC Quantization procedures. +// +// The values stored in ASTC blocks tend to be stored in a range much more +// restricted than the logical range used. For example, sometimes weights are +// stored in the range from [0, 3] but are used in the range [0, 64]. The +// process of translating a value to or from this range is known as quantization +// and dequantization. The ranges to which these values can be (de)quantized +// are defined by ISERange[Begin|End]() in integer_sequence_codec.h + +namespace astc_codec { + +// The minimum possible range for a pair of endpoints. If endpoints are +// quantized to something smaller than this, then it would constitute an +// illegal ASTC encoding. +constexpr int kEndpointRangeMinValue = 5; + +// The maximum possible range for a weight value. If weights are quantized to +// something larger than this, then it would constitute an illegal ASTC +// encoding. +constexpr int kWeightRangeMaxValue = 31; + +// Quantizes a value in the range [0, 255] to [0, |range|]. The quantized values +// have no correlation to the input values, and there should be no implicit +// assumptions made about their ordering. Valid values of |range_max_value| are +// in the interval [5, 255] +int QuantizeCEValueToRange(int value, int range_max_value); + +// Unquantizes a value in the range [0, |range|] to [0, 255]. Performs the +// inverse procedure of QuantizeValueToRange. Valid values of |range_max_value| +// are in the interval [5, 255] +int UnquantizeCEValueFromRange(int value, int range_max_value); + +// Quantizes a weight in the range [0, 64] to [0, |range_max_value|]. The +// quantized values have no correlation to the input values, and there should +// be no implicit assumptions made about their ordering. Valid values of +// |range_max_value| are in the interval [1, 31] +int QuantizeWeightToRange(int weight, int range_max_value); + +// Unquantizes a weight in the range [0, |range_max_value|] to [0, 64]. Performs +// the inverse procedure of QuantizeWeightToRange. Valid values of +// |range_max_value| are in the interval [1, 31] +int UnquantizeWeightFromRange(int weight, int range_max_value); + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_QUANTIZATION_H_ 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 + +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + std::string error; + std::unique_ptr file = + astc_codec::ASTCFile::LoadFromMemory(reinterpret_cast(data), + size, &error); + if (file) { + std::vector 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 + +#include + +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 << "(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 data(256); + std::vector 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 {}; + +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(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 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 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 +#include +#include +#include + +#include +#include + +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 kEndpointEncodingModes = {{ + EndpointEncodingMode::kDirectLuma, + EndpointEncodingMode::kDirectLumaAlpha, + EndpointEncodingMode::kBaseScaleRGB, + EndpointEncodingMode::kBaseScaleRGBA, + EndpointEncodingMode::kDirectRGB, + EndpointEncodingMode::kDirectRGBA }}; + +const std::array, 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 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 TestColors( + RgbaColor low, RgbaColor high, int quant, EndpointEncodingMode mode) { + ColorEndpointMode astc_mode; + std::vector 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 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(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 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(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 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 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 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 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 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 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 +#include +#include + +#include + +namespace astc_codec { + +namespace { + +TEST(FootprintTest, ParseAstcFootprintString) { + using ASTCTestPair = std::pair; + const std::array + 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::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 + +#include +#include + +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& Data() { return data_; } + const std::vector& 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 data_; +}; + +namespace std { +static void PrintTo(const vector& vec, ostream* os) { + ios::fmtflags origFlags(os->flags()); + + *os << '{'; + size_t count = 0; + for (vector::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(&data[0x0A]); + uint32_t imageSize = *reinterpret_cast(&data[0x22]); + const uint16_t bitsPerPixel = *reinterpret_cast(&data[0x1C]); + int width = *reinterpret_cast(&data[0x12]); + int height = *reinterpret_cast(&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& 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(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(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(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& image_data = image.Data(); + const std::vector& golden_data = golden.Data(); + + double sum = 0.0; + for (size_t i = 0; i < image_data.size(); ++i) { + const double diff = static_cast(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 +#include +#include + +#include + +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 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 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 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 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 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 vals = {{ 16, 18, 17, 4, 7, 14, 10, 0 }}; + const uint64_t kValEncoding = 0x2b9c83dc; + + BitStream 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 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 vals = {{ 6, 0, 0, 2, 0, 0, 0, 0, 8, 0, 0, 0, 0, 8, 8, 0 }}; + const uint64_t kValEncoding = 0x0004c0100001006ULL; + + BitStream 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 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 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 generated_vals(num_vals); + for (auto& val : generated_vals) { + val = rand(mt) % (range + 1); + } + + // Encode the values using the + BitStream 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 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 +#include + +#include + +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 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 { }; + +// 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 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 +#include + +#include +#include + +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 { }; + +// 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 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 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 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 +#include + +#include +#include +#include +#include + +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 {{ + 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::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(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 + +#include +#include + +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 + +#include +#include +#include + +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 vals = { 4, 6, 4, 6, 7, 5, 7, 5 }; + std::vector 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 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 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 + +#include + +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 weights = InfillWeights( + {{ 1, 3, 5, 3, 5, 7, 5, 7, 9 }}, Footprint::Get5x5(), 3, 3); + + std::vector 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 diff --git a/src/decoder/testdata/atlas_small_4x4.astc b/src/decoder/testdata/atlas_small_4x4.astc new file mode 100644 index 0000000..0dd080b Binary files /dev/null and b/src/decoder/testdata/atlas_small_4x4.astc differ diff --git a/src/decoder/testdata/atlas_small_4x4.bmp b/src/decoder/testdata/atlas_small_4x4.bmp new file mode 100644 index 0000000..32412b3 Binary files /dev/null and b/src/decoder/testdata/atlas_small_4x4.bmp differ diff --git a/src/decoder/testdata/atlas_small_5x5.astc b/src/decoder/testdata/atlas_small_5x5.astc new file mode 100644 index 0000000..2831c90 Binary files /dev/null and b/src/decoder/testdata/atlas_small_5x5.astc differ diff --git a/src/decoder/testdata/atlas_small_5x5.bmp b/src/decoder/testdata/atlas_small_5x5.bmp new file mode 100644 index 0000000..95b0186 Binary files /dev/null and b/src/decoder/testdata/atlas_small_5x5.bmp differ diff --git a/src/decoder/testdata/atlas_small_6x6.astc b/src/decoder/testdata/atlas_small_6x6.astc new file mode 100644 index 0000000..b2cc9c2 Binary files /dev/null and b/src/decoder/testdata/atlas_small_6x6.astc differ diff --git a/src/decoder/testdata/atlas_small_6x6.bmp b/src/decoder/testdata/atlas_small_6x6.bmp new file mode 100644 index 0000000..796812e Binary files /dev/null and b/src/decoder/testdata/atlas_small_6x6.bmp differ diff --git a/src/decoder/testdata/atlas_small_8x8.astc b/src/decoder/testdata/atlas_small_8x8.astc new file mode 100644 index 0000000..a8805f2 Binary files /dev/null and b/src/decoder/testdata/atlas_small_8x8.astc differ diff --git a/src/decoder/testdata/atlas_small_8x8.bmp b/src/decoder/testdata/atlas_small_8x8.bmp new file mode 100644 index 0000000..8f92698 Binary files /dev/null and b/src/decoder/testdata/atlas_small_8x8.bmp differ diff --git a/src/decoder/testdata/checkerboard.astc b/src/decoder/testdata/checkerboard.astc new file mode 100644 index 0000000..79acdca Binary files /dev/null and b/src/decoder/testdata/checkerboard.astc differ diff --git a/src/decoder/testdata/checkered_10.astc b/src/decoder/testdata/checkered_10.astc new file mode 100644 index 0000000..e3b1965 Binary files /dev/null and b/src/decoder/testdata/checkered_10.astc differ diff --git a/src/decoder/testdata/checkered_11.astc b/src/decoder/testdata/checkered_11.astc new file mode 100644 index 0000000..c80c6a4 Binary files /dev/null and b/src/decoder/testdata/checkered_11.astc differ diff --git a/src/decoder/testdata/checkered_12.astc b/src/decoder/testdata/checkered_12.astc new file mode 100644 index 0000000..a82583a Binary files /dev/null and b/src/decoder/testdata/checkered_12.astc differ diff --git a/src/decoder/testdata/checkered_4.astc b/src/decoder/testdata/checkered_4.astc new file mode 100644 index 0000000..ac60716 Binary files /dev/null and b/src/decoder/testdata/checkered_4.astc differ diff --git a/src/decoder/testdata/checkered_5.astc b/src/decoder/testdata/checkered_5.astc new file mode 100644 index 0000000..0389c53 Binary files /dev/null and b/src/decoder/testdata/checkered_5.astc differ diff --git a/src/decoder/testdata/checkered_6.astc b/src/decoder/testdata/checkered_6.astc new file mode 100644 index 0000000..210dd34 Binary files /dev/null and b/src/decoder/testdata/checkered_6.astc differ diff --git a/src/decoder/testdata/checkered_7.astc b/src/decoder/testdata/checkered_7.astc new file mode 100644 index 0000000..8ea10f8 Binary files /dev/null and b/src/decoder/testdata/checkered_7.astc differ diff --git a/src/decoder/testdata/checkered_8.astc b/src/decoder/testdata/checkered_8.astc new file mode 100644 index 0000000..3708882 Binary files /dev/null and b/src/decoder/testdata/checkered_8.astc differ diff --git a/src/decoder/testdata/checkered_9.astc b/src/decoder/testdata/checkered_9.astc new file mode 100644 index 0000000..b5c962a Binary files /dev/null and b/src/decoder/testdata/checkered_9.astc differ diff --git a/src/decoder/testdata/footprint_10x10.astc b/src/decoder/testdata/footprint_10x10.astc new file mode 100644 index 0000000..6799cd0 Binary files /dev/null and b/src/decoder/testdata/footprint_10x10.astc differ diff --git a/src/decoder/testdata/footprint_10x10.bmp b/src/decoder/testdata/footprint_10x10.bmp new file mode 100644 index 0000000..7ded6ad Binary files /dev/null and b/src/decoder/testdata/footprint_10x10.bmp differ diff --git a/src/decoder/testdata/footprint_10x5.astc b/src/decoder/testdata/footprint_10x5.astc new file mode 100644 index 0000000..cbfe4f2 Binary files /dev/null and b/src/decoder/testdata/footprint_10x5.astc differ diff --git a/src/decoder/testdata/footprint_10x5.bmp b/src/decoder/testdata/footprint_10x5.bmp new file mode 100644 index 0000000..68202f8 Binary files /dev/null and b/src/decoder/testdata/footprint_10x5.bmp differ diff --git a/src/decoder/testdata/footprint_10x6.astc b/src/decoder/testdata/footprint_10x6.astc new file mode 100644 index 0000000..53e37b4 Binary files /dev/null and b/src/decoder/testdata/footprint_10x6.astc differ diff --git a/src/decoder/testdata/footprint_10x6.bmp b/src/decoder/testdata/footprint_10x6.bmp new file mode 100644 index 0000000..3dffe72 Binary files /dev/null and b/src/decoder/testdata/footprint_10x6.bmp differ diff --git a/src/decoder/testdata/footprint_10x8.astc b/src/decoder/testdata/footprint_10x8.astc new file mode 100644 index 0000000..b9613e5 Binary files /dev/null and b/src/decoder/testdata/footprint_10x8.astc differ diff --git a/src/decoder/testdata/footprint_10x8.bmp b/src/decoder/testdata/footprint_10x8.bmp new file mode 100644 index 0000000..b7d75f7 Binary files /dev/null and b/src/decoder/testdata/footprint_10x8.bmp differ diff --git a/src/decoder/testdata/footprint_12x10.astc b/src/decoder/testdata/footprint_12x10.astc new file mode 100644 index 0000000..54f5919 Binary files /dev/null and b/src/decoder/testdata/footprint_12x10.astc differ diff --git a/src/decoder/testdata/footprint_12x10.bmp b/src/decoder/testdata/footprint_12x10.bmp new file mode 100644 index 0000000..2007a3d Binary files /dev/null and b/src/decoder/testdata/footprint_12x10.bmp differ diff --git a/src/decoder/testdata/footprint_12x12.astc b/src/decoder/testdata/footprint_12x12.astc new file mode 100644 index 0000000..d94d0a6 Binary files /dev/null and b/src/decoder/testdata/footprint_12x12.astc differ diff --git a/src/decoder/testdata/footprint_12x12.bmp b/src/decoder/testdata/footprint_12x12.bmp new file mode 100644 index 0000000..57e24a2 Binary files /dev/null and b/src/decoder/testdata/footprint_12x12.bmp differ diff --git a/src/decoder/testdata/footprint_4x4.astc b/src/decoder/testdata/footprint_4x4.astc new file mode 100644 index 0000000..fdf3cd6 Binary files /dev/null and b/src/decoder/testdata/footprint_4x4.astc differ diff --git a/src/decoder/testdata/footprint_4x4.bmp b/src/decoder/testdata/footprint_4x4.bmp new file mode 100644 index 0000000..fcb4e07 Binary files /dev/null and b/src/decoder/testdata/footprint_4x4.bmp differ diff --git a/src/decoder/testdata/footprint_5x4.astc b/src/decoder/testdata/footprint_5x4.astc new file mode 100644 index 0000000..0683059 Binary files /dev/null and b/src/decoder/testdata/footprint_5x4.astc differ diff --git a/src/decoder/testdata/footprint_5x4.bmp b/src/decoder/testdata/footprint_5x4.bmp new file mode 100644 index 0000000..9cae5f9 Binary files /dev/null and b/src/decoder/testdata/footprint_5x4.bmp differ diff --git a/src/decoder/testdata/footprint_5x5.astc b/src/decoder/testdata/footprint_5x5.astc new file mode 100644 index 0000000..03c3395 Binary files /dev/null and b/src/decoder/testdata/footprint_5x5.astc differ diff --git a/src/decoder/testdata/footprint_5x5.bmp b/src/decoder/testdata/footprint_5x5.bmp new file mode 100644 index 0000000..c5ddeec Binary files /dev/null and b/src/decoder/testdata/footprint_5x5.bmp differ diff --git a/src/decoder/testdata/footprint_6x5.astc b/src/decoder/testdata/footprint_6x5.astc new file mode 100644 index 0000000..d0d455d Binary files /dev/null and b/src/decoder/testdata/footprint_6x5.astc differ diff --git a/src/decoder/testdata/footprint_6x5.bmp b/src/decoder/testdata/footprint_6x5.bmp new file mode 100644 index 0000000..94a005d Binary files /dev/null and b/src/decoder/testdata/footprint_6x5.bmp differ diff --git a/src/decoder/testdata/footprint_6x6.astc b/src/decoder/testdata/footprint_6x6.astc new file mode 100644 index 0000000..47e038e Binary files /dev/null and b/src/decoder/testdata/footprint_6x6.astc differ diff --git a/src/decoder/testdata/footprint_6x6.bmp b/src/decoder/testdata/footprint_6x6.bmp new file mode 100644 index 0000000..fb1f41c Binary files /dev/null and b/src/decoder/testdata/footprint_6x6.bmp differ diff --git a/src/decoder/testdata/footprint_8x5.astc b/src/decoder/testdata/footprint_8x5.astc new file mode 100644 index 0000000..4ef49d6 Binary files /dev/null and b/src/decoder/testdata/footprint_8x5.astc differ diff --git a/src/decoder/testdata/footprint_8x5.bmp b/src/decoder/testdata/footprint_8x5.bmp new file mode 100644 index 0000000..6d4f609 Binary files /dev/null and b/src/decoder/testdata/footprint_8x5.bmp differ diff --git a/src/decoder/testdata/footprint_8x6.astc b/src/decoder/testdata/footprint_8x6.astc new file mode 100644 index 0000000..1135509 Binary files /dev/null and b/src/decoder/testdata/footprint_8x6.astc differ diff --git a/src/decoder/testdata/footprint_8x6.bmp b/src/decoder/testdata/footprint_8x6.bmp new file mode 100644 index 0000000..35e7121 Binary files /dev/null and b/src/decoder/testdata/footprint_8x6.bmp differ diff --git a/src/decoder/testdata/footprint_8x8.astc b/src/decoder/testdata/footprint_8x8.astc new file mode 100644 index 0000000..bdeb9c9 Binary files /dev/null and b/src/decoder/testdata/footprint_8x8.astc differ diff --git a/src/decoder/testdata/footprint_8x8.bmp b/src/decoder/testdata/footprint_8x8.bmp new file mode 100644 index 0000000..3b3a1c8 Binary files /dev/null and b/src/decoder/testdata/footprint_8x8.bmp differ diff --git a/src/decoder/testdata/rgb_12x12.astc b/src/decoder/testdata/rgb_12x12.astc new file mode 100644 index 0000000..cc0fc3d Binary files /dev/null and b/src/decoder/testdata/rgb_12x12.astc differ diff --git a/src/decoder/testdata/rgb_12x12.bmp b/src/decoder/testdata/rgb_12x12.bmp new file mode 100644 index 0000000..50538de Binary files /dev/null and b/src/decoder/testdata/rgb_12x12.bmp differ diff --git a/src/decoder/testdata/rgb_4x4.astc b/src/decoder/testdata/rgb_4x4.astc new file mode 100644 index 0000000..84527d3 Binary files /dev/null and b/src/decoder/testdata/rgb_4x4.astc differ diff --git a/src/decoder/testdata/rgb_4x4.bmp b/src/decoder/testdata/rgb_4x4.bmp new file mode 100644 index 0000000..fae4f91 Binary files /dev/null and b/src/decoder/testdata/rgb_4x4.bmp differ diff --git a/src/decoder/testdata/rgb_5x4.astc b/src/decoder/testdata/rgb_5x4.astc new file mode 100644 index 0000000..377938d Binary files /dev/null and b/src/decoder/testdata/rgb_5x4.astc differ diff --git a/src/decoder/testdata/rgb_5x4.bmp b/src/decoder/testdata/rgb_5x4.bmp new file mode 100644 index 0000000..a0c1fdb Binary files /dev/null and b/src/decoder/testdata/rgb_5x4.bmp differ diff --git a/src/decoder/testdata/rgb_6x6.astc b/src/decoder/testdata/rgb_6x6.astc new file mode 100644 index 0000000..8a70c0f Binary files /dev/null and b/src/decoder/testdata/rgb_6x6.astc differ diff --git a/src/decoder/testdata/rgb_6x6.bmp b/src/decoder/testdata/rgb_6x6.bmp new file mode 100644 index 0000000..e1a5dcd Binary files /dev/null and b/src/decoder/testdata/rgb_6x6.bmp differ diff --git a/src/decoder/testdata/rgb_8x8.astc b/src/decoder/testdata/rgb_8x8.astc new file mode 100644 index 0000000..1bca381 Binary files /dev/null and b/src/decoder/testdata/rgb_8x8.astc differ diff --git a/src/decoder/testdata/rgb_8x8.bmp b/src/decoder/testdata/rgb_8x8.bmp new file mode 100644 index 0000000..12b6eb2 Binary files /dev/null and b/src/decoder/testdata/rgb_8x8.bmp differ diff --git a/src/decoder/tools/astc_inspector_cli.cc b/src/decoder/tools/astc_inspector_cli.cc new file mode 100644 index 0000000..105f574 --- /dev/null +++ b/src/decoder/tools/astc_inspector_cli.cc @@ -0,0 +1,785 @@ +// 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_inspector_cli collects the various statistics of a stream of ASTC data +// stored in an ASTC file. +// +// Example usage: +// To dump statistics about an ASTC file, use: +// astc_inspector_cli +// +// To dump statistics on a specific block in an ASTC file, use: +// astc_inspector_cli + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "src/base/string_utils.h" +#include "src/decoder/astc_file.h" +#include "src/decoder/endpoint_codec.h" +#include "src/decoder/intermediate_astc_block.h" +#include "src/decoder/partition.h" +#include "src/decoder/quantization.h" +#include "src/decoder/weight_infill.h" + +using astc_codec::ASTCFile; +using astc_codec::ColorEndpointMode; +using astc_codec::IntermediateBlockData; +using astc_codec::PhysicalASTCBlock; +using astc_codec::RgbaColor; +using astc_codec::VoidExtentData; +using astc_codec::base::Optional; + +namespace { + +constexpr int kNumEndpointModes = + static_cast(ColorEndpointMode::kNumColorEndpointModes); +constexpr std::array kModeStrings {{ + "kLDRLumaDirect", "kLDRLumaBaseOffset", "kHDRLumaLargeRange", + "kHDRLumaSmallRange", "kLDRLumaAlphaDirect", "kLDRLumaAlphaBaseOffset", + "kLDRRGBBaseScale", "kHDRRGBBaseScale", "kLDRRGBDirect", + "kLDRRGBBaseOffset", "kLDRRGBBaseScaleTwoA", "kHDRRGBDirect", + "kLDRRGBADirect", "kLDRRGBABaseOffset", "kHDRRGBDirectLDRAlpha", + "kHDRRGBDirectHDRAlpha" }}; + +//////////////////////////////////////////////////////////////////////////////// +// +// A generic stat that should be tracked via an instance of ASTCFileStats. +class Stat { + public: + explicit Stat(const std::vector* blocks, size_t total) + : blocks_(blocks), total_(total) { } + virtual ~Stat() { } + + virtual std::ostream& PrintToStream(std::ostream& out) const = 0; + + protected: + // Utility function to iterate over all of the blocks that are not void-extent + // blocks. FoldFn optionally allows a value to accumulate. It should be of the + // type: + // (const IntermediateBlockData&, T x) -> T + template + T IterateBlocks(T initial, FoldFn f) const { + T result = initial; + for (const auto& block : *blocks_) { + result = f(block, std::move(result)); + } + return result; + } + + size_t NumBlocks() const { return total_; } + + private: + const std::vector* const blocks_; + const size_t total_; +}; + +//////////////////////////////////////////////////////////////////////////////// +// +// Computes the number of void extent blocks. +class VoidExtentCount : public Stat { + public: + VoidExtentCount(const std::vector* blocks, + size_t total, std::string description) + : Stat(blocks, total), description_(std::move(description)), + count_(total - blocks->size()) { } + + std::ostream& PrintToStream(std::ostream& out) const override { + return out << description_ << ": " << count_ + << " (" << (count_ * 100 / NumBlocks()) << "%)" << std::endl; + }; + + private: + const std::string description_; + const size_t count_; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// Computes a per-block stat and reports it as an average over all blocks. +class PerBlockAverage : public Stat { + public: + PerBlockAverage(const std::vector* blocks, + size_t total, std::string description, + const std::function &fn) + : Stat(blocks, total), + description_(std::move(description)) { + int sum = 0; + size_t count = 0; + for (const auto& block : *blocks) { + sum += fn(block); + ++count; + } + average_ = sum / count; + } + + std::ostream& PrintToStream(std::ostream& out) const override { + return out << description_ << ": " << average_ << std::endl; + } + + private: + size_t average_; + std::string description_; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// Computes a per-block true or false value and reports how many blocks return +// true with a percentage of total blocks. +class PerBlockPredicate : public Stat { + public: + PerBlockPredicate(const std::vector* blocks, + size_t total, std::string description, + const std::function &fn) + : Stat(blocks, total), + description_(std::move(description)), + count_(std::count_if(blocks->begin(), blocks->end(), fn)) { } + + std::ostream& PrintToStream(std::ostream& out) const override { + return out << description_ << ": " << count_ + << " (" << (count_ * 100 / NumBlocks()) << "%)" << std::endl; + }; + + private: + const std::string description_; + const size_t count_; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// Returns a histogram of the number of occurrences of each endpoint mode in +// the list of blocks. Note, due to multi-subset blocks, the sum of these +// values will not match the total number of blocks. +class ModeCountsStat : public Stat { + public: + explicit ModeCountsStat(const std::vector* blocks, + size_t total) + : Stat(blocks, total), + mode_counts_(IterateBlocks( + {}, [](const IntermediateBlockData& data, ModeArray&& m) { + auto result = m; + for (const auto& ep : data.endpoints) { + result[static_cast(ep.mode)]++; + } + return result; + })) { } + + std::ostream& PrintToStream(std::ostream& out) const override { + const size_t total_modes_used = + std::accumulate(mode_counts_.begin(), mode_counts_.end(), 0); + + out << "Endpoint modes used: " << std::endl; + for (size_t i = 0; i < kNumEndpointModes; ++i) { + out << " "; + out << std::setw(30) << std::left << std::setfill('.') << kModeStrings[i]; + out << std::setw(8) << std::right << std::setfill('.') << mode_counts_[i]; + + std::stringstream pct; + pct << " (" << (mode_counts_[i] * 100 / total_modes_used) << "%)"; + + out << std::setw(6) << std::right << std::setfill(' ') << pct.str(); + out << std::endl; + } + + return out; + } + + private: + using ModeArray = std::array; + const ModeArray mode_counts_; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// Counts the number of unique endpoints used across all blocks. +class UniqueEndpointsCount : public Stat { + public: + explicit UniqueEndpointsCount( + const std::vector* blocks, size_t total) + : Stat(blocks, total), + unique_endpoints_(IterateBlocks( + UniqueEndpointSet(), + [](const IntermediateBlockData& data, UniqueEndpointSet&& eps) { + UniqueEndpointSet result(eps); + for (const auto& ep : data.endpoints) { + RgbaColor ep_one, ep_two; + DecodeColorsForMode(ep.colors, data.endpoint_range.value(), + ep.mode, &ep_one, &ep_two); + result.insert(PackEndpoint(ep_one)); + result.insert(PackEndpoint(ep_two)); + } + return result; + })) { } + + std::ostream& PrintToStream(std::ostream& out) const override { + out << "Num unique endpoints: " << unique_endpoints_.size() << std::endl; + return out; + } + + private: + static uint32_t PackEndpoint(const RgbaColor& color) { + uint32_t result = 0; + for (const int& c : color) { + constexpr int kSaturatedChannelValue = 0xFF; + assert(c >= 0); + assert(c <= kSaturatedChannelValue); + result <<= 8; + result |= c; + } + return result; + } + + using UniqueEndpointSet = std::unordered_set; + const UniqueEndpointSet unique_endpoints_; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// Computes a histogram of the number of occurrences of 1-4 subset partitions. +class PartitionCountStat : public Stat { + public: + explicit PartitionCountStat(const std::vector* blocks, + size_t total) + : Stat(blocks, total) + , part_counts_(IterateBlocks( + {}, [](const IntermediateBlockData& data, PartCount&& m) { + PartCount result = m; + result[data.endpoints.size() - 1]++; + return result; + })) { } + + std::ostream& PrintToStream(std::ostream& out) const override { + out << "Num partitions used: " << std::endl; + for (size_t i = 0; i < part_counts_.size(); ++i) { + out << " " << i + 1 << ": " << part_counts_[i] << std::endl; + } + return out; + } + + private: + using PartCount = std::array; + const PartCount part_counts_; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// For each block that uses dual-plane mode, computes and stores the dual-plane +// channels in a vector. Outputs the number of each channel used across all +// blocks +class DualChannelStat : public Stat { + private: + static constexpr auto kNumDualPlaneChannels = + std::tuple_size::value; + using CountsArray = std::array; + + public: + explicit DualChannelStat(const std::vector* blocks, + size_t total) + : Stat(blocks, total), + dual_channels_(IterateBlocks( + std::vector(), + [](const IntermediateBlockData& data, std::vector&& input) { + auto result = input; + if (data.dual_plane_channel) { + result.push_back(data.dual_plane_channel.value()); + } + return result; + })) { } + + std::ostream& PrintToStream(std::ostream& out) const override { + // Similar to the number of partitions, the number of dual plane blocks + // can be determined by parsing the next four fields and summing them. + const int num_dual_plane_blocks = dual_channels_.size(); + out << "Number of dual-plane blocks: " << num_dual_plane_blocks + << " (" << (num_dual_plane_blocks * 100) / NumBlocks() << "%)" + << std::endl; + + CountsArray counts = GetCounts(); + assert(counts.size() == kNumDualPlaneChannels); + + for (size_t i = 0; i < counts.size(); ++i) { + out << " " << i << ": " << counts[i] << std::endl; + } + return out; + } + + private: + CountsArray GetCounts() const { + CountsArray counts; + for (size_t i = 0; i < kNumDualPlaneChannels; ++i) { + counts[i] = + std::count_if(dual_channels_.begin(), dual_channels_.end(), + [i](int channel) { return i == channel; }); + } + return counts; + } + + const std::vector dual_channels_; +}; + + +// Stores the intermediate block representations of the blocks associated with +// an ASTCFile. Also provides various facilities for extracting aggregate data +// from these blocks. +class ASTCFileStats { + public: + explicit ASTCFileStats(const std::unique_ptr& astc_file) { + const size_t total = astc_file->NumBlocks(); + + for (size_t block_idx = 0; block_idx < astc_file->NumBlocks(); ++block_idx) { + const PhysicalASTCBlock pb = astc_file->GetBlock(block_idx); + assert(!pb.IsIllegalEncoding()); + if (pb.IsIllegalEncoding()) { + std::cerr << "WARNING: Block " << block_idx << " has illegal encoding." << std::endl; + continue; + } + + if (!pb.IsVoidExtent()) { + Optional block = UnpackIntermediateBlock(pb); + if (!block) { + std::cerr << "WARNING: Block " << block_idx << " failed to unpack." << std::endl; + continue; + } + + blocks_.push_back(block.value()); + } + } + + stats_.emplace_back(new UniqueEndpointsCount(&blocks_, total)); + stats_.emplace_back(new VoidExtentCount( + &blocks_, total, "Num void extent blocks")); + + stats_.emplace_back(new PerBlockAverage( + &blocks_, total, "Average weight range", + [](const IntermediateBlockData& b) { return b.weight_range; })); + + stats_.emplace_back(new PerBlockAverage( + &blocks_, total, "Average number of weights", + [](const IntermediateBlockData& b) { return b.weights.size(); })); + + stats_.emplace_back(new PerBlockPredicate( + &blocks_, total, "Num blocks that use blue contract mode", + [](const IntermediateBlockData& block) { + for (const auto& ep : block.endpoints) { + if (UsesBlueContract( + block.endpoint_range.valueOr(255), ep.mode, ep.colors)) { + return true; + } + } + + return false; + })); + + stats_.emplace_back(new ModeCountsStat(&blocks_, total)); + + stats_.emplace_back(new PerBlockPredicate( + &blocks_, total, "Num multi-part blocks", + [](const IntermediateBlockData& block) { + return block.endpoints.size() > 1; + })); + stats_.emplace_back(new PartitionCountStat(&blocks_, total)); + + stats_.emplace_back(new DualChannelStat(&blocks_, total)); + } + + // Returns a sorted list of pairs of the form (part_id, count) where the + // |part_id| is the partition ID used for 2-subset blocks, and |count| is the + // number of times that particular ID was used. + std::vector> ComputePartIDHistogram() const { + std::vector part_ids(1 << 11, 0); + std::iota(part_ids.begin(), part_ids.end(), 0); + + // The histogram will then pair IDs with counts so that we can sort by + // the number of instances later on. + std::vector> part_id_histogram; + std::transform(part_ids.begin(), part_ids.end(), + std::back_inserter(part_id_histogram), + [](const int& x) { return std::make_pair(x, 0); }); + + // Actually count the IDs in the list of blocks. + for (const auto& block : blocks_) { + if (block.endpoints.size() == 2) { + const int id = block.partition_id.value(); + assert(part_id_histogram[id].first == id); + part_id_histogram[id].second++; + } + } + + struct OrderBySecondGreater { + typedef std::pair PairType; + bool operator()(const PairType& lhs, const PairType& rhs) { + return lhs.second > rhs.second; + } + }; + + // Sort by descending numbers of occurrence for each partition ID + std::sort(part_id_histogram.begin(), part_id_histogram.end(), + OrderBySecondGreater()); + + return part_id_histogram; + } + + // Weights range from 2x2 - 12x12. For simplicity define buckets for every + // pair in [0, 12]^2. + constexpr static int kResolutionBuckets = 13; + // Returns a linear array of buckets over all pairs of grid resolutions, + // x-major in memory. + std::vector ComputeWeightResolutionHistogram() const { + // Allocate one bucket for every grid resolution. + std::vector resolution_histogram( + kResolutionBuckets * kResolutionBuckets, 0); + + // Count the weight resolutions in the list of blocks. + for (const auto& block : blocks_) { + const int dim_x = block.weight_grid_dim_x; + const int dim_y = block.weight_grid_dim_y; + assert(dim_x > 0); + assert(dim_x < kResolutionBuckets); + assert(dim_y > 0); + assert(dim_y < kResolutionBuckets); + ++resolution_histogram[dim_x + dim_y * kResolutionBuckets]; + } + + return resolution_histogram; + } + + // Runs through each defined statistic and prints it out to stdout. Also + // prints a histogram of partition ids used for the given blocks. + void PrintStats() const { + for (const auto& stat : stats_) { + stat->PrintToStream(std::cout); + } + + // We also want to find if there are any 2-subset partition IDs that are + // used disproportionately often. Since partition IDs are 11 bits long, we + // can have as many as (1 << 11) used IDs in a given sequence of blocks. + const auto part_id_histogram = ComputePartIDHistogram(); + const int total_part_ids = std::accumulate( + part_id_histogram.begin(), part_id_histogram.end(), 0, + [](const int& x, const std::pair& hist) { + return x + hist.second; + }); + + if (total_part_ids > 0) { + // Display numbers until we either: + // A. Display the top 90% of used partitions + // B. Reach a point where the remaining partition IDs constitute < 1% of + // the total number of IDs used. + const auto prepare_part_entry = []() -> std::ostream& { + return std::cout << std::setw(6) << std::left << std::setfill('.'); + }; + int part_accum = 0; + std::cout << "Two subset partition ID histogram: " << std::endl; + std::cout << " "; + prepare_part_entry() << "ID" << "Count" << std::endl; + for (const auto& hist : part_id_histogram) { + part_accum += hist.second; + if ((hist.second * 100 / total_part_ids) < 1 || + (100 * (total_part_ids - part_accum)) / total_part_ids < 10) { + const int num_to_display = (total_part_ids - part_accum); + std::cout << " rest: " << num_to_display + << " (" << (num_to_display * 100 / total_part_ids) + << "%)" << std::endl; + break; + } else { + std::cout << " "; + prepare_part_entry() << hist.first << hist.second + << " (" << (hist.second * 100 / total_part_ids) + << "%)" << std::endl; + } + } + } + + // Build the 2D histogram of resolutions. + std::vector weight_histogram = ComputeWeightResolutionHistogram(); + // Labels the weight resolution table. + std::cout << "Weight resolutions:" << std::endl; + const auto prepare_weight_entry = []() -> std::ostream& { + return std::cout << std::setw(6) << std::left << std::setfill(' '); + }; + prepare_weight_entry() << "H W"; + for (int resolution_x = 2; resolution_x < kResolutionBuckets; + ++resolution_x) { + prepare_weight_entry() << resolution_x; + } + std::cout << std::endl; + + // Displays table; skips rows/cols {0, 1} since they will always be empty. + for (int resolution_y = 2; resolution_y < kResolutionBuckets; + ++resolution_y) { + prepare_weight_entry() << resolution_y; + for (int resolution_x = 2; resolution_x < kResolutionBuckets; + ++resolution_x) { + const int count = + weight_histogram[resolution_x + resolution_y * kResolutionBuckets]; + prepare_weight_entry(); + if (!count) { + std::cout << "*"; + } else { + std::cout << count; + } + } + std::cout << std::endl; + } + } + + size_t NumBlocks() const { return blocks_.size(); } + + private: + std::vector> stats_; + std::vector blocks_; +}; + +std::ostream& operator<<(std::ostream& stream, const RgbaColor& color) { + stream << "{"; + constexpr int kNumChannels = std::tuple_size::value; + for (int i = 0; i < kNumChannels; ++i) { + stream << color[i]; + if (i < (kNumChannels - 1)) { + stream << ", "; + } + } + return stream << "}"; +} + +void PrintStatsForBlock(const PhysicalASTCBlock& pb, + astc_codec::Footprint footprint) { + const auto print_void_extent = [&pb](const VoidExtentData& void_extent_data) { + std::cout << "Void extent block:" << std::endl; + std::cout << " 16-bit RGBA: {" + << void_extent_data.r << ", " + << void_extent_data.g << ", " + << void_extent_data.b << ", " + << void_extent_data.a << "}" << std::endl; + if (pb.VoidExtentCoords()) { + std::cout << " Extent (S): {" + << void_extent_data.coords[0] << ", " + << void_extent_data.coords[1] << "}" << std::endl; + std::cout << " Extent (T): {" + << void_extent_data.coords[2] << ", " + << void_extent_data.coords[3] << "}" << std::endl; + } else { + std::cout << " No valid extent data" << std::endl; + } + }; + + const auto print_endpoint_data = + [](ColorEndpointMode mode, int endpoint_range, + const std::vector& encoded_vals) { + std::cout << " Endpoint mode: " + << kModeStrings[static_cast(mode)] << std::endl; + std::cout << " Uses blue-contract mode: " + << (UsesBlueContract(endpoint_range, mode, encoded_vals) + ? "true" : "false") + << std::endl; + + RgbaColor endpoint_low, endpoint_high; + DecodeColorsForMode(encoded_vals, endpoint_range, mode, + &endpoint_low, &endpoint_high); + + std::cout << " Low endpoint: " << endpoint_low << std::endl; + std::cout << " High endpoint: " << endpoint_high << std::endl; + }; + + const auto print_color_data = + [&print_endpoint_data, &footprint](const IntermediateBlockData& ib_data) { + const int endpoint_range = ib_data.endpoint_range.value(); + std::cout << "Endpoint range: " << endpoint_range << std::endl; + + const int num_parts = ib_data.endpoints.size(); + if (ib_data.partition_id.hasValue()) { + const int part_id = ib_data.partition_id.value(); + std::cout << "Parititon ID: " << part_id << std::endl; + + const auto part = GetASTCPartition(footprint, num_parts, part_id); + assert(part.assignment.size() == footprint.Height() * footprint.Width()); + + std::cout << "Assignment:" << std::endl; + for (int y = 0; y < footprint.Height(); ++y) { + std::cout << " "; + for (int x = 0; x < footprint.Width(); ++x) { + const int texel_index = y * footprint.Width() + x; + std::cout << " " << part.assignment[texel_index]; + } + std::cout << std::endl; + } + } else { + std::cout << "Single partition" << std::endl; + } + + int endpoint_index = 0; + for (const auto& ep_data : ib_data.endpoints) { + if (num_parts == 1) { + std::cout << "Endpoints:" << std::endl; + } else { + std::cout << "Endpoint " << (endpoint_index++) << ": " << std::endl; + } + print_endpoint_data(ep_data.mode, endpoint_range, ep_data.colors); + } + + if (ib_data.dual_plane_channel) { + std::cout << "Dual plane channel: " + << ib_data.dual_plane_channel.value() << std::endl; + } else { + std::cout << "Single plane" << std::endl; + } + }; + + const auto print_weight_data = + [&footprint](const IntermediateBlockData& ib_data) { + std::cout << "Weight grid dimensions: " + << ib_data.weight_grid_dim_x << "x" << ib_data.weight_grid_dim_y + << std::endl; + std::cout << "Weight range: " << ib_data.weight_range << std::endl; + + std::cout << "Encoded weight grid: " << std::endl; + int weight_idx = 0; + for (int j = 0; j < ib_data.weight_grid_dim_y; ++j) { + std::cout << " "; + for (int i = 0; i < ib_data.weight_grid_dim_x; ++i) { + std::cout << std::setw(3) << std::left << std::setfill(' ') + << ib_data.weights[weight_idx++]; + } + std::cout << std::endl; + } + + std::cout << "Actual weight grid: " << std::endl; + std::vector actual_weights = ib_data.weights; + for (auto& weight : actual_weights) { + weight = astc_codec::UnquantizeWeightFromRange( + weight, ib_data.weight_range); + } + + actual_weights = astc_codec::InfillWeights( + actual_weights, footprint, ib_data.weight_grid_dim_x, + ib_data.weight_grid_dim_y); + + weight_idx = 0; + for (int j = 0; j < footprint.Height(); ++j) { + std::cout << " "; + for (int i = 0; i < footprint.Width(); ++i) { + std::cout << std::setw(3) << std::left << std::setfill(' ') + << actual_weights[weight_idx++]; + } + std::cout << std::endl; + } + }; + + if (pb.IsVoidExtent()) { + Optional ve = astc_codec::UnpackVoidExtent(pb); + if (!ve) { + std::cerr << "ERROR: Failed to unpack void extent block." << std::endl; + } else { + print_void_extent(ve.value()); + } + } else { + Optional ib = + astc_codec::UnpackIntermediateBlock(pb); + if (!ib) { + std::cerr << "ERROR: Failed to unpack intermediate block." << std::endl; + } else { + const auto& ib_data = ib.value(); + print_color_data(ib_data); + print_weight_data(ib_data); + } + } +} + +} // namespace + +int main(int argc, char* argv[]) { + bool error = false; + + std::string filename; + size_t block_index = 0; + bool has_block_index = false; + + if (argc >= 2) { + filename = argv[1]; + + if (argc == 3) { + int32_t param = astc_codec::base::ParseInt32(argv[2], -1); + if (param < 0) { + std::cerr << "ERROR: Invalid block index." << std::endl; + error = true; + } else { + block_index = static_cast(param); + has_block_index = true; + } + } else if (argc != 2) { + std::cerr << "ERROR: Too many parameters." << std::endl; + error = true; + } + } else { + error = true; + } + + if (error) { + std::cout << ((argc >= 0) ? argv[0] : "astc_inspector_cli") + << " []" << std::endl + << std::endl + << "Collects the various statistics of a stream of ASTC data " + << "stored in an ASTC file." << std::endl + << std::endl + << " filename ASTC file path." << std::endl + << " block index If specified, show detailed information about a block" + << std::endl; + return 1; + } + + std::string error_string; + std::unique_ptr astc_file = ASTCFile::LoadFile(argv[1], &error_string); + if (!astc_file) { + std::cerr << "ERROR: " << error_string << std::endl; + return 2; + } + + if (has_block_index) { + Optional footprint = + astc_codec::Footprint::Parse(astc_file->GetFootprintString().c_str()); + if (!footprint) { + std::cerr << "ERROR: Invalid footprint \"" << astc_file->GetFootprintString() << "\"" << std::endl; + return 3; + } + + PrintStatsForBlock(astc_file->GetBlock(block_index), footprint.value()); + } else { + std::cout << "Dimensions: " << astc_file->GetWidth() << "x" + << astc_file->GetHeight() << ", depth " << astc_file->GetDepth() + << std::endl; + + ASTCFileStats stats(astc_file); + + std::cout << std::endl + << "Total bits used: " << 128 * astc_file->NumBlocks() + << " (" << astc_file->NumBlocks() << " blocks, " + << (astc_file->NumBlocks() * 16) << " bytes)" + << std::endl << std::endl; + + stats.PrintStats(); + } + + return 0; +} diff --git a/src/decoder/types.h b/src/decoder/types.h new file mode 100644 index 0000000..728d5ad --- /dev/null +++ b/src/decoder/types.h @@ -0,0 +1,74 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_ASTC_TYPES_H_ +#define ASTC_CODEC_DECODER_ASTC_TYPES_H_ + +#include +#include +#include + +namespace astc_codec { + +// The color endpoint mode determines how the values encoded in the ASTC block +// are interpreted in order to create the RGBA values for the given endpoint +// pair. The order of this enum is required to match the ASTC specification in +// Section C.2.14. +enum class ColorEndpointMode { + kLDRLumaDirect = 0, + kLDRLumaBaseOffset, + kHDRLumaLargeRange, + kHDRLumaSmallRange, + kLDRLumaAlphaDirect, + kLDRLumaAlphaBaseOffset, + kLDRRGBBaseScale, + kHDRRGBBaseScale, + kLDRRGBDirect, + kLDRRGBBaseOffset, + kLDRRGBBaseScaleTwoA, + kHDRRGBDirect, + kLDRRGBADirect, + kLDRRGBABaseOffset, + kHDRRGBDirectLDRAlpha, + kHDRRGBDirectHDRAlpha, + + // The total number of color endpoints defined by the ASTC specification. + // This isn't a specific endpoint mode and its sole purpose is to be used + // as a constant number. + kNumColorEndpointModes +}; + +// Returns the class for the given mode as defined in Section C.2.11. +constexpr int EndpointModeClass(ColorEndpointMode mode) { + return static_cast(mode) / 4; +} + +// Returns the number of encoded color values for the given endpoint mode. The +// number of encoded color values and their range determines the size of the +// color data in a physical ASTC block. This information is taken from +// Section C.2.17 of the ASTC specification. +constexpr int NumColorValuesForEndpointMode(ColorEndpointMode mode) { + return (EndpointModeClass(mode) + 1) * 2; +} + +// We define a number of convenience types here that give more logical meaning +// throughout the ASTC utilities. +using RgbColor = std::array; +using RgbaColor = std::array; +using Endpoint = RgbaColor; +using EndpointPair = std::pair; + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_ASTC_TYPES_H_ diff --git a/src/decoder/weight_infill.cc b/src/decoder/weight_infill.cc new file mode 100644 index 0000000..62909aa --- /dev/null +++ b/src/decoder/weight_infill.cc @@ -0,0 +1,122 @@ +// 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/integer_sequence_codec.h" + +#include +#include +#include + +namespace astc_codec { + +namespace { + +// The following functions are based on Section C.2.18 of the ASTC specification +int GetScaleFactorD(int block_dim) { + return static_cast((1024.f + static_cast(block_dim >> 1)) / + static_cast(block_dim - 1)); +} + +std::pair GetGridSpaceCoordinates( + Footprint footprint, int s, int t, int weight_dim_x, int weight_dim_y) { + const int ds = GetScaleFactorD(footprint.Width()); + const int dt = GetScaleFactorD(footprint.Height()); + + const int cs = ds * s; + const int ct = dt * t; + + const int gs = (cs * (weight_dim_x - 1) + 32) >> 6; + const int gt = (ct * (weight_dim_y - 1) + 32) >> 6; + + assert(gt < 1 << 8); + assert(gs < 1 << 8); + + return std::make_pair(gs, gt); +} + +// Returns the weight-grid values that are to be used for bilinearly +// interpolating the weight to its final value. If the returned value +// is equal to weight_dim_x * weight_dim_y, it may be ignored. +std::array BilerpGridPointsForWeight( + const std::pair& grid_space_coords, int weight_dim_x) { + const int js = grid_space_coords.first >> 4; + const int jt = grid_space_coords.second >> 4; + + std::array result; + result[0] = js + weight_dim_x * jt; + result[1] = js + weight_dim_x * jt + 1; + result[2] = js + weight_dim_x * (jt + 1); + result[3] = js + weight_dim_x * (jt + 1) + 1; + + return result; +} + +std::array BilerpGridPointFactorsForWeight( + const std::pair& grid_space_coords) { + const int fs = grid_space_coords.first & 0xF; + const int ft = grid_space_coords.second & 0xF; + + std::array result; + result[3] = (fs * ft + 8) >> 4; + result[2] = ft - result[3]; + result[1] = fs - result[3]; + result[0] = 16 - fs - ft + result[3]; + + assert(result[0] <= 16); + assert(result[1] <= 16); + assert(result[2] <= 16); + assert(result[3] <= 16); + + return result; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +int CountBitsForWeights(int weight_dim_x, int weight_dim_y, + int target_weight_range) { + int num_weights = weight_dim_x * weight_dim_y; + return IntegerSequenceCodec:: + GetBitCountForRange(num_weights, target_weight_range); +} + +std::vector InfillWeights(const std::vector& weights, + Footprint footprint, int dim_x, int dim_y) { + std::vector result; + result.reserve(footprint.NumPixels()); + for (int t = 0; t < footprint.Height(); ++t) { + for (int s = 0; s < footprint.Width(); ++s) { + const auto grid_space_coords = + GetGridSpaceCoordinates(footprint, s, t, dim_x, dim_y); + const auto grid_pts = + BilerpGridPointsForWeight(grid_space_coords, dim_x); + const auto grid_factors = + BilerpGridPointFactorsForWeight(grid_space_coords); + + int weight = 0; + for (int i = 0; i < 4; ++i) { + if (grid_pts[i] < dim_x * dim_y) { + weight += weights.at(grid_pts[i]) * grid_factors[i]; + } + } + result.push_back((weight + 8) >> 4); + } + } + + return result; +} + +} // namespace astc_codec diff --git a/src/decoder/weight_infill.h b/src/decoder/weight_infill.h new file mode 100644 index 0000000..4a09d35 --- /dev/null +++ b/src/decoder/weight_infill.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef ASTC_CODEC_DECODER_WEIGHT_INFILL_H_ +#define ASTC_CODEC_DECODER_WEIGHT_INFILL_H_ + +#include "src/decoder/footprint.h" + +#include + +namespace astc_codec { + +// Returns the number of bits used to represent the weight grid at the target +// dimensions and weight range. +int CountBitsForWeights(int weight_dim_x, int weight_dim_y, + int target_weight_range); + +// Performs weight infill of a grid of weights of size |dim_x * dim_y|. The +// weights are fit using the algorithm laid out in Section C.2.18 of the ASTC +// specification. Weights are expected to be passed unquantized and the returned +// grid will be unquantized as well (i.e. each weight within the range [0, 64]). +std::vector InfillWeights(const std::vector& weights, + Footprint footprint, int dim_x, int dim_y); + +} // namespace astc_codec + +#endif // ASTC_CODEC_DECODER_WEIGHT_INFILL_H_ -- cgit v1.2.3