From b63c26a45a71b77652d4485805cb6db884f7d3bc Mon Sep 17 00:00:00 2001 From: drewcassidy Date: Sun, 7 Mar 2021 01:39:51 -0800 Subject: [PATCH] More unit tests and bindings --- python/BC1.cpp | 15 +++++++- python/Module.cpp | 10 +++++ src/BC1/BC1Encoder.cpp | 21 +++++------ src/BC1/BC1Encoder.h | 25 ++++++------ tests/color.py | 77 +++++++++++++++++++++++++++++++++++++ tests/s3tc.py | 86 ++++++++++++++++++++++++++++++++++++++++++ tests/test_rgbcx.py | 45 ++++++++++++++++++---- 7 files changed, 247 insertions(+), 32 deletions(-) create mode 100644 tests/color.py create mode 100644 tests/s3tc.py diff --git a/python/BC1.cpp b/python/BC1.cpp index f42d719..d6b0308 100644 --- a/python/BC1.cpp +++ b/python/BC1.cpp @@ -29,11 +29,24 @@ namespace py = pybind11; namespace rgbcx::bindings { +std::unique_ptr MakeBC1Encoder(Interpolator::Type interpolator, unsigned level, bool use_3color, bool use_3color_black) { + auto interpolator_ptr = (std::shared_ptr)Interpolator::MakeInterpolator(interpolator); + return std::make_unique(interpolator_ptr, level, use_3color, use_3color_black); +} + void InitBC1(py::module_ &m) { auto block_encoder = py::type::of(); py::class_ bc1_encoder(m, "BC1Encoder", block_encoder); - bc1_encoder.def(py::init<>()); + bc1_encoder.def(py::init(&MakeBC1Encoder), py::arg("interpolator") = Interpolator::Type::Ideal, py::arg("level") = 5, py::arg("use_3color") = true, + py::arg("use_3color_black") = true); + bc1_encoder.def("set_level", &BC1Encoder::SetLevel); + bc1_encoder.def_property("flags", &BC1Encoder::GetFlags, &BC1Encoder::SetFlags); + bc1_encoder.def_property("error_mode", &BC1Encoder::GetErrorMode, &BC1Encoder::SetErrorMode); + bc1_encoder.def_property("endpoint_mode", &BC1Encoder::GetEndpointMode, &BC1Encoder::SetEndpointMode); + bc1_encoder.def_property("search_rounds", &BC1Encoder::GetSearchRounds, &BC1Encoder::SetSearchRounds); + bc1_encoder.def_property("orderings_4", &BC1Encoder::GetOrderings4, &BC1Encoder::SetOrderings4); + bc1_encoder.def_property("orderings_3", &BC1Encoder::GetOrderings3, &BC1Encoder::SetOrderings3); using Flags = BC1Encoder::Flags; py::enum_(bc1_encoder, "Flags", py::arithmetic()) diff --git a/python/Module.cpp b/python/Module.cpp index 934550c..4eb16dd 100644 --- a/python/Module.cpp +++ b/python/Module.cpp @@ -19,6 +19,7 @@ #include #include "../src/BlockEncoder.h" +#include "../src/Interpolator.h" #define STRINGIFY(x) #x #define MACRO_STRINGIFY(x) STRINGIFY(x) @@ -30,7 +31,16 @@ void InitBlockEncoder(py::module_ &m); void InitBC1(py::module_ &m); PYBIND11_MODULE(python_rgbcx, m) { + m.doc() = "More Stuff"; + + using IType = Interpolator::Type; + py::enum_(m, "InterpolatorType") + .value("Ideal", IType::Ideal) + .value("IdealRound", IType::IdealRound) + .value("Nvidia", IType::Nvidia) + .value("AMD", IType::AMD); + InitBlockEncoder(m); InitBC1(m); } diff --git a/src/BC1/BC1Encoder.cpp b/src/BC1/BC1Encoder.cpp index ed9be84..cefb8f7 100644 --- a/src/BC1/BC1Encoder.cpp +++ b/src/BC1/BC1Encoder.cpp @@ -41,8 +41,6 @@ namespace rgbcx { using namespace BC1; -using ColorMode = BC1Encoder::ColorMode; - // constructors BC1Encoder::BC1Encoder(InterpolatorPtr interpolator) : _interpolator(interpolator) { @@ -75,7 +73,8 @@ BC1Encoder::BC1Encoder(InterpolatorPtr interpolator, Flags flags, ErrorMode erro SetErrorMode(error_mode); SetEndpointMode(endpoint_mode); SetSearchRounds(search_rounds); - SetOrderings(orderings4, orderings3); + SetOrderings4(orderings4); + SetOrderings3(orderings3); } // Getters and Setters @@ -213,10 +212,8 @@ void BC1Encoder::SetLevel(unsigned level, bool allow_3color, bool allow_3color_b _orderings3 = clamp(_orderings3, 1U, OrderTable<3>::BestOrderCount); } -void BC1Encoder::SetOrderings(unsigned orderings4, unsigned orderings3) { - _orderings4 = clamp(orderings4, 1U, OrderTable<4>::BestOrderCount); - _orderings3 = clamp(orderings3, 1U, OrderTable<3>::BestOrderCount); -} +void BC1Encoder::SetOrderings4(unsigned orderings4) { _orderings4 = clamp(orderings4, 1U, OrderTable<4>::BestOrderCount); } +void BC1Encoder::SetOrderings3(unsigned orderings3) { _orderings3 = clamp(orderings3, 1U, OrderTable<3>::BestOrderCount); } // Public methods void BC1Encoder::EncodeBlock(Color4x4 pixels, BC1Block *dest) const { @@ -643,7 +640,7 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc block.color_mode = ColorMode::Incomplete; } -template void BC1Encoder::FindSelectors(Color4x4 &pixels, EncodeResults &block, ErrorMode error_mode) const { +template void BC1Encoder::FindSelectors(Color4x4 &pixels, EncodeResults &block, ErrorMode error_mode) const { assert(!((error_mode != ErrorMode::Full) && (bool)(M & ColorMode::ThreeColor))); assert(!(bool)(M & ColorMode::Solid)); @@ -746,7 +743,7 @@ template void BC1Encoder::FindSelectors(Color4x4 &pixels, EncodeRe block.color_mode = M; } -template bool BC1Encoder::RefineEndpointsLS(Color4x4 pixels, EncodeResults &block, BlockMetrics metrics) const { +template bool BC1Encoder::RefineEndpointsLS(Color4x4 pixels, EncodeResults &block, BlockMetrics metrics) const { const int color_count = (unsigned)M & 0x0F; static_assert(color_count == 3 || color_count == 4); static_assert(!(bool)(M & ColorMode::Solid)); @@ -793,7 +790,7 @@ template bool BC1Encoder::RefineEndpointsLS(Color4x4 pixels, Encod return true; } -template void BC1Encoder::RefineEndpointsLS(std::array &sums, EncodeResults &block, Vector4 &matrix, Hash hash) const { +template void BC1Encoder::RefineEndpointsLS(std::array &sums, EncodeResults &block, Vector4 &matrix, Hash hash) const { const int color_count = (unsigned)M & 0x0F; static_assert(color_count == 3 || color_count == 4); static_assert(!(bool)(M & ColorMode::Solid)); @@ -819,7 +816,7 @@ template void BC1Encoder::RefineEndpointsLS(std::array +template void BC1Encoder::RefineBlockLS(Color4x4 &pixels, EncodeResults &block, BlockMetrics &metrics, ErrorMode error_mode, unsigned passes) const { assert(error_mode != ErrorMode::None || passes == 1); @@ -844,7 +841,7 @@ void BC1Encoder::RefineBlockLS(Color4x4 &pixels, EncodeResults &block, BlockMetr } } -template +template void BC1Encoder::RefineBlockCF(Color4x4 &pixels, EncodeResults &block, BlockMetrics &metrics, ErrorMode error_mode, unsigned orderings) const { const int color_count = (unsigned)M & 0x0F; static_assert(color_count == 3 || color_count == 4); diff --git a/src/BC1/BC1Encoder.h b/src/BC1/BC1Encoder.h index 0f8802c..4edc504 100644 --- a/src/BC1/BC1Encoder.h +++ b/src/BC1/BC1Encoder.h @@ -76,17 +76,6 @@ class BC1Encoder final : public BlockEncoderTemplate { TryAllInitialEndpoints = 128, }; - enum class ColorMode { - Incomplete = 0x00, - ThreeColor = 0x03, - FourColor = 0x04, - UseBlack = 0x10, - Solid = 0x20, - ThreeColorBlack = ThreeColor | UseBlack, - ThreeColorSolid = ThreeColor | Solid, - FourColorSolid = FourColor | Solid, - }; - enum class ErrorMode { // Perform no error checking at all. None, @@ -144,7 +133,8 @@ class BC1Encoder final : public BlockEncoderTemplate { unsigned int GetOrderings4() const { return _orderings4; } unsigned int GetOrderings3() const { return _orderings3; } - void SetOrderings(unsigned orderings4, unsigned orderings3); + void SetOrderings4(unsigned orderings4); + void SetOrderings3(unsigned orderings3); void EncodeBlock(Color4x4 pixels, BC1Block *dest) const override; @@ -152,6 +142,17 @@ class BC1Encoder final : public BlockEncoderTemplate { using Hash = uint16_t; using BlockMetrics = Color4x4::BlockMetrics; + enum class ColorMode { + Incomplete = 0x00, + ThreeColor = 0x03, + FourColor = 0x04, + UseBlack = 0x10, + Solid = 0x20, + ThreeColorBlack = ThreeColor | UseBlack, + ThreeColorSolid = ThreeColor | Solid, + FourColorSolid = FourColor | Solid, + }; + // Unpacked BC1 block with metadata struct EncodeResults { Color low; diff --git a/tests/color.py b/tests/color.py new file mode 100644 index 0000000..04b6ae7 --- /dev/null +++ b/tests/color.py @@ -0,0 +1,77 @@ +class Color: + def __init__(self, r=0, g=0, b=0, a=1): + self.r = r + self.g = g + self.b = b + self.a = a + + def __add__(self, a): + return Color(self.r + a.r, self.g + a.g, self.b + a.b, self.a + a.a) + + def __mul__(self, c): + return Color(self.r * c, self.g * c, self.b * c, self.a * c) + + def __rmul__(self, c): + return Color(self.r * c, self.g * c, self.b * c, self.a * c) + + def __iter__(self): + return iter([self.r, self.g, self.b, self.a]) + + def __repr__(self): + return f'r: {self.r} g: {self.g} b: {self.b} a: {self.a}' + + def __str__(self): + return self.to_hex() + + @classmethod + def from_565(cls, int_value): + r = float((int_value & 0xF800) >> 11) / 0x1F + g = float((int_value & 0x07E0) >> 5) / 0x3F + b = float(int_value & 0x001F) / 0x1F + + return cls(r, g, b) + + def to_565(self): + r = int(self.r * 0x1F) + g = int(self.g * 0x3F) + b = int(self.b * 0x1F) + + return (r << 11) | (g << 5) | b + + @classmethod + def from_rgb24(cls, int_value): + r = float((int_value & 0xFF0000) >> 16) / 0xFF + g = float((int_value & 0x00FF00) >> 8) / 0xFF + b = float(int_value & 0x0000FF) / 0xFF + + return cls(r, g, b) + + def to_rgb24(self): + r = int(self.r * 0xFF) + g = int(self.g * 0xFF) + b = int(self.b * 0xFF) + + return (r << 16) | (g << 8) | b + + @classmethod + def from_rgba32(cls, int_value): + r = float((int_value & 0xFF000000) >> 24) / 0xFF + g = float((int_value & 0x00FF0000) >> 16) / 0xFF + b = float((int_value & 0x0000FF00) >> 8) / 0xFF + a = float(int_value & 0x000000FF) / 0xFF + + return cls(r, g, b, a) + + def to_rgba32(self): + r = int(self.r * 0xFF) + g = int(self.g * 0xFF) + b = int(self.b * 0xFF) + a = int(self.a * 0xFF) + + return (r << 24) | (g << 16) | (b << 8) | a + + def to_hex(self): + if self.a < 1: + return hex(self.to_rgba32()) + else: + return hex(self.to_rgb24()) diff --git a/tests/s3tc.py b/tests/s3tc.py new file mode 100644 index 0000000..93379f2 --- /dev/null +++ b/tests/s3tc.py @@ -0,0 +1,86 @@ +import struct +import math +import operator +from functools import reduce +from color import Color + + +def bit_slice(value, size, count): + mask = (2 ** size) - 1 + return [(value >> offset) & mask for offset in range(0, size * count, size)] + + +def bit_merge(values, size): + offsets = range(0, len(values) * size, size) + return reduce(operator.__or__, map(operator.lshift, values, offsets)) + + +def triple_slice(triplet): + values = bit_slice(bit_merge(triplet, 8), 3, 8) + return [values[0:4], values[4:8]] + + +def triple_merge(rows): + values = rows[0] + rows[1] + return bit_slice(bit_merge(values, 3), 8, 3) + + +class BC1Block: + size = 8 + + def __init__(self): + self.color0 = Color() + self.color1 = Color() + self.selectors = [[0] * 4] * 4 + + def __repr__(self): + return repr(self.__dict__) + + def __str__(self): + return f'color0: {str(self.color0)} color1: {str(self.color1)}, indices:{self.selectors}' + + @staticmethod + def from_bytes(data): + block = struct.unpack_from('<2H4B', data) + result = BC1Block() + + result.color0 = Color.from_565(block[0]) + result.color1 = Color.from_565(block[1]) + result.selectors = [bit_slice(row, 2, 4) for row in block[2:6]] + return result + + def to_bytes(self): + return struct.pack('<2H4B', + self.color0.to_565(), self.color1.to_565(), + *(bit_merge(row, 2) for row in self.selectors)) + + def is_3color(self): + return self.color0.to_565() <= self.color1.to_565() + + +class BC4Block: + size = 8 + + def __init__(self): + self.alpha0 = 1 + self.alpha1 = 1 + self.selectors = [[0] * 4] * 4 + + def __repr__(self): + return repr(self.__dict__) + + @staticmethod + def from_bytes(data): + block = struct.unpack_from('<2B6B', data) + result = BC4Block() + + result.alpha0 = block[0] / 0xFF + result.alpha1 = block[1] / 0xFF + result.selectors = triple_slice(block[2:5]) + triple_slice(block[5:8]) + return result + + def to_bytes(self): + return struct.pack('<2B6B', + int(self.alpha0 * 0xFF), int(self.alpha1 * 0xFF), + *triple_merge(self.selectors[0:2]), + *triple_merge(self.selectors[2:4])) diff --git a/tests/test_rgbcx.py b/tests/test_rgbcx.py index ee3dbde..6348442 100644 --- a/tests/test_rgbcx.py +++ b/tests/test_rgbcx.py @@ -1,20 +1,51 @@ import unittest import python_rgbcx +import color +import s3tc -class MyTestCase(unittest.TestCase): +class TestBC1Encoder(unittest.TestCase): def setUp(self): - self.bc1_encoder = python_rgbcx.BC1Encoder() - self.image = b'\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x55\x55\x55\xFF\xAA\xAA\xAA\xFF' \ - b'\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x55\x55\x55\xFF\xAA\xAA\xAA\xFF' \ - b'\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x55\x55\x55\xFF\xAA\xAA\xAA\xFF' \ - b'\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x55\x55\x55\xFF\xAA\xAA\xAA\xFF' + self.bc1_encoder = python_rgbcx.BC1Encoder(python_rgbcx.InterpolatorType.Ideal, 5) + self.bc1_encoder_no3color = python_rgbcx.BC1Encoder(python_rgbcx.InterpolatorType.Ideal, 5, False, False) + self.bc1_encoder_noblack = python_rgbcx.BC1Encoder(python_rgbcx.InterpolatorType.Ideal, 5, True, False) + + # A block that should always encode greyscale + self.greyscale = b'\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x55\x55\x55\xFF\xAA\xAA\xAA\xFF' \ + b'\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x55\x55\x55\xFF\xAA\xAA\xAA\xFF' \ + b'\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x55\x55\x55\xFF\xAA\xAA\xAA\xFF' \ + b'\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x55\x55\x55\xFF\xAA\xAA\xAA\xFF' + + # A block that should always encode 3-color with black when available + self.chroma_black = b'\x00\x00\x00\xFF\xFF\x00\x00\xFF\x88\x88\x00\xFF\x00\xFF\x00\xFF' \ + b'\x00\x00\x00\xFF\xFF\x00\x00\xFF\x88\x88\x00\xFF\x00\xFF\x00\xFF' \ + b'\x00\x00\x00\xFF\xFF\x00\x00\xFF\x88\x88\x00\xFF\x00\xFF\x00\xFF' \ + b'\x00\x00\x00\xFF\xFF\x00\x00\xFF\x88\x88\x00\xFF\x00\xFF\x00\xFF' def test_block_size(self): - out = self.bc1_encoder.encode_image(self.image, 4, 4) + out = self.bc1_encoder.encode_image(self.greyscale, 4, 4) + + self.assertEqual(self.bc1_encoder.block_width, 4, 'incorrect reported block width') + self.assertEqual(self.bc1_encoder.block_height, 4, 'incorrect reported block height') self.assertEqual(self.bc1_encoder.block_size, 8, 'incorrect reported block size') self.assertEqual(len(out), 8, 'incorrect returned block size') + def test_block_3color(self): + out = s3tc.BC1Block.from_bytes(self.bc1_encoder.encode_image(self.chroma_black, 4, 4)) + out_no_3color = s3tc.BC1Block.from_bytes(self.bc1_encoder_no3color.encode_image(self.chroma_black, 4, 4)) + + self.assertTrue(out.is_3color(), "incorrect color mode with use_3color enabled") + self.assertFalse(out_no_3color.is_3color(), "incorrect color mode with use_3color disabled") + + def test_block_black(self): + out = s3tc.BC1Block.from_bytes(self.bc1_encoder.encode_image(self.chroma_black, 4, 4)) + out_no_black = s3tc.BC1Block.from_bytes(self.bc1_encoder_noblack.encode_image(self.chroma_black, 4, 4)) + + self.assertTrue(any(3 in row for row in out.selectors), "use_3color_black enabled but not used") + self.assertFalse(out_no_black.is_3color() + and any(3 in row for row in out_no_black.selectors), + "use_3color_black disabled but 3 color block has black selectors") + if __name__ == '__main__': unittest.main()