Merge branch 'feature-cli'

This commit is contained in:
Andrew Cassidy 2021-04-07 17:28:36 -07:00
commit 6578729e1c
68 changed files with 2577 additions and 1229 deletions

8
.gitignore vendored
View File

@ -4,15 +4,21 @@ dist/
build/
*.egg-info
*.pyc
*.pyi
#sphinx
docs/_build/
#mypy
out
# IDEs
**/.idea
# cmake
# binaries
*.so
# cmake
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles

View File

@ -49,11 +49,12 @@ add_executable(test_quicktex
target_link_libraries(test_quicktex PRIVATE pybind11::embed)
target_compile_definitions(test_quicktex PRIVATE -DCUSTOM_SYS_PATH="${CMAKE_HOME_DIRECTORY}/env/lib/python3.8/site-packages")
target_compile_definitions(test_quicktex PRIVATE -DCUSTOM_SYS_PATH="${CMAKE_HOME_DIRECTORY}/env/lib/python3.9/site-packages")
# enable openMP if available
if (OpenMP_CXX_FOUND)
target_link_libraries(_quicktex PUBLIC OpenMP::OpenMP_CXX)
target_link_libraries(test_quicktex PUBLIC OpenMP::OpenMP_CXX)
endif ()
# Set module features, like C/C++ standards

View File

@ -68,4 +68,7 @@ autodoc_default_options = {
# This config value contains the locations and names of other projects that
# should be linked to in this documentation.
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'PIL': ('https://pillow.readthedocs.io/en/stable/', None)
}

6
docs/reference/dds.rst Normal file
View File

@ -0,0 +1,6 @@
dds module
==========
.. automodule:: quicktex.dds
:members:

View File

@ -0,0 +1,5 @@
image_utils module
==================
.. automodule:: quicktex.image_utils
:members:

View File

@ -5,4 +5,6 @@ Reference
:maxdepth: 2
conversion.rst
dds.rst
image_utils.rst
formats/index.rst

View File

@ -1,90 +0,0 @@
/* Python-rgbcx Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich 2020 <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <cmath>
#include <cstdint>
#include <memory>
#include <span>
#include <vector>
#include "BlockView.h"
#include "ndebug.h"
#include "util.h"
namespace quicktex {
class BlockDecoder {
public:
using DecoderPtr = std::shared_ptr<BlockDecoder>;
virtual ~BlockDecoder() = default;
virtual void DecodeImage(uint8_t *encoded, Color *decoded, unsigned image_width, unsigned image_height) const = 0;
virtual size_t BlockSize() const = 0;
virtual size_t BlockWidth() const = 0;
virtual size_t BlockHeight() const = 0;
};
template <class B, size_t M, size_t N>
class BlockDecoderTemplate : public BlockDecoder {
public:
using DecodedBlock = ColorBlockView<M, N>;
using EncodedBlock = B;
BlockDecoderTemplate() noexcept = default;
virtual ~BlockDecoderTemplate() noexcept = default;
virtual void DecodeBlock(DecodedBlock dest, EncodedBlock *const block) const noexcept(ndebug) = 0;
virtual void DecodeImage(uint8_t *encoded, Color *decoded, unsigned image_width, unsigned image_height) const override {
assert(image_width % N == 0);
assert(image_width % M == 0);
unsigned block_width = image_width / N;
unsigned block_height = image_height / M;
auto blocks = reinterpret_cast<B *>(encoded);
// from experimentation, multithreading this using OpenMP actually makes decoding slower
// due to thread creation/teardown taking longer than the decoding process itself.
// As a result, this is left as a serial operation despite being embarassingly parallelizable
for (unsigned y = 0; y < block_height; y++) {
for (unsigned x = 0; x < block_width; x++) {
unsigned pixel_x = x * N;
unsigned pixel_y = y * M;
assert(pixel_x >= 0);
assert(pixel_y >= 0);
assert(pixel_y + M <= image_height);
assert(pixel_x + N <= image_width);
unsigned top_left = pixel_x + (pixel_y * image_width);
auto dest = DecodedBlock(&decoded[top_left], (int)image_width);
DecodeBlock(dest, &blocks[x + block_width * y]);
}
}
}
virtual size_t BlockSize() const override { return sizeof(B); }
virtual size_t BlockWidth() const override { return N; }
virtual size_t BlockHeight() const override { return M; }
};
} // namespace quicktex

View File

@ -1,92 +0,0 @@
/* Python-rgbcx Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich 2020 <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <climits>
#include <cstdint>
#include <memory>
#include <string>
#include "BlockView.h"
namespace quicktex {
class BlockEncoder {
public:
using EncoderPtr = std::shared_ptr<BlockEncoder>;
virtual ~BlockEncoder() = default;
virtual void EncodeImage(uint8_t *encoded, Color *decoded, unsigned image_width, unsigned image_height) const = 0;
virtual size_t BlockSize() const = 0;
virtual size_t BlockWidth() const = 0;
virtual size_t BlockHeight() const = 0;
};
template <class B, size_t M, size_t N> class BlockEncoderTemplate : public BlockEncoder {
public:
using DecodedBlock = ColorBlockView<M, N>;
using EncodedBlock = B;
BlockEncoderTemplate() noexcept = default;
virtual ~BlockEncoderTemplate() noexcept = default;
virtual void EncodeBlock(DecodedBlock pixels, EncodedBlock *dest) const = 0;
virtual void EncodeImage(uint8_t *encoded, Color *decoded, unsigned image_width, unsigned image_height) const override {
assert(image_width % N == 0);
assert(image_width % M == 0);
unsigned block_width = image_width / N;
unsigned block_height = image_height / M;
auto blocks = reinterpret_cast<B *>(encoded);
// from experimentation, multithreading this using OpenMP sometimes actually makes decoding slower
// due to thread creation/teardown taking longer than the decoding process itself.
// As a result, this is sometimes left as a serial operation despite being embarassingly parallelizable
// threshold for number of blocks before multithreading is set by overriding MTThreshold()
#pragma omp parallel for if (block_width * block_height >= MTThreshold())
for (int y = 0; y < (int)block_height; y++) {
for (int x = 0; x < (int)block_width; x++) {
unsigned pixel_x = (unsigned)x * N;
unsigned pixel_y = (unsigned)y * M;
assert(pixel_x >= 0);
assert(pixel_y >= 0);
assert(pixel_y + M <= image_height);
assert(pixel_x + N <= image_width);
unsigned top_left = pixel_x + (pixel_y * image_width);
unsigned block_index = (unsigned)x + (block_width * (unsigned)y);
auto src = DecodedBlock(&decoded[top_left], (int)image_width);
EncodeBlock(src, &blocks[block_index]);
}
}
}
virtual size_t BlockSize() const override { return sizeof(B); }
virtual size_t BlockWidth() const override { return N; }
virtual size_t BlockHeight() const override { return M; }
virtual size_t MTThreshold() const { return SIZE_MAX; };
};
} // namespace quicktex

View File

@ -1,170 +0,0 @@
/* Python-rgbcx Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <algorithm>
#include <array>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <vector>
#include "Color.h"
#include "Vector4Int.h"
#include "ndebug.h"
namespace quicktex {
template <typename S, size_t N> class RowView {
public:
RowView(S *start, int pixel_stride = 1) : start(start), pixel_stride(pixel_stride) {}
constexpr S &operator[](size_t index) noexcept(ndebug) {
assert(index < N);
return start[index * pixel_stride];
}
constexpr const S &operator[](size_t index) const noexcept(ndebug) {
assert(index < N);
return start[index * pixel_stride];
}
constexpr int Size() noexcept { return N; }
S *const start;
const int pixel_stride;
};
template <typename S, size_t M, size_t N> class BlockView {
public:
using Row = RowView<S, N>;
BlockView(S *start, int row_stride = N, int pixel_stride = 1) : start(start), row_stride(row_stride), pixel_stride(pixel_stride) {}
constexpr Row operator[](unsigned index) noexcept(ndebug) {
assert(index < M);
return RowView<S, N>(&start[row_stride * (int)index], pixel_stride);
}
constexpr int Width() noexcept { return N; }
constexpr int Height() noexcept { return M; }
constexpr int Size() noexcept { return N * M; }
constexpr S &Get(unsigned x, unsigned y) noexcept(ndebug) {
assert(x < N);
assert(y < M);
return start[(row_stride * (int)y) + (pixel_stride * (int)x)];
}
constexpr S Get(unsigned x, unsigned y) const noexcept(ndebug) {
assert(x < N);
assert(y < M);
return start[(row_stride * (int)y) + (pixel_stride * (int)x)];
}
constexpr void Set(unsigned x, unsigned y, S value) noexcept(ndebug) {
assert(x < N);
assert(y < M);
start[(row_stride * (int)y) + (pixel_stride * (int)x)] = value;
}
constexpr S &Get(unsigned i) noexcept(ndebug) { return Get(i % N, i / N); }
constexpr S Get(unsigned i) const noexcept(ndebug) { return Get(i % N, i / N); }
constexpr std::array<S, M * N> Flatten() noexcept {
std::array<S, M * N> result;
for (unsigned x = 0; x < N; x++) {
for (unsigned y = 0; y < M; y++) { result[x + (N * y)] = Get(x, y); }
}
return result;
}
S *const start;
const int row_stride;
const int pixel_stride;
};
template <size_t M, size_t N> class ColorBlockView : public BlockView<Color, M, N> {
public:
using Base = BlockView<Color, M, N>;
using ChannelView = BlockView<uint8_t, M, N>;
struct BlockMetrics {
Color min;
Color max;
Color avg;
bool is_greyscale;
bool has_black;
Vector4Int sums;
};
ColorBlockView(Color *start, int row_stride = N, int pixel_stride = 1) : Base(start, row_stride, pixel_stride) {}
constexpr ChannelView GetChannel(uint8_t index) noexcept(ndebug) {
assert(index < 4U);
auto channelStart = reinterpret_cast<uint8_t *>(Base::start) + index;
return ChannelView(channelStart, Base::row_stride * 4, Base::pixel_stride * 4);
}
void SetRGB(unsigned x, unsigned y, Color value) noexcept(ndebug) { Base::Get(x, y).SetRGB(value); }
bool IsSingleColor() {
auto first = Base::Get(0, 0);
for (unsigned j = 1; j < M * N; j++) {
if (Base::Get(j) != first) return false;
}
return true;
}
BlockMetrics GetMetrics(bool ignore_black = false) {
BlockMetrics metrics;
metrics.min = Color(UINT8_MAX, UINT8_MAX, UINT8_MAX);
metrics.max = Color(0, 0, 0);
metrics.has_black = false;
metrics.is_greyscale = true;
metrics.sums = {0, 0, 0};
unsigned total = 0;
for (unsigned i = 0; i < M * N; i++) {
Color val = Base::Get(i);
bool is_black = val.IsBlack();
metrics.has_black |= is_black;
if (ignore_black && is_black) { continue; }
metrics.is_greyscale &= val.IsGrayscale();
for (unsigned c = 0; c < 3; c++) {
metrics.min[c] = std::min(metrics.min[c], val[c]);
metrics.max[c] = std::max(metrics.max[c], val[c]);
metrics.sums[c] += val[c];
}
total++;
}
if (total > 0) metrics.avg = (metrics.sums + Vector4Int(total / 2)) / (int)total; // half-total added for better rounding
return metrics;
}
};
using Color4x4 = ColorBlockView<4, 4>;
using Byte4x4 = BlockView<uint8_t, 4, 4>;
} // namespace quicktex

View File

@ -18,7 +18,8 @@
*/
#include "Color.h"
#include <algorithm> // for max, Min
#include <algorithm>
#include <stdexcept>
#include "Vector4.h"
#include "Vector4Int.h"
@ -26,11 +27,17 @@
namespace quicktex {
Color::Color() { SetRGBA(0, 0, 0, 0xFF); }
Color::Color(Vector4Int v) {
if (v.MaxAbs() > 0xFF) throw std::invalid_argument("Vector members out of range");
for (int i = 0; i < 4; i++) {
if (v[i] < 0) throw std::range_error("Color members cannot be negative");
}
Color::Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { SetRGBA(r, g, b, a); }
Color::Color(Vector4Int v) { SetRGBA((uint8_t)v[0], (uint8_t)v[1], (uint8_t)v[2], (uint8_t)v[3]); }
r = static_cast<uint8_t>(v[0]);
g = static_cast<uint8_t>(v[1]);
b = static_cast<uint8_t>(v[2]);
a = static_cast<uint8_t>(v[3]);
}
uint16_t Color::Pack565Unscaled(uint8_t r, uint8_t g, uint8_t b) {
assert5bit(r);
@ -79,13 +86,6 @@ Color Color::PreciseRound565(Vector4 &v) {
return Color(r, g, b);
}
void Color::SetRGBA(uint8_t vr, uint8_t vg, uint8_t vb, uint8_t va = 0xFF) {
r = vr;
g = vg;
b = vb;
a = va;
}
void Color::SetRGB(uint8_t vr, uint8_t vg, uint8_t vb) {
r = vr;
g = vg;

View File

@ -34,9 +34,9 @@ class Color {
uint8_t b;
uint8_t a;
Color();
constexpr Color() : Color(0, 0, 0, 0xFF) {}
Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF);
constexpr Color(uint8_t vr, uint8_t vg, uint8_t vb, uint8_t va = 0xFF) : r(vr), g(vg), b(vb), a(va) {}
Color(Vector4Int v);
@ -66,9 +66,6 @@ class Color {
operator Vector4Int() const;
friend Vector4Int operator-(const Color &lhs, const Color &rhs);
void SetRGBA(uint8_t vr, uint8_t vg, uint8_t vb, uint8_t va);
void SetRGBA(const Color &other) { SetRGBA(other.r, other.g, other.b, other.a); }
void SetRGB(uint8_t vr, uint8_t vg, uint8_t vb);
void SetRGB(const Color &other) { SetRGB(other.r, other.g, other.b); }

124
quicktex/ColorBlock.h Normal file
View File

@ -0,0 +1,124 @@
/* Python-rgbcx Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <algorithm>
#include <array>
#include <climits>
#include <cstring>
#include <stdexcept>
#include "Color.h"
#include "Vector4Int.h"
namespace quicktex {
using Coords = std::tuple<int, int>;
template <int N, int M> class ColorBlock {
public:
struct Metrics {
Color min;
Color max;
Color avg;
bool is_greyscale;
bool has_black;
Vector4Int sums;
};
static constexpr int Width = N;
static constexpr int Height = M;
constexpr Color Get(int x, int y) const {
if (x >= Width || x < 0) throw std::invalid_argument("x value out of range");
if (y >= Height || y < 0) throw std::invalid_argument("y value out of range");
return _pixels[x + (N * y)];
}
constexpr Color Get(int i) const {
if (i >= N * M || i < 0) throw std::invalid_argument("i value out of range");
return _pixels[i];
}
void Set(int x, int y, const Color &value) {
if (x >= Width || x < 0) throw std::invalid_argument("x value out of range");
if (y >= Height || y < 0) throw std::invalid_argument("y value out of range");
_pixels[x + (N * y)] = value;
}
void Set(int i, const Color &value) {
if (i >= N * M || i < 0) throw std::invalid_argument("i value out of range");
_pixels[i] = value;
}
void GetRow(int y, Color *dst) const {
if (y >= Height || y < 0) throw std::invalid_argument("y value out of range");
std::memcpy(dst, &_pixels[N * y], N * sizeof(Color));
}
void SetRow(int y, const Color *src) {
if (y >= Height || y < 0) throw std::invalid_argument("y value out of range");
std::memcpy(&_pixels[N * y], src, N * sizeof(Color));
}
bool IsSingleColor() const {
auto first = Get(0, 0);
for (unsigned j = 1; j < M * N; j++) {
if (Get(j) != first) return false;
}
return true;
}
Metrics GetMetrics(bool ignore_black = false) const {
Metrics metrics;
metrics.min = Color(UINT8_MAX, UINT8_MAX, UINT8_MAX);
metrics.max = Color(0, 0, 0);
metrics.has_black = false;
metrics.is_greyscale = true;
metrics.sums = {0, 0, 0};
unsigned total = 0;
for (unsigned i = 0; i < M * N; i++) {
Color val = Get(i);
bool is_black = val.IsBlack();
metrics.has_black |= is_black;
if (ignore_black && is_black) { continue; }
metrics.is_greyscale &= val.IsGrayscale();
for (unsigned c = 0; c < 3; c++) {
metrics.min[c] = std::min(metrics.min[c], val[c]);
metrics.max[c] = std::max(metrics.max[c], val[c]);
metrics.sums[c] += val[c];
}
total++;
}
if (total > 0) metrics.avg = (metrics.sums + Vector4Int(total / 2)) / (int)total; // half-total added for better rounding
return metrics;
}
private:
std::array<Color, N * M> _pixels;
};
} // namespace quicktex

68
quicktex/Decoder.h Normal file
View File

@ -0,0 +1,68 @@
/* Python-rgbcx Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich 2020 <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include "ColorBlock.h"
#include "Texture.h"
namespace quicktex {
template <class T> class Decoder {
public:
using Texture = T;
virtual ~Decoder() = default;
virtual RawTexture Decode(const T &encoded) const = 0;
};
template <class T> class BlockDecoder : public Decoder<T> {
public:
inline static constexpr int BlockWidth = T::BlockType::Width;
inline static constexpr int BlockHeight = T::BlockType::Height;
using Texture = T;
using EncodedBlock = typename T::BlockType;
using DecodedBlock = ColorBlock<BlockWidth, BlockHeight>;
virtual DecodedBlock DecodeBlock(const EncodedBlock &block) const = 0;
virtual RawTexture Decode(const T &encoded) const override {
auto decoded = RawTexture(encoded.Width(), encoded.Height());
int blocks_x = encoded.BlocksX();
int blocks_y = encoded.BlocksY();
// from experimentation, multithreading this using OpenMP actually makes decoding slower
// due to thread creation/teardown taking longer than the decoding process itself.
// As a result, this is left as a serial operation despite being embarassingly parallelizable
for (int y = 0; y < blocks_y; y++) {
for (int x = 0; x < blocks_x; x++) {
auto block = encoded.GetBlock(x, y);
auto pixels = DecodeBlock(block);
decoded.SetBlock<BlockWidth, BlockHeight>(x, y, pixels);
}
}
return decoded;
}
};
} // namespace quicktex

72
quicktex/Encoder.h Normal file
View File

@ -0,0 +1,72 @@
/* Python-rgbcx Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich 2020 <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <memory>
#include "ColorBlock.h"
#include "Texture.h"
namespace quicktex {
template <typename T> class Encoder {
public:
using Texture = T;
virtual ~Encoder() = default;
virtual T Encode(const RawTexture &decoded) const = 0;
};
template <typename T> class BlockEncoder : public Encoder<T> {
public:
inline static constexpr int BlockWidth = T::BlockType::Width;
inline static constexpr int BlockHeight = T::BlockType::Height;
using Texture = T;
using EncodedBlock = typename T::BlockType;
using DecodedBlock = ColorBlock<BlockWidth, BlockHeight>;
virtual EncodedBlock EncodeBlock(const DecodedBlock &block) const = 0;
virtual T Encode(const RawTexture &decoded) const override {
auto encoded = T(decoded.Width(), decoded.Height());
int blocks_x = encoded.BlocksX();
int blocks_y = encoded.BlocksY();
// from experimentation, multithreading this using OpenMP sometimes actually makes encoding slower
// due to thread creation/teardown taking longer than the encoding process itself.
// As a result, this is sometimes left as a serial operation despite being embarassingly parallelizable
// threshold for number of blocks before multithreading is set by overriding MTThreshold()
#pragma omp parallel for if (blocks_x * blocks_y >= MTThreshold())
for (int y = 0; y < blocks_y; y++) {
for (int x = 0; x < blocks_x; x++) {
auto pixels = decoded.GetBlock<BlockWidth, BlockHeight>(x, y);
auto block = EncodeBlock(pixels);
encoded.SetBlock(x, y, block);
}
}
return encoded;
}
virtual size_t MTThreshold() const { return SIZE_MAX; };
};
} // namespace quicktex

187
quicktex/Texture.h Normal file
View File

@ -0,0 +1,187 @@
/* Python-rgbcx Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <array>
#include <climits>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#include <vector>
#include "Color.h"
#include "ColorBlock.h"
namespace quicktex {
class Texture {
public:
virtual ~Texture() = default;
virtual int Width() const { return _width; }
virtual int Height() const { return _height; }
virtual std::tuple<int, int> Dimensions() const { return std::tuple<int, int>(_width, _height); }
/**
* The texture's total size
* @return The size of the texture in bytes.
*/
virtual size_t Size() const noexcept = 0;
virtual const uint8_t *Data() const noexcept = 0;
virtual uint8_t *Data() noexcept = 0;
protected:
Texture(int width, int height) : _width(width), _height(height) {
if (width <= 0) throw std::invalid_argument("Texture width must be greater than 0");
if (height <= 0) throw std::invalid_argument("Texture height must be greater than 0");
}
int _width;
int _height;
};
class RawTexture : public Texture {
using Base = Texture;
public:
/**
* Create a new RawTexture
* @param width width of the texture in pixels
* @param height height of the texture in pixels
*/
RawTexture(int width, int height) : Base(width, height), _pixels(_width * _height) {}
Color GetPixel(int x, int y) const {
if (x < 0 || x >= _width) throw std::invalid_argument("x value out of range.");
if (y < 0 || y >= _height) throw std::invalid_argument("y value out of range.");
return _pixels.at(x + (y * _width));
}
void SetPixel(int x, int y, Color val) {
if (x < 0 || x >= _width) throw std::invalid_argument("x value out of range.");
if (y < 0 || y >= _height) throw std::invalid_argument("y value out of range.");
_pixels.at(x + (y * _width)) = val;
}
size_t Size() const noexcept override { return static_cast<unsigned long>(Width() * Height()) * sizeof(Color); }
template <int N, int M> ColorBlock<N, M> GetBlock(int block_x, int block_y) const {
if (block_x < 0 || (block_x + 1) * N > _width) throw std::out_of_range("x value out of range.");
if (block_y < 0 || (block_y + 1) * M > _height) throw std::out_of_range("y value out of range.");
// coordinates in the image of the top-left pixel of the selected block
ColorBlock<N, M> block;
int pixel_x = block_x * N;
int pixel_y = block_y * M;
if (pixel_x + N < _width && pixel_y + M < _height) {
// fast memcpy if the block is entirely inside the bounds of the texture
for (int y = 0; y < M; y++) {
// copy each row into the ColorBlock
block.SetRow(y, &_pixels[pixel_x + (_width * (pixel_y + y))]);
}
} else {
// slower pixel-wise copy if the block goes over the edges
for (int x = 0; x < N; x++) {
for (int y = 0; y < M; y++) { block.Set(x, y, GetPixel((pixel_x + x) % _width, (pixel_y + y) % _height)); }
}
}
return block;
}
template <int N, int M> void SetBlock(int block_x, int block_y, const ColorBlock<N, M> &block) {
if (block_x < 0) throw std::out_of_range("x value out of range.");
if (block_y < 0) throw std::out_of_range("y value out of range.");
// coordinates in the image of the top-left pixel of the selected block
int pixel_x = block_x * N;
int pixel_y = block_y * M;
if (pixel_x + N < _width && pixel_y + M < _height) {
// fast row-wise memcpy if the block is entirely inside the bounds of the texture
for (int y = 0; y < M; y++) {
// copy each row out of the ColorBlock
block.GetRow(y, &_pixels[pixel_x + (_width * (pixel_y + y))]);
}
} else {
// slower pixel-wise copy if the block goes over the edges
for (int x = 0; x < N; x++) {
for (int y = 0; y < M; y++) { SetPixel((pixel_x + x) % _width, (pixel_y + y) % _height, block.Get(x, y)); }
}
}
}
virtual const uint8_t *Data() const noexcept override { return reinterpret_cast<const uint8_t *>(_pixels.data()); }
virtual uint8_t *Data() noexcept override { return reinterpret_cast<uint8_t *>(_pixels.data()); }
protected:
std::vector<Color> _pixels;
};
template <typename B> class BlockTexture final : public Texture {
private:
std::vector<B> _blocks;
int _width_b;
int _height_b;
public:
using BlockType = B;
using Base = Texture;
/**
* Create a new BlockTexture
* @param width width of the texture in pixels. must be divisible by B::Width
* @param height height of the texture in pixels. must be divisible by B::Height
*/
BlockTexture(int width, int height) : Base(width, height) {
_width_b = (_width + B::Width - 1) / B::Width;
_height_b = (_height + B::Height - 1) / B::Height;
_blocks = std::vector<B>(_width_b * _height_b);
}
constexpr int BlocksX() const { return _width_b; }
constexpr int BlocksY() const { return _height_b; }
constexpr std::tuple<int, int> BlocksXY() const { return std::tuple<int, int>(_width_b, _height_b); }
B GetBlock(int x, int y) const {
if (x < 0 || x >= _width_b) throw std::out_of_range("x value out of range.");
if (y < 0 || y >= _height_b) throw std::out_of_range("y value out of range.");
return _blocks.at(x + (y * _width_b));
}
void SetBlock(int x, int y, const B &val) {
if (x < 0 || x >= _width_b) throw std::out_of_range("x value out of range.");
if (y < 0 || y >= _height_b) throw std::out_of_range("y value out of range.");
_blocks.at(x + (y * _width_b)) = val;
}
size_t Size() const noexcept override { return _blocks.size() * sizeof(B); }
const uint8_t *Data() const noexcept override { return reinterpret_cast<const uint8_t *>(_blocks.data()); }
uint8_t *Data() noexcept override{ return reinterpret_cast<uint8_t *>(_blocks.data()); }
};
} // namespace quicktex

View File

@ -17,78 +17,47 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "_bindings.h"
#include <pybind11/pybind11.h>
#include "BlockDecoder.h"
#include "BlockEncoder.h"
#include "Color.h"
#include "Decoder.h"
#include "Encoder.h"
#include "Texture.h"
#include "_bindings.h"
namespace py = pybind11;
namespace quicktex::bindings {
void InitS3TC(py::module_ &m);
py::bytes EncodeImage(const BlockEncoder &self, py::bytes decoded, unsigned image_width, unsigned image_height) {
if (image_width % self.BlockWidth() != 0) throw std::invalid_argument("Width is not an even multiple of block_width");
if (image_height % self.BlockHeight() != 0) throw std::invalid_argument("Height is not an even multiple of block_height");
if (image_width == 0 || image_height == 0) throw std::invalid_argument("Image has zero size");
size_t size = image_width * image_height;
size_t block_size = (size / (self.BlockHeight() * self.BlockWidth())) * self.BlockSize();
size_t color_size = size * sizeof(Color);
std::string encoded_str = std::string(block_size, 0);
std::string decoded_str = (std::string)decoded; // decoded data is copied here, unfortunately
if (decoded_str.size() != color_size) throw std::invalid_argument("Incompatible data: image width and height do not match the size of the decoded image");
self.EncodeImage(reinterpret_cast<uint8_t *>(encoded_str.data()), reinterpret_cast<Color *>(decoded_str.data()), image_width, image_height);
auto bytes = py::bytes(encoded_str); // encoded data is copied here, unfortunately
return bytes;
}
py::bytes DecodeImage(const BlockDecoder &self, py::bytes encoded, unsigned image_width, unsigned image_height) {
if (image_width % self.BlockWidth() != 0) throw std::invalid_argument("Width is not an even multiple of block_width");
if (image_height % self.BlockHeight() != 0) throw std::invalid_argument("Height is not an even multiple of block_height");
if (image_width == 0 || image_height == 0) throw std::invalid_argument("Image has zero size");
size_t size = image_width * image_height;
size_t block_size = (size / (self.BlockHeight() * self.BlockWidth())) * self.BlockSize();
size_t color_size = size * sizeof(Color);
std::string encoded_str = (std::string)encoded; // encoded data is copied here, unfortunately
std::string decoded_str = std::string(color_size, 0);
if (encoded_str.size() != block_size) throw std::invalid_argument("Incompatible data: image width and height do not match the size of the encoded image");
self.DecodeImage(reinterpret_cast<uint8_t *>(encoded_str.data()), reinterpret_cast<Color *>(decoded_str.data()), image_width, image_height);
auto bytes = py::bytes(decoded_str); // decoded data is copied here, unfortunately
return bytes;
}
PYBIND11_MODULE(_quicktex, m) {
m.doc() = "More Stuff";
py::options options;
// BlockDecoder
py::class_<BlockDecoder> block_decoder(m, "BlockDecoder");
// Texture
block_decoder.def("decode_image", &DecodeImage);
block_decoder.def_property_readonly("block_size", &BlockDecoder::BlockSize);
block_decoder.def_property_readonly("block_width", &BlockDecoder::BlockWidth);
block_decoder.def_property_readonly("block_height", &BlockDecoder::BlockHeight);
py::class_<Texture> texture(m, "Texture", py::buffer_protocol());
// BlockEncoder
py::class_<BlockEncoder> block_encoder(m, "BlockEncoder");
texture.def_property_readonly("size", &Texture::Size);
texture.def_property_readonly("dimensions", &Texture::Dimensions);
texture.def_property_readonly("width", &Texture::Width);
texture.def_property_readonly("height", &Texture::Height);
block_encoder.def("encode_image", &EncodeImage);
block_encoder.def_property_readonly("block_size", &BlockEncoder::BlockSize);
block_encoder.def_property_readonly("block_width", &BlockEncoder::BlockWidth);
block_encoder.def_property_readonly("block_height", &BlockEncoder::BlockHeight);
texture.def_buffer([](Texture &t) { return py::buffer_info(t.Data(), t.Size()); });
texture.def("tobytes", [](const Texture &t) { return py::bytes(reinterpret_cast<const char *>(t.Data()), t.Size()); });
// RawTexture
py::class_<RawTexture, Texture> raw_texture(m, "RawTexture");
raw_texture.def(py::init<int, int>(), "width"_a, "height"_a);
raw_texture.def_static("frombytes", &BufferToTexture<RawTexture>, "data"_a, "width"_a, "height"_a);
DefSubscript2D(raw_texture, &RawTexture::GetPixel, &RawTexture::SetPixel, &RawTexture::Dimensions);
InitS3TC(m);
}

236
quicktex/_bindings.h Normal file
View File

@ -0,0 +1,236 @@
/* Quicktex Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <pybind11/operators.h>
#include <pybind11/pybind11.h>
#include <cstdint>
#include <cstring>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include "Color.h"
#include "ColorBlock.h"
#include "Texture.h"
#include "util.h"
namespace pybind11::detail {
using namespace quicktex;
/// Type caster for color class to allow it to be converted to and from a python tuple
template <> struct type_caster<Color> {
public:
PYBIND11_TYPE_CASTER(Color, _("Color"));
bool load(handle src, bool) {
PyObject* source = src.ptr();
PyObject* tmp = PySequence_Tuple(source);
// if the object is not a tuple, return false
if (!tmp) { return false; } // incorrect type
// check the size
Py_ssize_t size = PyTuple_Size(tmp);
if (size < 3 || size > 4) { return false; } // incorrect size
value.a = 0xFF;
// now we get the contents
for (int i = 0; i < size; i++) {
PyObject* src_chan = PyTuple_GetItem(tmp, i);
PyObject* tmp_chan = PyNumber_Long(src_chan);
if (!tmp_chan) return false; // incorrect channel type
auto chan = PyLong_AsLong(tmp_chan);
if (chan > 0xFF || chan < 0) return false; // item out of range
value[static_cast<unsigned>(i)] = static_cast<uint8_t>(chan);
Py_DECREF(tmp_chan);
}
Py_DECREF(tmp);
return !PyErr_Occurred();
}
static handle cast(Color src, return_value_policy, handle) {
PyObject* val = PyTuple_New(4);
for (int i = 0; i < 4; i++) {
PyObject* chan = PyLong_FromLong(src[static_cast<unsigned>(i)]);
PyTuple_SetItem(val, i, chan);
}
return val;
}
};
} // namespace pybind11::detail
namespace py = pybind11;
namespace quicktex::bindings {
using namespace pybind11::literals;
template <typename T> T BufferToTexture(py::buffer buf, int width, int height) {
static_assert(std::is_base_of<Texture, T>::value);
static_assert(std::is_constructible<T, int, int>::value);
auto info = buf.request(false);
auto output = T(width, height);
auto dst_size = output.Size();
if (info.format != py::format_descriptor<uint8_t>::format()) throw std::runtime_error("Incompatible format in python buffer: expected a byte array.");
if (info.size < (ssize_t)dst_size) std::runtime_error("Incompatible format in python buffer: Input data is smaller than texture size.");
if (info.ndim == 1) {
if (info.shape[0] < (ssize_t)dst_size) throw std::runtime_error("Incompatible format in python buffer: 1-D buffer has incorrect length.");
if (info.strides[0] != 1) throw std::runtime_error("Incompatible format in python buffer: 1-D buffer is not contiguous.");
} else {
throw std::runtime_error("Incompatible format in python buffer: Incorrect number of dimensions.");
}
std::memcpy(output.Data(), info.ptr, dst_size);
return output;
}
template <typename T> T BufferToPOD(py::buffer buf) {
static_assert(std::is_trivially_copyable_v<T>);
auto info = buf.request(false);
if (info.format != py::format_descriptor<uint8_t>::format()) throw std::runtime_error("Incompatible format in python buffer: expected a byte array.");
if (info.size < (ssize_t)sizeof(T)) std::runtime_error("Incompatible format in python buffer: Input data is smaller than texture size.");
if (info.ndim == 1) {
if (info.shape[0] < (ssize_t)sizeof(T)) throw std::runtime_error("Incompatible format in python buffer: 1-D buffer has incorrect length.");
if (info.strides[0] != 1) throw std::runtime_error("Incompatible format in python buffer: 1-D buffer is not contiguous.");
} else {
throw std::runtime_error("Incompatible format in python buffer: Incorrect number of dimensions.");
}
const T* ptr = reinterpret_cast<const T*>(info.ptr);
return *ptr;
}
inline int PyIndex(int val, int size, std::string name = "index") {
if (val < -size || val >= size) throw std::out_of_range(name + " value out of range");
if (val < 0) return size + val;
return val;
}
template <typename T, typename Getter, typename Setter, typename Extent> void DefSubscript(py::class_<T> t, Getter&& get, Setter&& set, Extent&& ext) {
using V = typename std::invoke_result<Getter, T*, int>::type;
t.def(
"__getitem__", [get, ext](T& self, int index) { return (self.*get)(PyIndex(index, (self.*ext)())); }, "key"_a);
t.def(
"__setitem__", [set, ext](T& self, int index, V val) { (self.*set)(PyIndex(index, (self.*ext)()), val); }, "key"_a, "value"_a);
}
template <typename Tpy, typename Getter, typename Setter, typename Extent> void DefSubscript2D(Tpy t, Getter&& get, Setter&& set, Extent&& ext) {
using T = typename Tpy::type;
using V = typename std::invoke_result<Getter, T*, int, int>::type;
using Coords = std::tuple<int, int>;
t.def(
"__getitem__",
[get, ext](T& self, Coords pnt) {
Coords s = (self.*ext)();
int x = PyIndex(std::get<0>(pnt), std::get<0>(s), "x");
int y = PyIndex(std::get<1>(pnt), std::get<1>(s), "y");
return (self.*get)(x, y);
},
"key"_a);
t.def(
"__setitem__",
[set, ext](T& self, Coords pnt, const V& val) {
Coords s = (self.*ext)();
int x = PyIndex(std::get<0>(pnt), std::get<0>(s), "x");
int y = PyIndex(std::get<1>(pnt), std::get<1>(s), "y");
(self.*set)(x, y, val);
},
"key"_a, "value"_a);
}
template <typename B> py::class_<B> BindBlock(py::module_& m, const char* name) {
const char* frombytes_doc = R"doc(
Create a new {0} by copying a bytes-like object.
:param b: A bytes-like object at least the size of the block.
)doc";
const char* tobytes_doc = R"doc(
Pack the {0} into a bytestring.
:returns: A bytes object of length {1}.
)doc";
py::class_<B> block(m, name, py::buffer_protocol());
block.def_static("frombytes", &BufferToPOD<B>, "data"_a, Format(frombytes_doc, name).c_str());
block.def_readonly_static("width", &B::Width, "The width of the block in pixels.");
block.def_readonly_static("height", &B::Height, "The height of the block in pixels.");
block.def_property_readonly_static(
"dimensions", [](py::object) { return std::make_tuple(B::Width, B::Height); }, "The dimensions of the block in pixels.");
block.def_property_readonly_static(
"size", [](py::object) { return sizeof(B); }, "The size of the block in bytes.");
block.def(py::self == py::self);
block.def_buffer([](B& b) { return py::buffer_info(reinterpret_cast<uint8_t*>(&b), sizeof(B)); });
block.def(
"tobytes", [](const B& b) { return py::bytes(reinterpret_cast<const char*>(&b), sizeof(B)); },
Format(tobytes_doc, name, std::to_string(sizeof(B))).c_str());
return block;
}
template <typename B> py::class_<BlockTexture<B>> BindBlockTexture(py::module_& m, const char* name) {
const auto* const constructor_str = R"doc(
Create a new blank {0} with the given dimensions.
If the dimenions are not multiples of the block dimensions, enough blocks will be allocated
to cover the entire texture, and it will be implicitly cropped during decoding.
:param int width: The width of the texture in pixels. Must be > 0.
:param int height: The height of the texture in pixels. must be > 0
)doc";
const auto* const from_bytes_str = R"doc(
Create a new {0} with the given dimensions, and copy a bytes-like object into it.
If the dimenions are not multiples of the block dimensions, enough blocks will be allocated
to cover the entire texture, and it will be implicitly cropped during decoding.
:param b: A bytes-like object at least the size of the resulting texture.
:param int width: The width of the texture in pixels. Must be > 0.
:param int height: The height of the texture in pixels. must be > 0
)doc";
using BTex = BlockTexture<B>;
py::class_<BTex, Texture> block_texture(m, name);
block_texture.def(py::init<int, int>(), "width"_a, "height"_a, Format(constructor_str, name).c_str());
block_texture.def_static("from_bytes", &BufferToTexture<BTex>, "data"_a, "width"_a, "height"_a, Format(from_bytes_str, name).c_str());
block_texture.def_property_readonly("width_blocks", &BTex::BlocksX, "The width of the texture in blocks.");
block_texture.def_property_readonly("height_blocks", &BTex::BlocksY, "The height of the texture in blocks.");
block_texture.def_property_readonly("dimensions_blocks", &BTex::BlocksXY, "The dimensions of the texture in blocks.");
DefSubscript2D(block_texture, &BTex::GetBlock, &BTex::SetBlock, &BTex::BlocksXY);
return block_texture;
}
} // namespace quicktex::bindings

15
quicktex/cli/__init__.py Normal file
View File

@ -0,0 +1,15 @@
import click
from encode import encode
from decode import decode
@click.group()
def cli():
"""Encode and Decode various image formats"""
cli.add_command(encode)
cli.add_command(decode)
if __name__ == '__main__':
cli()

16
quicktex/cli/decode.py Normal file
View File

@ -0,0 +1,16 @@
import click
import io
import os
from PIL import Image
@click.command()
@click.option('-f', '--flip', type=bool, default=True, show_default=True, help="vertically flip image after converting")
@click.option('-r', '--remove', help="remove input images after converting")
@click.option('-o', '--output', help="output file name. Must only specify one input image.")
@click.option('-s', '--suffix', type=str, default='', help="suffix to append to output file(s).")
@click.argument('filenames', nargs=-1, type=click.Path(exists=True))
def decode(flip, remove, output, suffix, filenames):
"""Decode an input texture file to an image"""
for filename in filenames:
assert filename.endswith(".dds"), "Incorrect file extension"

6
quicktex/cli/encode.py Normal file
View File

@ -0,0 +1,6 @@
import click
@click.group()
def encode():
"""Encode an input image to a texture file of the given format"""

315
quicktex/dds.py Normal file
View File

@ -0,0 +1,315 @@
from __future__ import annotations
import enum
import struct
import typing
@typing.final
class PixelFormat:
"""
DDS header surface format.
For more information, see microsoft documentation for `DDS_PIXELFORMAT <https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat>`_.
"""
size = 32
"""The size of a PixelFormat block in bytes."""
class Flags(enum.IntFlag):
"""Values which indicate what type of data is in the surface."""
ALPHAPIXELS = 0x1
"""Texture contains alpha data (:py:attr:`~PixelFormat.pixel_bitmasks[3]` contains valid data)."""
ALPHA = 0x2
"""Used in some older DDS files for alpha channel only uncompressed data
(:py:attr:`~PixelFormat.pixel_size` contains the alpha channel bitcount; :py:attr:`~PixelFormat.pixel_bitmasks[3]` contains valid data)."""
FOURCC = 0x4
"""Texture contains compressed RGB data; :py:attr:`~PixelFormat.four_cc` contains valid data."""
RGB = 0x40
"""Texture contains uncompressed RGB data; :py:attr:`~PixelFormat.pixel_size` and the RGB masks
(:py:attr:`~PixelFormat.pixel_bitmasks[0:3]`) contain valid data."""
YUV = 0x200
"""Used in some older DDS files for YUV uncompressed data
(:py:attr:`~PixelFormat.pixel_size` contains the YUV bit count; :py:attr:`~PixelFormat.pixel_bitmasks[0]` contains the Y mask,
:py:attr:`~PixelFormat.pixel_bitmasks[1]` contains the U mask, :py:attr:`~PixelFormat.pixel_bitmasks[2]` contains the V mask)."""
LUMINANCE = 0x20000
"""Used in some older DDS files for single channel color uncompressed data (:py:attr:`~PixelFormat.pixel_size`
contains the luminance channel bit count; :py:attr:`~PixelFormat.pixel_bitmasks[0]` contains the channel mask).
Can be combined with :py:attr:`ALPHAPIXELS` for a two channel uncompressed DDS file."""
def __init__(self):
self.flags: PixelFormat.Flags = PixelFormat.Flags.FOURCC
"""Flags representing which data is valid."""
self.four_cc: str = "NONE"
"""FourCC code of the texture format. Valid texture format strings are ``DXT1``, ``DXT2``, ``DXT3``, ``DXT4``, or ``DXT5``.
If a DirectX 10 header is used, this is ``DX10``."""
self.pixel_size: int = 0
"""Number of bits in each pixel if the texture is uncompressed"""
self.pixel_bitmasks: typing.Tuple[int, int, int, int] = (0, 0, 0, 0)
"""Tuple of bitmasks for each channel"""
@staticmethod
def from_bytes(data) -> PixelFormat:
"""
Create a new PixelFormat object from a bytes-like object
:param data: A bytes-like object holding the raw data to unpack
:return: An unpacked PixelFormat object
"""
assert len(data) == PixelFormat.size, "Incorrect number of bytes in input."
unpacked = struct.unpack('<2I4s5I', data)
assert unpacked[0] == PixelFormat.size, "Incorrect pixelformat size."
pf = PixelFormat()
pf.flags = PixelFormat.Flags(unpacked[1])
pf.four_cc = unpacked[2].decode()
pf.pixel_size = unpacked[3]
pf.pixel_bitmasks = unpacked[4:8]
return pf
@staticmethod
def from_file(file: typing.BinaryIO) -> PixelFormat:
"""
Create a new PixelFormat object from a file. The file position will be advanced by 32 bytes.
:param file: A file-like object to read from.
:return: An unpacked PixelFormat object
"""
assert file.readable(), "Input file is not readable."
data = file.read(PixelFormat.size)
return PixelFormat.from_bytes(data)
def to_bytes(self) -> bytes:
"""
Write the PixelFormat object to a bytes object.
:return: The packed PixelFormat object
"""
data = struct.pack('<2I4s5I', 32, int(self.flags), bytes(self.four_cc, 'ascii'), self.pixel_size, *self.pixel_bitmasks)
assert len(data) == PixelFormat.size
return data
@typing.final
class DDSHeader:
"""
Header for a microsoft DDS file
For more information, see microsoft documentation for `DDS_HEADER <https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header>`_.
"""
size = 124
"""The size of a DDS header in bytes."""
class Flags(enum.IntFlag):
"""Flags to indicate which members contain valid data."""
CAPS = 0x1
"""Required in every .dds file."""
HEIGHT = 0x2
"""Required in every .dds file."""
WIDTH = 0x4
"""Required in every .dds file."""
PITCH = 0x8
"""Required when :py:attr:`~DDSHeader.pitch` is provided for an uncompressed texture."""
PIXEL_FORMAT = 0x1000
"""Required in every .dds file."""
MIPMAPCOUNT = 0x20000
"""Required when :py:attr:`~DDSHeader.mipmap_count` is provided for a mipmapped texture."""
LINEAR_SIZE = 0x80000
"""Required when :py:attr:`~DDSHeader.pitch` is provided for a compressed texture."""
DEPTH = 0x800000
"""Required when :py:attr:`~DDSHeader.depth` is provided for a depth texture."""
REQUIRED = CAPS | HEIGHT | WIDTH | PIXEL_FORMAT
def __init__(self):
self.flags: DDSHeader.Flags = DDSHeader.Flags.REQUIRED
"""Flags to indicate which members contain valid data."""
self.dimensions: typing.Tuple[int, int] = (0, 0)
"""Width and height of the texture or its first mipmap"""
self.pitch: int = 0
"""The pitch or number of bytes per scan line in an uncompressed texture;
the total number of bytes in the top level texture for a compressed texture."""
self.depth: int = 0
"""Depth of a volume texture (in pixels), otherwise unused."""
self.mipmap_count: int = 0
"""Number of mipmap levels, otherwise unused."""
self.pixel_format: PixelFormat = PixelFormat()
"""The pixel format"""
self.caps: typing.Tuple[int, int, int, int] = (0, 0, 0, 0)
"""Specifies the complexity of the surfaces stored."""
@staticmethod
def from_bytes(data) -> DDSHeader:
"""
Create a new DDS Header from a bytes-like object
:param data: A bytes-like object holding the raw data to unpack
:return: An unpacked DDS header
"""
assert len(data) == PixelFormat.size, "Incorrect number of bytes in input."
unpacked = struct.unpack('<7I44x', data[0:72])
assert unpacked[0] == DDSHeader.size, "Incorrect DDS header size."
header = DDSHeader()
header.flags = DDSHeader.Flags(unpacked[1])
header.dimensions = unpacked[2:4:-1]
header.pitch, header.depth, header.mipmapcount = unpacked[4:7]
header.pixel_format = PixelFormat.from_bytes(data[72:104])
header.caps = struct.unpack('<4I4x', data[104:124])
assert all([val > 0 for val in header.dimensions]), "Image size is zero"
return header
@staticmethod
def from_file(file: typing.BinaryIO) -> DDSHeader:
"""
Create a new DDS header from a file. the file position will be advanced by 124 bytes.
:param file: A file-like object to read from.
:return: An unpacked DDS header
"""
assert file.readable(), "Input file is not readable."
return DDSHeader.from_bytes(file.read(DDSHeader.size))
def to_bytes(self) -> bytes:
"""
Write the DDS header to a bytes object.
:return: The packed DDS header
"""
data = b''
# write header
data += struct.pack('<7I44x', DDSHeader.size, int(self.flags), self.dimensions[1], self.dimensions[0],
self.pitch, self.depth, self.mipmap_count)
data += self.pixel_format.to_bytes()
data += struct.pack('<4I4x', *self.caps)
assert len(data) == DDSHeader.size
return data
@typing.final
class DDSFile:
"""
A microsoft DDS file, containing header information and one or more textures
For more information, see microsoft's `Reference for DDS <https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-reference>`_.
"""
magic = b'DDS '
"""Magic bytes at the start of every DDS file."""
extension = 'dds'
"""Extension for a DDS file."""
def __init__(self):
self.header: DDSHeader = DDSHeader()
"""The DDS file's header object"""
self.textures: typing.List[bytes] = []
"""A list of bytes objects for each texture in the file"""
@staticmethod
def from_file(file: typing.BinaryIO) -> DDSFile:
"""
Create a new DDSFile object from the contents of a file.
:param file: A file-like object to read from.
:return: An unpacked DDS file
"""
assert file.readable(), "Input file is not readable."
assert file.read(4) == DDSFile.magic, "Incorrect magic bytes in DDS file."
dds = DDSFile()
dds.header = DDSHeader.from_file(file)
four_cc = dds.header.pixel_format.four_cc
assert four_cc == 'DXT1' or four_cc == 'DXT5'
block_size = 8 if four_cc == 'DXT1' else 16
mip_count = dds.header.mipmap_count if DDSHeader.Flags.MIPMAPCOUNT in dds.header.flags else 1
dds.textures = []
for mip in range(mip_count):
block_dimensions = dds.image_block_dimensions()
blocks = block_dimensions[0] * block_dimensions[1]
size = blocks * block_size
dds.textures.append(file.read(size))
return dds
def write(self, file: typing.BinaryIO):
"""
Write a DDSFile object to a file.
:param file: a file-like object to write to.
"""
assert file.writable(), "Output file is not writable"
file.write(DDSFile.magic)
file.write(self.header.to_bytes())
for texture in self.textures:
file.write(texture)
def mipmap_count(self) -> int:
"""
Get the number of mipmaps in the dds file.
:return: The number of mipmaps
"""
if DDSHeader.Flags.MIPMAPCOUNT in self.header.flags:
mips = self.header.mipmap_count
assert mips > 0
return mips
else:
return 1
def image_dimensions(self, mip: int = 0) -> typing.Tuple[int, int]:
"""
Calculate the dimensions of a mip level in pixels.
:param int mip: which mip level to calculate the dimensions of, between 0 and :py:attr:`~DDSHeader.mipmap_count` exclusive.
:return: the dimensions of the selected mipmap in pixels
"""
assert self.mipmap_count() > mip >= 0, "Invalid mip level"
return tuple([max(size // (2 ** mip), 1) for size in self.header.dimensions])
def image_block_dimensions(self, mip: int = 0) -> typing.Tuple[int, int]:
"""
Calculate the dimensions of a mip level in 4x4 blocks. Only relevent for compressed textures.
:param int mip: Whick mip level to calculate the dimensions of, between 0 and :py:attr:`~DDSHeader.mipmap_count` exclusive.
:return: the dimensions of the selected mipmap in blocks
"""
dimensions = self.image_dimensions(mip)
assert all(size > 0 for size in dimensions)
return tuple([max((size + 3) // 4, 1) for size in dimensions])

64
quicktex/image_utils.py Normal file
View File

@ -0,0 +1,64 @@
"""Various utilities for working with Pillow images"""
from PIL import Image
import typing
import math
def pad(source: Image.Image, block_dimensions=(4, 4)) -> Image.Image:
"""
Pad an image to be divisible by a specific block size. The input image is repeated into the unused areas so that bilinar filtering works correctly.
:param source: Input image to add padding to. This will not be modified.
:param block_dimensions: The size of a single block that the output must be divisible by.
:return: A new image with the specified padding added.
"""
assert all([dim > 0 for dim in block_dimensions]), "Invalid block size"
padded_dimensions = tuple([
math.ceil(i_dim / b_dim) * b_dim
for i_dim in source.size
for b_dim in block_dimensions
])
if padded_dimensions == source.size:
# no padding is necessary
return source
output = Image.new(source.mode, padded_dimensions)
for x in range(math.ceil(padded_dimensions[0] / source.width)):
for y in range(math.ceil(padded_dimensions[1] / source.height)):
output.paste(source, (x * source.width, y * source.height))
return output
def mip_sizes(dimensions: typing.Tuple[int, int], mip_count: typing.Optional[int] = None) -> typing.List[typing.Tuple[int, int]]:
"""
Create a chain of mipmap sizes for a given source source size, where each source is half the size of the one before.
Note that the division by 2 rounds down. So a 63x63 texture has as its next lowest mipmap level 31x31. And so on.
See the `OpenGL wiki page on mipmaps <https://www.khronos.org/opengl/wiki/Texture#Mip_maps>`_ for more info.
:param dimensions: Size of the source source in pixels
:param mip_count: Number of mipmap sizes to generate. By default, generate until the last mip level is 1x1.
Resulting mip chain will be smaller if a 1x1 mip level is reached before this value.
:return: A list of 2-tuples representing the dimensions of each mip level, including ``dimensions`` at element 0.
"""
assert all([dim > 0 for dim in dimensions]), "Invalid source dimensions"
if not mip_count:
mip_count = math.ceil(math.log2(max(dimensions))) # maximum possible number of mips for a given source
assert mip_count > 0, "mip_count must be greater than 0"
chain = []
for mip in range(mip_count):
chain.append(dimensions)
dimensions = tuple([max(dim // 2, 1) for dim in dimensions])
if all([dim == 1 for dim in dimensions]):
break # we've reached a 1x1 mip and can get no smaller
return chain

View File

@ -1,26 +0,0 @@
/* Python-rgbcx Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#ifdef NDEBUG // asserts disabled
constexpr bool ndebug = true;
#else // asserts enabled
constexpr bool ndebug = false;
#endif

0
quicktex/py.typed Normal file
View File

View File

@ -38,8 +38,8 @@ void InitS3TC(py::module_ &m) {
InitInterpolator(s3tc);
InitBC1(s3tc);
InitBC3(s3tc);
InitBC4(s3tc);
InitBC3(s3tc);
InitBC5(s3tc);
}
} // namespace quicktex::bindings

View File

@ -0,0 +1,44 @@
/* Quicktex Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "BC1Block.h"
#include <stdexcept>
#include <algorithm>
#include "../../util.h"
namespace quicktex::s3tc {
uint16_t BC1Block::GetColor0Raw() const { return Pack<uint8_t, uint16_t, 8, EndpointSize>(_color0); }
uint16_t BC1Block::GetColor1Raw() const { return Pack<uint8_t, uint16_t, 8, EndpointSize>(_color1); }
void BC1Block::SetColor0Raw(uint16_t c) { _color0 = Unpack<uint16_t, uint8_t, 8, EndpointSize>(c); }
void BC1Block::SetColor1Raw(uint16_t c) { _color1 = Unpack<uint16_t, uint8_t, 8, EndpointSize>(c); }
BC1Block::SelectorArray BC1Block::GetSelectors() const { return MapArray(_selectors, Unpack<uint8_t, uint8_t, SelectorBits, Width>); }
void BC1Block::SetSelectors(const BC1Block::SelectorArray& unpacked) {
for (unsigned y = 0; y < (unsigned)Height; y++) {
if (std::any_of(unpacked[y].begin(), unpacked[y].end(), [](uint8_t i) { return i > SelectorMax; }))
throw std::invalid_argument("Selector value out of bounds.");
}
_selectors = MapArray(unpacked, Pack<uint8_t, uint8_t, SelectorBits, Width>);
}
} // namespace quicktex::s3tc

View File

@ -20,66 +20,110 @@
#pragma once
#include <array>
#include <cassert>
#include <cstdint>
#include <cstdlib>
#include <utility>
#include "../../Color.h"
#include "../../util.h"
namespace quicktex::s3tc {
namespace quicktex::s3tc {
#pragma pack(push, 1)
class BC1Block {
class alignas(8) BC1Block {
public:
using UnpackedSelectors = std::array<std::array<uint8_t, 4>, 4>;
static constexpr size_t Width = 4;
static constexpr size_t Height = 4;
uint16_t GetLowColor() const { return static_cast<uint16_t>(_low_color[0] | (_low_color[1] << 8U)); }
uint16_t GetHighColor() const { return static_cast<uint16_t>(_high_color[0] | (_high_color[1] << 8U)); }
Color GetLowColor32() const { return Color::Unpack565(GetLowColor()); }
Color GetHighColor32() const { return Color::Unpack565(GetHighColor()); }
static constexpr size_t EndpointSize = 2; // size of a 5:6:5 endpoint in bytes
static constexpr size_t SelectorSize = 4; // size of selector array in bytes
static constexpr size_t SelectorBits = 2; // size of a selector in bits
static constexpr uint8_t SelectorMax = (1 << SelectorBits) - 1; // maximum value of a selector
bool Is3Color() const { return GetLowColor() <= GetHighColor(); }
void SetLowColor(uint16_t c) {
_low_color[0] = c & 0xFF;
_low_color[1] = (c >> 8) & 0xFF;
}
void SetHighColor(uint16_t c) {
_high_color[0] = c & 0xFF;
_high_color[1] = (c >> 8) & 0xFF;
}
uint32_t GetSelector(uint32_t x, uint32_t y) const {
assert((x < 4U) && (y < 4U));
return (selectors[y] >> (x * SelectorBits)) & SelectorMask;
}
void SetSelector(uint32_t x, uint32_t y, uint32_t val) {
assert((x < 4U) && (y < 4U) && (val < 4U));
selectors[y] &= (~(SelectorMask << (x * SelectorBits)));
selectors[y] |= (val << (x * SelectorBits));
}
UnpackedSelectors UnpackSelectors() const {
UnpackedSelectors unpacked;
for (unsigned i = 0; i < 4; i++) { unpacked[i] = Unpack<uint8_t, uint8_t, 2, 4>(selectors[i]); }
return unpacked;
}
void PackSelectors(const UnpackedSelectors& unpacked, uint8_t mask = 0) {
for (unsigned i = 0; i < 4; i++) { selectors[i] = mask ^ Pack<uint8_t, uint8_t, 2, 4>(unpacked[i]); }
}
constexpr static inline size_t EndpointSize = 2;
constexpr static inline size_t SelectorSize = 4;
constexpr static inline uint8_t SelectorBits = 2;
constexpr static inline uint8_t SelectorValues = 1 << SelectorBits;
constexpr static inline uint8_t SelectorMask = SelectorValues - 1;
using SelectorArray = std::array<std::array<uint8_t, Width>, Height>;
using ColorPair = std::pair<Color, Color>;
private:
std::array<uint8_t, EndpointSize> _low_color;
std::array<uint8_t, EndpointSize> _high_color;
std::array<uint8_t, EndpointSize> _color0;
std::array<uint8_t, EndpointSize> _color1;
std::array<uint8_t, SelectorSize> _selectors;
public:
std::array<uint8_t, 4> selectors;
/// Create a new BC1Block
constexpr BC1Block() {
static_assert(sizeof(BC1Block) == 8);
static_assert(sizeof(std::array<BC1Block, 10>) == 8 * 10);
static_assert(alignof(BC1Block) >= 8);
_color0 = _color1 = {0, 0};
_selectors = {0, 0, 0, 0};
}
/**
* Create a new BC1Block
* @param color0 first endpoint color
* @param color1 second endpoint color
* @param selectors the selectors as a 4x4 list of integers, between 0 and 3 inclusive.
*/
BC1Block(Color color0, Color color1, const SelectorArray& selectors) {
SetColor0(color0);
SetColor1(color1);
SetSelectors(selectors);
}
/**
* Create a new BC1Block
* @param ep0 first endpoint
* @param ep1 second endpoint
* @param selectors the selectors as a 4x4 list of integers, between 0 and 3 inclusive.
*/
BC1Block(uint16_t ep0, uint16_t ep1, const SelectorArray& selectors) {
SetColor0Raw(ep0);
SetColor1Raw(ep1);
SetSelectors(selectors);
}
/**
* Create a new BC1Block
* @param ep0 first endpoint
* @param ep1 second endpoint
* @param solid_mask single byte mask to use for each row
*/
BC1Block(uint16_t ep0, uint16_t ep1, uint8_t solid_mask) {
SetColor0Raw(ep0);
SetColor1Raw(ep1);
_selectors.fill(solid_mask);
}
uint16_t GetColor0Raw() const;
uint16_t GetColor1Raw() const;
void SetColor0Raw(uint16_t c);
void SetColor1Raw(uint16_t c);
Color GetColor0() const { return Color::Unpack565(GetColor0Raw()); }
Color GetColor1() const { return Color::Unpack565(GetColor1Raw()); }
ColorPair GetColors() const { return {GetColor0(), GetColor1()}; }
void SetColor0(Color c) { SetColor0Raw(c.Pack565()); }
void SetColor1(Color c) { SetColor1Raw(c.Pack565()); }
void SetColors(ColorPair cs) {
SetColor0(cs.first);
SetColor1(cs.second);
}
/**
* Get this block's selectors
* @return a 4x4 array of integers between 0 and 3 inclusive
*/
SelectorArray GetSelectors() const;
/**
* Set this block's selectors
* @param unpacked a 4x4 array of integers between 0 and 3 inclusive
*/
void SetSelectors(const SelectorArray& unpacked);
bool Is3Color() const { return GetColor0Raw() <= GetColor1Raw(); }
bool operator==(const BC1Block& other) const = default;
bool operator!=(const BC1Block& other) const = default;
};
#pragma pack(pop)
} // namespace quicktex::s3tc

View File

@ -23,34 +23,32 @@
#include <cassert>
#include <cstdint>
#include "../../BlockView.h"
#include "../../Color.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "BC1Block.h"
namespace quicktex::s3tc {
void BC1Decoder::DecodeBlock(Color4x4 dest, BC1Block *const block) const noexcept(ndebug) {
DecodeBlock(dest, block, true);
}
namespace quicktex::s3tc {
void BC1Decoder::DecodeBlock(Color4x4 dest, BC1Block *const block, bool allow_3color) const noexcept(ndebug) {
const auto l = block->GetLowColor();
const auto h = block->GetHighColor();
const auto selectors = block->UnpackSelectors();
const auto colors = _interpolator->InterpolateBC1(l, h, allow_3color);
ColorBlock<4, 4> BC1Decoder::DecodeBlock(const BC1Block &block) const { return DecodeBlock(block, true); }
ColorBlock<4, 4> BC1Decoder::DecodeBlock(const BC1Block &block, bool use_3color) const {
auto output = ColorBlock<4, 4>();
const auto l = block.GetColor0Raw();
const auto h = block.GetColor1Raw();
const auto selectors = block.GetSelectors();
const auto colors = _interpolator->Interpolate565BC1(l, h, use_3color);
for (unsigned y = 0; y < 4; y++) {
for (unsigned x = 0; x < 4; x++) {
const auto selector = selectors[y][x];
const auto color = colors[selector];
auto color = colors[selector];
assert(selector < 4);
assert((color.a == 0 && selector == 3 && l <= h) || color.a == UINT8_MAX);
if (write_alpha) {
dest.Get(x, y).SetRGBA(color);
} else {
dest.Get(x, y).SetRGB(color);
}
if (!write_alpha) { color.a = output.Get(x, y).a; }
output.Set(x, y, color);
}
}
return output;
}
} // namespace quicktex::s3tc

View File

@ -20,28 +20,26 @@
#pragma once
#include <memory>
#include <type_traits>
#include "../../BlockDecoder.h"
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "../../Decoder.h"
#include "../../Texture.h"
#include "../interpolator/Interpolator.h"
#include "BC1Block.h"
namespace quicktex::s3tc {
class BC1Decoder final : public BlockDecoderTemplate<BC1Block, 4, 4> {
class BC1Decoder final : public BlockDecoder<BlockTexture<BC1Block>> {
public:
using InterpolatorPtr = std::shared_ptr<Interpolator>;
BC1Decoder(bool write_alpha, InterpolatorPtr interpolator) : write_alpha(write_alpha), _interpolator(interpolator) {}
BC1Decoder(bool vwrite_alpha, InterpolatorPtr interpolator) : write_alpha(vwrite_alpha), _interpolator(interpolator) {}
BC1Decoder(bool write_alpha = false) : BC1Decoder(write_alpha, std::make_shared<Interpolator>()) {}
BC1Decoder(bool vwrite_alpha = false) : BC1Decoder(vwrite_alpha, std::make_shared<Interpolator>()) {}
BC1Decoder(InterpolatorPtr interpolator) : BC1Decoder(false, interpolator) {}
void DecodeBlock(Color4x4 dest, BC1Block *const block) const noexcept(ndebug) override;
void DecodeBlock(Color4x4 dest, BC1Block *const block, bool allow_3color) const noexcept(ndebug);
ColorBlock<4, 4> DecodeBlock(const BC1Block& block) const override;
ColorBlock<4, 4> DecodeBlock(const BC1Block& block, bool use_3color) const;
InterpolatorPtr GetInterpolator() const { return _interpolator; }

View File

@ -21,6 +21,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <cmath>
#include <cstdint>
@ -28,14 +29,14 @@
#include <stdexcept>
#include <type_traits>
#include "../../BlockView.h"
#include "../../Color.h"
#include "../../ColorBlock.h"
#include "../../Matrix4x4.h"
#include "../../Texture.h"
#include "../../Vector4.h"
#include "../../Vector4Int.h"
#include "../../bitwiseEnums.h"
#include "../../util.h"
#include "../interpolator/Interpolator.h"
#include "Histogram.h"
#include "OrderTable.h"
#include "SingleColorTable.h"
@ -258,11 +259,10 @@ void BC1Encoder::SetOrderings(OrderingPair orderings) {
void BC1Encoder::SetPowerIterations(unsigned int power_iters) { _power_iterations = clamp(power_iters, min_power_iterations, max_power_iterations); }
// Public methods
void BC1Encoder::EncodeBlock(Color4x4 pixels, BC1Block *dest) const {
BC1Block BC1Encoder::EncodeBlock(const ColorBlock<4, 4> &pixels) const {
if (pixels.IsSingleColor()) {
// single-color pixel block, do it the fast way
WriteBlockSolid(pixels.Get(0, 0), dest);
return;
return WriteBlockSolid(pixels.Get(0, 0));
}
auto metrics = pixels.GetMetrics();
@ -289,12 +289,12 @@ void BC1Encoder::EncodeBlock(Color4x4 pixels, BC1Block *dest) const {
EndpointMode endpoint_mode = (round == 1) ? EndpointMode::BoundingBox : _endpoint_mode;
EncodeResults trial_orig;
FindEndpoints(pixels, trial_orig, metrics, endpoint_mode);
FindEndpoints(trial_orig, pixels, metrics, endpoint_mode);
EncodeResults trial_result = trial_orig;
FindSelectors<ColorMode::FourColor>(pixels, trial_result, error_mode);
RefineBlockLS<ColorMode::FourColor>(pixels, trial_result, metrics, error_mode, total_ls_passes);
FindSelectors<ColorMode::FourColor>(trial_result, pixels, error_mode);
RefineBlockLS<ColorMode::FourColor>(trial_result, pixels, metrics, error_mode, total_ls_passes);
if (!needs_block_error || trial_result.error < result.error) {
result = trial_result;
@ -304,20 +304,20 @@ void BC1Encoder::EncodeBlock(Color4x4 pixels, BC1Block *dest) const {
// First refinement pass using ordered cluster fit
if (result.error > 0 && use_likely_orderings) {
for (unsigned iter = 0; iter < total_cf_passes; iter++) { RefineBlockCF<ColorMode::FourColor>(pixels, result, metrics, _error_mode, _orderings4); }
for (unsigned iter = 0; iter < total_cf_passes; iter++) { RefineBlockCF<ColorMode::FourColor>(result, pixels, metrics, _error_mode, _orderings4); }
}
// try for 3-color block
if (result.error > 0 && (bool)(_color_mode & ColorMode::ThreeColor)) {
EncodeResults trial_result = orig;
FindSelectors<ColorMode::ThreeColor>(pixels, trial_result, ErrorMode::Full);
RefineBlockLS<ColorMode::ThreeColor>(pixels, trial_result, metrics, ErrorMode::Full, total_ls_passes);
FindSelectors<ColorMode::ThreeColor>(trial_result, pixels, ErrorMode::Full);
RefineBlockLS<ColorMode::ThreeColor>(trial_result, pixels, metrics, ErrorMode::Full, total_ls_passes);
// First refinement pass using ordered cluster fit
if (trial_result.error > 0 && use_likely_orderings) {
for (unsigned iter = 0; iter < total_cf_passes; iter++) {
RefineBlockCF<ColorMode::ThreeColor>(pixels, trial_result, metrics, ErrorMode::Full, _orderings3);
RefineBlockCF<ColorMode::ThreeColor>(trial_result, pixels, metrics, ErrorMode::Full, _orderings3);
}
}
@ -329,21 +329,21 @@ void BC1Encoder::EncodeBlock(Color4x4 pixels, BC1Block *dest) const {
EncodeResults trial_result;
BlockMetrics metrics_no_black = pixels.GetMetrics(true);
FindEndpoints(pixels, trial_result, metrics_no_black, EndpointMode::PCA, true);
FindSelectors<ColorMode::ThreeColorBlack>(pixels, trial_result, ErrorMode::Full);
RefineBlockLS<ColorMode::ThreeColorBlack>(pixels, trial_result, metrics_no_black, ErrorMode::Full, total_ls_passes);
FindEndpoints(trial_result, pixels, metrics_no_black, EndpointMode::PCA, true);
FindSelectors<ColorMode::ThreeColorBlack>(trial_result, pixels, ErrorMode::Full);
RefineBlockLS<ColorMode::ThreeColorBlack>(trial_result, pixels, metrics_no_black, ErrorMode::Full, total_ls_passes);
if (trial_result.error < result.error) { result = trial_result; }
}
// refine endpoints by searching for nearby colors
if (result.error > 0 && _search_rounds > 0) { EndpointSearch(pixels, result); }
if (result.error > 0 && _search_rounds > 0) { EndpointSearch(result, pixels); }
WriteBlock(result, dest);
return WriteBlock(result);
}
// Private methods
void BC1Encoder::WriteBlockSolid(Color color, BC1Block *dest) const {
BC1Block BC1Encoder::WriteBlockSolid(Color color) const {
uint8_t mask = 0xAA; // 2222
uint16_t min16, max16;
@ -390,65 +390,58 @@ void BC1Encoder::WriteBlockSolid(Color color, BC1Block *dest) const {
}
}
dest->SetLowColor(max16);
dest->SetHighColor(min16);
dest->selectors[0] = mask;
dest->selectors[1] = mask;
dest->selectors[2] = mask;
dest->selectors[3] = mask;
return BC1Block(max16, min16, mask);
}
void BC1Encoder::WriteBlock(EncodeResults &block, BC1Block *dest) const {
BC1Block::UnpackedSelectors selectors;
uint16_t color1 = block.low.Pack565Unscaled();
uint16_t color0 = block.high.Pack565Unscaled();
BC1Block BC1Encoder::WriteBlock(EncodeResults &result) const {
BC1Block::SelectorArray selectors;
uint16_t ep1 = result.low.Pack565Unscaled();
uint16_t ep0 = result.high.Pack565Unscaled();
std::array<uint8_t, 4> lut;
assert(block.color_mode != ColorMode::Incomplete);
assert(result.color_mode != ColorMode::Incomplete);
if ((bool)(block.color_mode & ColorMode::FourColor)) {
if ((bool)(result.color_mode & ColorMode::FourColor)) {
lut = {1, 3, 2, 0};
if (color1 > color0) {
std::swap(color1, color0);
if (ep1 > ep0) {
std::swap(ep1, ep0);
lut = {0, 2, 3, 1};
} else if (color1 == color0) {
if (color1 > 0) {
color1--;
} else if (ep1 == ep0) {
if (ep1 > 0) {
ep1--;
lut = {0, 0, 0, 0};
} else {
assert(color1 == 0 && color0 == 0);
color0 = 1;
color1 = 0;
assert(ep1 == 0 && ep0 == 0);
ep0 = 1;
ep1 = 0;
lut = {1, 1, 1, 1};
}
}
assert(color0 > color1);
assert(ep0 > ep1);
} else {
lut = {1, 2, 0, 3};
if (color1 < color0) {
std::swap(color1, color0);
if (ep1 < ep0) {
std::swap(ep1, ep0);
lut = {0, 2, 1, 3};
}
assert(color0 <= color1);
assert(ep0 <= ep1);
}
for (unsigned i = 0; i < 16; i++) {
unsigned x = i % 4;
unsigned y = i / 4;
selectors[y][x] = lut[block.selectors[i]];
if (block.color_mode == ColorMode::ThreeColor) { assert(selectors[y][x] != 3); }
selectors[y][x] = lut[result.selectors[i]];
if (result.color_mode == ColorMode::ThreeColor) { assert(selectors[y][x] != 3); }
}
dest->SetLowColor(color0);
dest->SetHighColor(color1);
dest->PackSelectors(selectors);
return BC1Block(ep0, ep1, selectors);
}
void BC1Encoder::FindEndpointsSingleColor(EncodeResults &block, Color color, bool is_3color) const {
void BC1Encoder::FindEndpointsSingleColor(EncodeResults &result, Color color, bool is_3color) const {
auto &match5 = is_3color ? _single_match5_half : _single_match5;
auto &match6 = is_3color ? _single_match6_half : _single_match6;
@ -456,40 +449,40 @@ void BC1Encoder::FindEndpointsSingleColor(EncodeResults &block, Color color, boo
BC1MatchEntry match_g = match6->at(color.g);
BC1MatchEntry match_b = match5->at(color.b);
block.color_mode = is_3color ? ColorMode::ThreeColor : ColorMode::FourColor;
block.error = match_r.error + match_g.error + match_b.error;
block.low = Color(match_r.low, match_g.low, match_b.low);
block.high = Color(match_r.high, match_g.high, match_b.high);
result.color_mode = is_3color ? ColorMode::ThreeColor : ColorMode::FourColor;
result.error = match_r.error + match_g.error + match_b.error;
result.low = Color(match_r.low, match_g.low, match_b.low);
result.high = Color(match_r.high, match_g.high, match_b.high);
// selectors decided when writing, no point deciding them now
}
void BC1Encoder::FindEndpointsSingleColor(EncodeResults &block, Color4x4 &pixels, Color color, bool is_3color) const {
std::array<Color, 4> colors = _interpolator->InterpolateBC1(block.low, block.high, is_3color);
void BC1Encoder::FindEndpointsSingleColor(EncodeResults &result, const CBlock &pixels, Color color, bool is_3color) const {
std::array<Color, 4> colors = _interpolator->InterpolateBC1(result.low, result.high, is_3color);
Vector4Int result_vector = (Vector4Int)colors[2];
FindEndpointsSingleColor(block, color, is_3color);
FindEndpointsSingleColor(result, color, is_3color);
block.error = 0;
for (unsigned i = 0; i < 16; i++) {
result.error = 0;
for (int i = 0; i < 16; i++) {
Vector4Int pixel_vector = (Vector4Int)pixels.Get(i);
auto diff = pixel_vector - result_vector;
block.error += diff.SqrMag();
block.selectors[i] = 1;
result.error += diff.SqrMag();
result.selectors[i] = 1;
}
}
void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const BlockMetrics &metrics, EndpointMode endpoint_mode, bool ignore_black) const {
void BC1Encoder::FindEndpoints(EncodeResults &result, const CBlock &pixels, const BlockMetrics &metrics, EndpointMode endpoint_mode, bool ignore_black) const {
if (metrics.is_greyscale) {
// specialized greyscale case
const unsigned fr = pixels.Get(0).r;
const unsigned fr = pixels.Get(0, 0).r;
if (metrics.max.r - metrics.min.r < 2) {
// single color block
uint8_t fr5 = (uint8_t)scale8To5(fr);
uint8_t fr6 = (uint8_t)scale8To6(fr);
block.low = Color(fr5, fr6, fr5);
block.high = block.low;
result.low = Color(fr5, fr6, fr5);
result.high = result.low;
} else {
uint8_t lr5 = scale8To5(metrics.min.r);
uint8_t lr6 = scale8To6(metrics.min.r);
@ -497,8 +490,8 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
uint8_t hr5 = scale8To5(metrics.max.r);
uint8_t hr6 = scale8To6(metrics.max.r);
block.low = Color(lr5, lr6, lr5);
block.high = Color(hr5, hr6, hr5);
result.low = Color(lr5, lr6, lr5);
result.high = Color(hr5, hr6, hr5);
}
} else if (endpoint_mode == EndpointMode::LeastSquares) {
// 2D Least Squares approach from Humus's example, with added inset and optimal rounding.
@ -517,7 +510,7 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
std::array<unsigned, 3> sums_xy;
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
auto val = pixels.Get(i);
for (unsigned c = 0; c < 3; c++) { sums_xy[c] += val[chan0] * val[c]; }
}
@ -556,8 +549,8 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
h[c] = ((h[c] - inset) / 255.0f);
}
block.low = Color::PreciseRound565(l);
block.high = Color::PreciseRound565(h);
result.low = Color::PreciseRound565(l);
result.high = Color::PreciseRound565(h);
} else if (endpoint_mode == EndpointMode::BoundingBox) {
// Algorithm from icbc.h compress_dxt1_fast()
Vector4 l, h;
@ -575,7 +568,7 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
// Select the correct diagonal across the bounding box
int icov_xz = 0, icov_yz = 0;
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
int b = (int)pixels.Get(i).b - metrics.avg.b;
icov_xz += b * (int)pixels.Get(i).r - metrics.avg.r;
icov_yz += b * (int)pixels.Get(i).g - metrics.avg.g;
@ -584,8 +577,8 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
if (icov_xz < 0) std::swap(l[0], h[0]);
if (icov_yz < 0) std::swap(l[1], h[1]);
block.low = Color::PreciseRound565(l);
block.high = Color::PreciseRound565(h);
result.low = Color::PreciseRound565(l);
result.high = Color::PreciseRound565(h);
} else if (endpoint_mode == EndpointMode::BoundingBoxInt) {
// Algorithm from icbc.h compress_dxt1_fast(), but converted to integer.
@ -600,7 +593,7 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
}
int icov_xz = 0, icov_yz = 0;
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
int b = (int)pixels.Get(i).b - metrics.avg.b;
icov_xz += b * (int)pixels.Get(i).r - metrics.avg.r;
icov_yz += b * (int)pixels.Get(i).g - metrics.avg.g;
@ -609,8 +602,8 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
if (icov_xz < 0) std::swap(min.r, max.r);
if (icov_yz < 0) std::swap(min.g, max.g);
block.low = min.ScaleTo565();
block.high = max.ScaleTo565();
result.low = min.ScaleTo565();
result.high = max.ScaleTo565();
} else if (endpoint_mode == EndpointMode::PCA) {
// the slow way
// Select 2 colors along the principle axis. (There must be a faster/simpler way.)
@ -621,7 +614,7 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
Vector4 axis = {306, 601, 117}; // Luma vector
Matrix4x4 covariance = Matrix4x4::Identity();
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
auto val = pixels.Get(i);
if (ignore_black && val.IsBlack()) continue;
@ -657,9 +650,9 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
float min_dot = INFINITY;
float max_dot = -INFINITY;
unsigned min_index = 0, max_index = 0;
int min_index = 0, max_index = 0;
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
auto val = pixels.Get(i);
if (ignore_black && val.IsBlack()) continue;
@ -677,19 +670,19 @@ void BC1Encoder::FindEndpoints(Color4x4 pixels, EncodeResults &block, const Bloc
}
}
block.low = pixels.Get(min_index).ScaleTo565();
block.high = pixels.Get(max_index).ScaleTo565();
result.low = pixels.Get(min_index).ScaleTo565();
result.high = pixels.Get(max_index).ScaleTo565();
}
block.color_mode = ColorMode::Incomplete;
result.color_mode = ColorMode::Incomplete;
}
template <BC1Encoder::ColorMode M> void BC1Encoder::FindSelectors(Color4x4 &pixels, EncodeResults &block, ErrorMode error_mode) const {
template <BC1Encoder::ColorMode M> void BC1Encoder::FindSelectors(EncodeResults &result, const CBlock &pixels, ErrorMode error_mode) const {
assert(!((error_mode != ErrorMode::Full) && (bool)(M & ColorMode::ThreeColor)));
const int color_count = (unsigned)M & 0x0F;
std::array<Color, 4> colors = _interpolator->InterpolateBC1(block.low, block.high, color_count == 3);
std::array<Color, 4> colors = _interpolator->InterpolateBC1(result.low, result.high, color_count == 3);
std::array<Vector4Int, 4> color_vectors;
if (color_count == 4) {
@ -705,11 +698,11 @@ template <BC1Encoder::ColorMode M> void BC1Encoder::FindSelectors(Color4x4 &pixe
if (error_mode == ErrorMode::None || error_mode == ErrorMode::Faster) {
Vector4Int axis = color_vectors[3] - color_vectors[0];
std::array<int, 4> dots;
for (unsigned i = 0; i < 4; i++) { dots[i] = axis.Dot(color_vectors[i]); }
for (int i = 0; i < 4; i++) { dots[i] = axis.Dot(color_vectors[i]); }
int t0 = dots[0] + dots[1], t1 = dots[1] + dots[2], t2 = dots[2] + dots[3];
axis *= 2;
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
Vector4Int pixel_vector = Vector4Int::FromColorRGB(pixels.Get(i));
int dot = axis.Dot(pixel_vector);
uint8_t level = (dot <= t0) + (dot < t1) + (dot < t2);
@ -721,16 +714,16 @@ template <BC1Encoder::ColorMode M> void BC1Encoder::FindSelectors(Color4x4 &pixe
// llvm is just going to unswitch this anyways so its not an issue
auto diff = pixel_vector - color_vectors[selector];
total_error += diff.SqrMag();
if (i % 4 != 0 && total_error >= block.error) break; // check only once per row if we're generating too much error
if (i % 4 != 0 && total_error >= result.error) break; // check only once per row if we're generating too much error
}
block.selectors[i] = selector;
result.selectors[i] = selector;
}
} else if (error_mode == ErrorMode::Check2) {
Vector4Int axis = color_vectors[3] - color_vectors[0];
const float f = 4.0f / ((float)axis.SqrMag() + .00000125f);
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
Vector4Int pixel_vector = Vector4Int::FromColorRGB(pixels.Get(i));
auto diff = pixel_vector - color_vectors[0];
float sel_f = (float)diff.Dot(axis) * f + 0.5f;
@ -751,14 +744,14 @@ template <BC1Encoder::ColorMode M> void BC1Encoder::FindSelectors(Color4x4 &pixe
total_error += best_err;
if (total_error >= block.error) break;
if (total_error >= result.error) break;
block.selectors[i] = best_sel;
result.selectors[i] = best_sel;
}
} else if (error_mode == ErrorMode::Full) {
unsigned max_sel = (bool)(M == ColorMode::ThreeColor) ? 3 : 4;
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
unsigned best_error = UINT_MAX;
uint8_t best_sel = 0;
Vector4Int pixel_vector = Vector4Int::FromColorRGB(pixels.Get(i));
@ -774,31 +767,31 @@ template <BC1Encoder::ColorMode M> void BC1Encoder::FindSelectors(Color4x4 &pixe
}
total_error += best_error;
if (total_error >= block.error) { break; }
if (total_error >= result.error) { break; }
assert(best_sel < max_sel);
block.selectors[i] = best_sel;
result.selectors[i] = best_sel;
}
} else {
assert(false);
}
block.error = total_error;
block.color_mode = M;
result.error = total_error;
result.color_mode = M;
}
template <BC1Encoder::ColorMode M> bool BC1Encoder::RefineEndpointsLS(Color4x4 pixels, EncodeResults &block, BlockMetrics metrics) const {
template <BC1Encoder::ColorMode M> bool BC1Encoder::RefineEndpointsLS(EncodeResults &result, const CBlock &pixels, BlockMetrics metrics) const {
const int color_count = (unsigned)M & 0x0F;
static_assert(color_count == 3 || color_count == 4);
assert(block.color_mode != ColorMode::Incomplete);
assert(result.color_mode != ColorMode::Incomplete);
int denominator = color_count - 1;
Vector4 q00 = {0, 0, 0};
Vector4 matrix = Vector4(0);
for (unsigned i = 0; i < 16; i++) {
for (int i = 0; i < 16; i++) {
const Color color = pixels.Get(i);
const uint8_t sel = block.selectors[i];
const uint8_t sel = result.selectors[i];
if ((bool)(M & ColorMode::ThreeColorBlack) && color.IsBlack()) continue;
if ((bool)(M & ColorMode::ThreeColor) && sel == 3U) continue; // NOTE: selectors for 3-color are in linear order here, but not in original
@ -813,7 +806,7 @@ template <BC1Encoder::ColorMode M> bool BC1Encoder::RefineEndpointsLS(Color4x4 p
// invert matrix
float det = matrix.Determinant2x2(); // z00 * z11 - z01 * z10;
if (fabs(det) < 1e-8f) {
block.color_mode = ColorMode::Incomplete;
result.color_mode = ColorMode::Incomplete;
return false;
}
@ -826,16 +819,16 @@ template <BC1Encoder::ColorMode M> bool BC1Encoder::RefineEndpointsLS(Color4x4 p
Vector4 low = (matrix[0] * q00) + (matrix[1] * q10);
Vector4 high = (matrix[2] * q00) + (matrix[3] * q10);
block.color_mode = M;
block.low = Color::PreciseRound565(low);
block.high = Color::PreciseRound565(high);
result.color_mode = M;
result.low = Color::PreciseRound565(low);
result.high = Color::PreciseRound565(high);
return true;
}
template <BC1Encoder::ColorMode M> void BC1Encoder::RefineEndpointsLS(std::array<Vector4, 17> &sums, EncodeResults &block, Vector4 &matrix, Hash hash) const {
template <BC1Encoder::ColorMode M> void BC1Encoder::RefineEndpointsLS(EncodeResults &result, std::array<Vector4, 17> &sums, Vector4 &matrix, Hash hash) const {
const int color_count = (unsigned)M & 0x0F;
static_assert(color_count == 3 || color_count == 4);
assert(block.color_mode != ColorMode::Incomplete);
assert(result.color_mode != ColorMode::Incomplete);
int denominator = color_count - 1;
@ -852,30 +845,30 @@ template <BC1Encoder::ColorMode M> void BC1Encoder::RefineEndpointsLS(std::array
Vector4 low = (matrix[0] * q00) + (matrix[1] * q10);
Vector4 high = (matrix[2] * q00) + (matrix[3] * q10);
block.color_mode = M;
block.low = Color::PreciseRound565(low);
block.high = Color::PreciseRound565(high);
result.color_mode = M;
result.low = Color::PreciseRound565(low);
result.high = Color::PreciseRound565(high);
}
template <BC1Encoder::ColorMode M>
void BC1Encoder::RefineBlockLS(Color4x4 &pixels, EncodeResults &block, BlockMetrics &metrics, ErrorMode error_mode, unsigned passes) const {
void BC1Encoder::RefineBlockLS(EncodeResults &result, const CBlock &pixels, const BlockMetrics &metrics, ErrorMode error_mode, unsigned passes) const {
assert(error_mode != ErrorMode::None || passes == 1);
for (unsigned pass = 0; pass < passes; pass++) {
EncodeResults trial_result = block;
EncodeResults trial_result = result;
Vector4 low, high;
bool multicolor = RefineEndpointsLS<ColorMode::FourColor>(pixels, trial_result, metrics);
bool multicolor = RefineEndpointsLS<ColorMode::FourColor>(trial_result, pixels, metrics);
if (!multicolor) {
FindEndpointsSingleColor(trial_result, pixels, metrics.avg, (M != ColorMode::FourColor));
} else {
FindSelectors<M>(pixels, trial_result, error_mode);
FindSelectors<M>(trial_result, pixels, error_mode);
}
if (trial_result.low == block.low && trial_result.high == block.high) break;
if (trial_result.low == result.low && trial_result.high == result.high) break;
if (error_mode == ErrorMode::None || trial_result.error < block.error) {
block = trial_result;
if (error_mode == ErrorMode::None || trial_result.error < result.error) {
result = trial_result;
} else {
return;
}
@ -883,15 +876,15 @@ void BC1Encoder::RefineBlockLS(Color4x4 &pixels, EncodeResults &block, BlockMetr
}
template <BC1Encoder::ColorMode M>
void BC1Encoder::RefineBlockCF(Color4x4 &pixels, EncodeResults &block, BlockMetrics &metrics, ErrorMode error_mode, unsigned orderings) const {
void BC1Encoder::RefineBlockCF(EncodeResults &result, const CBlock &pixels, const BlockMetrics &metrics, ErrorMode error_mode, unsigned orderings) const {
const int color_count = (unsigned)M & 0x0F;
static_assert(color_count == 3 || color_count == 4);
assert(block.color_mode != ColorMode::Incomplete);
assert(result.color_mode != ColorMode::Incomplete);
using OrderTable = OrderTable<color_count>;
using Hist = Histogram<color_count>;
EncodeResults orig = block;
EncodeResults orig = result;
Hist h = Hist(orig.selectors);
Hash start_hash = OrderTable::GetHash(h);
@ -900,11 +893,11 @@ void BC1Encoder::RefineBlockCF(Color4x4 &pixels, EncodeResults &block, BlockMetr
std::array<Vector4, 16> color_vectors;
std::array<uint32_t, 16> dots;
for (unsigned i = 0; i < 16; i++) {
color_vectors[i] = Vector4::FromColorRGB(pixels.Get(i));
int dot = 0x1000000 + (int)color_vectors[i].Dot(axis);
for (int i = 0; i < 16; i++) {
color_vectors[(unsigned)i] = Vector4::FromColorRGB(pixels.Get(i));
int dot = 0x1000000 + (int)color_vectors[(unsigned)i].Dot(axis);
assert(dot >= 0);
dots[i] = (uint32_t)(dot << 4) | i;
dots[(unsigned)i] = (uint32_t)(dot << 4) | i;
}
std::sort(dots.begin(), dots.end());
@ -926,17 +919,17 @@ void BC1Encoder::RefineBlockCF(Color4x4 &pixels, EncodeResults &block, BlockMetr
if (OrderTable::IsSingleColor(trial_hash)) {
FindEndpointsSingleColor(trial_result, pixels, metrics.avg, (color_count == 3));
} else {
RefineEndpointsLS<M>(sums, trial_result, trial_matrix, trial_hash);
FindSelectors<M>(pixels, trial_result, error_mode);
RefineEndpointsLS<M>(trial_result, sums, trial_matrix, trial_hash);
FindSelectors<M>(trial_result, pixels, error_mode);
}
if (trial_result.error < block.error) { block = trial_result; }
if (trial_result.error < result.error) { result = trial_result; }
if (trial_result.error == 0) break;
}
}
void BC1Encoder::EndpointSearch(Color4x4 &pixels, EncodeResults &block) const {
if (block.solid) return;
void BC1Encoder::EndpointSearch(EncodeResults &result, const CBlock &pixels) const {
if (result.solid) return;
static const std::array<Vector4Int, 16> Voxels = {{
{1, 0, 0, 3}, // 0
@ -967,7 +960,7 @@ void BC1Encoder::EndpointSearch(Color4x4 &pixels, EncodeResults &block) const {
if ((int)(i & 31) == forbidden_direction) continue;
Vector4Int delta = Voxels[voxel_index];
EncodeResults trial_result = block;
EncodeResults trial_result = result;
if (i & 16) {
trial_result.low.r = (uint8_t)clamp(trial_result.low.r + delta[0], 0, 31);
@ -979,21 +972,21 @@ void BC1Encoder::EndpointSearch(Color4x4 &pixels, EncodeResults &block) const {
trial_result.high.b = (uint8_t)clamp(trial_result.high.b + delta[2], 0, 31);
}
switch (block.color_mode) {
switch (result.color_mode) {
default:
case ColorMode::FourColor:
FindSelectors<ColorMode::FourColor>(pixels, trial_result, _error_mode);
FindSelectors<ColorMode::FourColor>(trial_result, pixels, _error_mode);
break;
case ColorMode::ThreeColor:
FindSelectors<ColorMode::ThreeColor>(pixels, trial_result, ErrorMode::Full);
FindSelectors<ColorMode::ThreeColor>(trial_result, pixels, ErrorMode::Full);
break;
case ColorMode::ThreeColorBlack:
FindSelectors<ColorMode::ThreeColorBlack>(pixels, trial_result, ErrorMode::Full);
FindSelectors<ColorMode::ThreeColorBlack>(trial_result, pixels, ErrorMode::Full);
break;
}
if (trial_result.error < block.error) {
block = trial_result;
if (trial_result.error < result.error) {
result = trial_result;
forbidden_direction = delta[3] | (int)(i & 16);
prev_improvement_index = i;

View File

@ -25,11 +25,11 @@
#include <cstdint>
#include <memory>
#include <tuple>
#include <type_traits>
#include "../../BlockEncoder.h"
#include "../../BlockView.h"
#include "../../Color.h"
#include "../../ColorBlock.h"
#include "../../Encoder.h"
#include "../../Texture.h"
#include "../interpolator/Interpolator.h"
#include "BC1Block.h"
#include "SingleColorTable.h"
@ -40,13 +40,14 @@ class Vector4;
namespace quicktex::s3tc {
class BC1Encoder final : public BlockEncoderTemplate<BC1Block, 4, 4> {
class BC1Encoder final : public BlockEncoder<BlockTexture<BC1Block>> {
public:
using InterpolatorPtr = std::shared_ptr<Interpolator>;
using OrderingPair = std::tuple<unsigned, unsigned>;
using CBlock = ColorBlock<4, 4>;
inline static constexpr unsigned min_power_iterations = 4;
inline static constexpr unsigned max_power_iterations = 10;
static constexpr unsigned min_power_iterations = 4;
static constexpr unsigned max_power_iterations = 10;
enum class ColorMode {
// An incomplete block with invalid selectors or endpoints
@ -130,13 +131,13 @@ class BC1Encoder final : public BlockEncoderTemplate<BC1Block, 4, 4> {
void SetPowerIterations(unsigned power_iters);
// Public Methods
void EncodeBlock(Color4x4 pixels, BC1Block *dest) const override;
BC1Block EncodeBlock(const CBlock &pixels) const override;
virtual size_t MTThreshold() const override { return 16; }
private:
using Hash = uint16_t;
using BlockMetrics = Color4x4::BlockMetrics;
using BlockMetrics = CBlock::Metrics;
// Unpacked BC1 block with metadata
struct EncodeResults {
@ -168,23 +169,25 @@ class BC1Encoder final : public BlockEncoderTemplate<BC1Block, 4, 4> {
unsigned _orderings4;
unsigned _orderings3;
void WriteBlockSolid(Color color, BC1Block *dest) const;
void WriteBlock(EncodeResults &block, BC1Block *dest) const;
BC1Block WriteBlockSolid(Color color) const;
BC1Block WriteBlock(EncodeResults &result) const;
void FindEndpoints(Color4x4 pixels, EncodeResults &block, const BlockMetrics &metrics, EndpointMode endpoint_mode, bool ignore_black = false) const;
void FindEndpointsSingleColor(EncodeResults &block, Color color, bool is_3color = false) const;
void FindEndpointsSingleColor(EncodeResults &block, Color4x4 &pixels, Color color, bool is_3color) const;
void FindEndpoints(EncodeResults &result, const CBlock &pixels, const BlockMetrics &metrics, EndpointMode endpoint_mode, bool ignore_black = false) const;
void FindEndpointsSingleColor(EncodeResults &result, Color color, bool is_3color = false) const;
void FindEndpointsSingleColor(EncodeResults &result, const CBlock &pixels, Color color, bool is_3color) const;
template <ColorMode M> void FindSelectors(Color4x4 &pixels, EncodeResults &block, ErrorMode error_mode) const;
template <ColorMode M> void FindSelectors(EncodeResults &result, const CBlock &pixels, ErrorMode error_mode) const;
template <ColorMode M> bool RefineEndpointsLS(Color4x4 pixels, EncodeResults &block, BlockMetrics metrics) const;
template <ColorMode M> bool RefineEndpointsLS(EncodeResults &result, const CBlock &pixels, BlockMetrics metrics) const;
template <ColorMode M> void RefineEndpointsLS(std::array<Vector4, 17> &sums, EncodeResults &block, Vector4 &matrix, Hash hash) const;
template <ColorMode M> void RefineEndpointsLS(EncodeResults &result, std::array<Vector4, 17> &sums, Vector4 &matrix, Hash hash) const;
template <ColorMode M> void RefineBlockLS(Color4x4 &pixels, EncodeResults &block, BlockMetrics &metrics, ErrorMode error_mode, unsigned passes) const;
template <ColorMode M>
void RefineBlockLS(EncodeResults &result, const CBlock &pixels, const BlockMetrics &metrics, ErrorMode error_mode, unsigned passes) const;
template <ColorMode M> void RefineBlockCF(Color4x4 &pixels, EncodeResults &block, BlockMetrics &metrics, ErrorMode error_mode, unsigned orderings) const;
template <ColorMode M>
void RefineBlockCF(EncodeResults &result, const CBlock &pixels, const BlockMetrics &metrics, ErrorMode error_mode, unsigned orderings) const;
void EndpointSearch(Color4x4 &pixels, EncodeResults &block) const;
void EndpointSearch(EncodeResults &result, const CBlock &pixels) const;
};
} // namespace quicktex::s3tc

View File

@ -27,11 +27,12 @@
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <type_traits>
#include "../../Vector4.h"
#include "Histogram.h"
namespace quicktex::s3tc {
namespace quicktex::s3tc {
template <size_t N> class OrderTable {
public:
static constexpr unsigned HashCount = 1 << ((N - 1) * 4); // 16**(N-1)

View File

@ -17,7 +17,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../../_bindings.h"
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <array>
#include <cstddef>
@ -25,8 +28,8 @@
#include <stdexcept>
#include <string>
#include "../../BlockDecoder.h"
#include "../../BlockEncoder.h"
#include "../../Decoder.h"
#include "../../Encoder.h"
#include "../interpolator/Interpolator.h"
#include "BC1Decoder.h"
#include "BC1Encoder.h"
@ -41,19 +44,45 @@ using InterpolatorPtr = std::shared_ptr<Interpolator>;
void InitBC1(py::module_ &s3tc) {
auto bc1 = s3tc.def_submodule("_bc1", "internal bc1 module");
auto block_encoder = py::type::of<BlockEncoder>();
auto block_decoder = py::type::of<BlockDecoder>();
py::options options;
options.disable_function_signatures();
// region BC1Block
auto bc1_block = BindBlock<BC1Block>(bc1, "BC1Block");
bc1_block.doc() = "A single BC1 block.";
// BC1Encoder
py::class_<BC1Encoder> bc1_encoder(bc1, "BC1Encoder", block_encoder, R"doc(
Base: :py:class:`~quicktex.BlockEncoder`
bc1_block.def(py::init<>());
bc1_block.def(py::init<Color, Color, BC1Block::SelectorArray>(), "color0"_a, "color1"_a, "selectors"_a, R"doc(
Create a new BC1Block with the specified endpoints and selectors
Encodes RGB textures to BC1.
:param color0: The first endpoint
:param color1: The second endpoint
:param selectors: the selectors as a 4x4 list of integers, between 0 and 3 inclusive.
)doc");
bc1_block.def_property("endpoints", &BC1Block::GetColors, &BC1Block::SetColors, "The block's endpoint colors as a 2-tuple.");
bc1_block.def_property("selectors", &BC1Block::GetSelectors, &BC1Block::SetSelectors, R"doc(
The block's selectors as a 4x4 list of integers between 0 and 3 inclusive.
.. note::
This is a property, so directly modifying its value will not propogate back to the block.
Instead you must read, modify, then write the new list back to the property, like so::
selectors = block.selectors
selectors[0,0] = 0
block.selectors = selectors
)doc");
bc1_block.def_property_readonly("is_3color", &BC1Block::Is3Color, R"doc(
"True if the block uses 3-color interpolation, i.e. color0 <= color1. This value should be ignored when decoding as part of a BC3 block. Readonly.
)doc");
// endregion
// region BC1Texture
auto bc1_texture = BindBlockTexture<BC1Block>(bc1, "BC1Texture");
bc1_texture.doc() = "A texture comprised of BC1 blocks.";
// endregion
// region BC1Encoder
py::class_<BC1Encoder> bc1_encoder(bc1, "BC1Encoder", "Encodes RGB textures to BC1.");
py::enum_<BC1Encoder::EndpointMode>(bc1_encoder, "EndpointMode", "Enum representing various methods of finding endpoints in a block.")
.value("LeastSquares", BC1Encoder::EndpointMode::LeastSquares, "Find endpoints using a 2D least squares approach.")
.value("BoundingBox", BC1Encoder::EndpointMode::BoundingBox, "Find endpoints using a simple bounding box. Fast but inaccurate.")
@ -74,24 +103,27 @@ void InitBC1(py::module_ &s3tc) {
"when using a BC1 texture.");
bc1_encoder.def(py::init<unsigned, BC1Encoder::ColorMode>(), "level"_a = 5, "color_mode"_a = BC1Encoder::ColorMode::FourColor);
bc1_encoder.def(py::init<unsigned, BC1Encoder::ColorMode, InterpolatorPtr>(), "level"_a, "color_mode"_a, "interpolator"_a, R"pbdoc(
__init__(self, level: int = 5, color_mode=ColorMode.FourColor, interpolator=Interpolator()) -> None
bc1_encoder.def(py::init<unsigned, BC1Encoder::ColorMode, InterpolatorPtr>(), "level"_a, "color_mode"_a, "interpolator"_a, R"doc(
Create a new BC1 encoder with the specified preset level, color mode, and interpolator.
:param int level: The preset level of the resulting encoder, between 0 and 18 inclusive. See :py:meth:`set_level` for more information. Default: 5.
:param ColorMode color_mode: The color mode of the resulting BC1Encoder. Default: :py:class:`~quicktex.s3tc.bc1.BC1Encoder.ColorMode.FourColor`.
:param Interpolator interpolator: The interpolation mode to use for encoding. Default: :py:class:`~quicktex.s3tc.interpolator.Interpolator`.
)pbdoc");
)doc");
bc1_encoder.def("set_level", &BC1Encoder::SetLevel, "level"_a, R"pbdoc(
set_level(self, level : int = 5) -> None
bc1_encoder.def("encode", &BC1Encoder::Encode, "texture"_a, R"doc(
Encode a raw texture into a new BC1Texture using the encoder's current settings.
:param RawTexture texture: Input texture to encode.
:returns: A new BC1Texture with the same dimension as the input.
)doc");
bc1_encoder.def("set_level", &BC1Encoder::SetLevel, "level"_a, R"doc(
Select a preset quality level, between 0 and 18 inclusive. Higher quality levels are slower, but produce blocks that are a closer match to input.
This has no effect on the size of the resulting texture, since BC1 is a fixed-ratio compression method. For better control, see the advanced API below
:param int level: The preset level of the resulting encoder, between 0 and 18 inclusive. Default: 5.
)pbdoc");
)doc");
bc1_encoder.def_property_readonly("interpolator", &BC1Encoder::GetInterpolator,
"The :py:class:`~quicktex.s3tc.interpolator.Interpolator` used by this encoder. This is a readonly property.");
@ -131,25 +163,30 @@ void InitBC1(py::module_ &s3tc) {
bc1_encoder.def_property("power_iterations", &BC1Encoder::GetPowerIterations, &BC1Encoder::SetPowerIterations,
"Number of power iterations used with the PCA endpoint mode. Value should be around 4 to 6. "
"Automatically clamped to between :py:const:`BC1Encoder.min_power_iterations` and :py:const:`BC1Encoder.max_power_iterations`");
// endregion
// BC1Decoder
py::class_<BC1Decoder> bc1_decoder(bc1, "BC1Decoder", block_decoder, R"doc(
Base: :py:class:`~quicktex.BlockDecoder`
// region BC1Decoder
py::class_<BC1Decoder> bc1_decoder(bc1, "BC1Decoder", R"doc(
Decodes BC1 textures to RGB
)doc");
bc1_decoder.def(py::init<bool>(), "write_alpha"_a = false);
bc1_decoder.def(py::init<bool, InterpolatorPtr>(), "write_alpha"_a, "interpolator"_a, R"pbdoc(
__init__(self, interpolator = Interpolator()) -> None
bc1_decoder.def(py::init<bool, InterpolatorPtr>(), "write_alpha"_a, "interpolator"_a, R"doc(
Create a new BC1 decoder with the specificied interpolator.
:param bool write_alpha: Determines if the alpha channel of the output is written to. Default: False;
:param Interpolator interpolator: The interpolation mode to use for decoding. Default: :py:class:`~quicktex.s3tc.interpolator.Interpolator`.
)pbdoc");
)doc");
bc1_decoder.def("decode", &BC1Decoder::Decode, "texture"_a, R"doc(
Decode a BC1 texture into a new RawTexture using the decoder's current settings.
:param RawTexture texture: Input texture to encode.
:returns: A new RawTexture with the same dimensions as the input
)doc");
bc1_decoder.def_property_readonly("interpolator", &BC1Decoder::GetInterpolator, "The interpolator used by this decoder. This is a readonly property.");
bc1_decoder.def_readwrite("write_alpha", &BC1Decoder::write_alpha, "Determines if the alpha channel of the output is written to.");
// endregion
}
} // namespace quicktex::bindings

View File

@ -19,16 +19,44 @@
#pragma once
#include <utility>
#include "../bc1/BC1Block.h"
#include "../bc4/BC4Block.h"
namespace quicktex::s3tc {
namespace quicktex::s3tc {
#pragma pack(push, 1)
class BC3Block {
class alignas(8) BC3Block {
public:
static constexpr int Width = 4;
static constexpr int Height = 4;
using BlockPair = std::pair<BC4Block, BC1Block>;
BC4Block alpha_block;
BC1Block color_block;
constexpr BC3Block() {
static_assert(sizeof(BC3Block) == 16);
static_assert(sizeof(std::array<BC3Block, 10>) == 16 * 10);
static_assert(alignof(BC3Block) >= 8);
alpha_block = BC4Block();
color_block = BC1Block();
}
BC3Block(const BC4Block &alpha, const BC1Block &color) {
alpha_block = alpha;
color_block = color;
}
BlockPair GetBlocks() const { return BlockPair(alpha_block, color_block); }
void SetBlocks(const BlockPair &blocks) {
alpha_block = blocks.first;
color_block = blocks.second;
}
bool operator==(const BC3Block& other) const = default;
bool operator!=(const BC3Block& other) const = default;
};
#pragma pack(pop)
} // namespace quicktex::s3tc

View File

@ -19,16 +19,16 @@
#include "BC3Decoder.h"
#include <type_traits>
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "BC3Block.h"
namespace quicktex::s3tc {
namespace quicktex::s3tc {
void BC3Decoder::DecodeBlock(Color4x4 dest, BC3Block *const block) const noexcept(ndebug) {
_bc1_decoder->DecodeBlock(dest, &(block->color_block), false);
_bc4_decoder->DecodeBlock(dest, &(block->alpha_block));
ColorBlock<4, 4> BC3Decoder::DecodeBlock(const BC3Block &block) const {
auto output = _bc1_decoder->DecodeBlock(block.color_block, false);
_bc4_decoder->DecodeInto(output, block.alpha_block);
return output;
}
} // namespace quicktex::s3tc

View File

@ -21,9 +21,9 @@
#include <memory>
#include "../../BlockDecoder.h"
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "../../Decoder.h"
#include "../../Texture.h"
#include "../bc1/BC1Decoder.h"
#include "../bc4/BC4Decoder.h"
#include "../interpolator/Interpolator.h"
@ -31,7 +31,7 @@
namespace quicktex::s3tc {
class BC3Decoder : public BlockDecoderTemplate<BC3Block, 4, 4> {
class BC3Decoder : public BlockDecoder<BlockTexture<BC3Block>> {
public:
using BC1DecoderPtr = std::shared_ptr<BC1Decoder>;
using BC4DecoderPtr = std::shared_ptr<BC4Decoder>;
@ -41,7 +41,7 @@ class BC3Decoder : public BlockDecoderTemplate<BC3Block, 4, 4> {
BC3Decoder() : BC3Decoder(std::make_shared<Interpolator>()) {}
void DecodeBlock(Color4x4 dest, BC3Block *const block) const noexcept(ndebug) override;
ColorBlock<4, 4> DecodeBlock(const BC3Block &block) const override;
BC1DecoderPtr GetBC1Decoder() const { return _bc1_decoder; }
BC4DecoderPtr GetBC4Decoder() const { return _bc4_decoder; }

View File

@ -19,12 +19,16 @@
#include "BC3Encoder.h"
#include "../../BlockView.h"
#include "../../ColorBlock.h"
#include "../bc1/BC1Block.h"
#include "../bc4/BC4Block.h"
#include "BC3Block.h"
namespace quicktex::s3tc {
void BC3Encoder::EncodeBlock(Color4x4 pixels, BC3Block *dest) const {
_bc1_encoder->EncodeBlock(pixels, &(dest->color_block));
_bc4_encoder->EncodeBlock(pixels, &(dest->alpha_block));
BC3Block BC3Encoder::EncodeBlock(const ColorBlock<4, 4> &pixels) const {
auto output = BC3Block();
output.color_block = _bc1_encoder->EncodeBlock(pixels);
output.alpha_block = _bc4_encoder->EncodeBlock(pixels);
return output;
}
} // namespace quicktex::s3tc

View File

@ -21,8 +21,9 @@
#include <memory>
#include "../../BlockEncoder.h"
#include "../../BlockView.h"
#include "../../ColorBlock.h"
#include "../../Encoder.h"
#include "../../Texture.h"
#include "../bc1/BC1Encoder.h"
#include "../bc4/BC4Encoder.h"
#include "../interpolator/Interpolator.h"
@ -30,7 +31,7 @@
namespace quicktex::s3tc {
class BC3Encoder : public BlockEncoderTemplate<BC3Block, 4, 4> {
class BC3Encoder : public BlockEncoder<BlockTexture<BC3Block>> {
public:
using BC1EncoderPtr = std::shared_ptr<BC1Encoder>;
using BC4EncoderPtr = std::shared_ptr<BC4Encoder>;
@ -41,7 +42,7 @@ class BC3Encoder : public BlockEncoderTemplate<BC3Block, 4, 4> {
BC3Encoder(unsigned level = 5) : BC3Encoder(level, std::make_shared<Interpolator>()) {}
void EncodeBlock(Color4x4 pixels, BC3Block *dest) const override;
BC3Block EncodeBlock(const ColorBlock<4, 4>& pixels) const override;
BC1EncoderPtr GetBC1Encoder() const { return _bc1_encoder; }
BC4EncoderPtr GetBC4Encoder() const { return _bc4_encoder; }

View File

@ -17,6 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../../_bindings.h"
#include <pybind11/pybind11.h>
#include <array>
@ -25,8 +27,8 @@
#include <stdexcept>
#include <string>
#include "../../BlockDecoder.h"
#include "../../BlockEncoder.h"
#include "../../Decoder.h"
#include "../../Encoder.h"
#include "../interpolator/Interpolator.h"
#include "BC3Decoder.h"
#include "BC3Encoder.h"
@ -42,22 +44,36 @@ using BC1DecoderPtr = std::shared_ptr<BC1Decoder>;
void InitBC3(py::module_ &s3tc) {
auto bc3 = s3tc.def_submodule("_bc3", "internal bc3 module");
auto block_encoder = py::type::of<BlockEncoder>();
auto block_decoder = py::type::of<BlockDecoder>();
py::options options;
options.disable_function_signatures();
// BC3Encoder
py::class_<BC3Encoder> bc3_encoder(bc3, "BC3Encoder", block_encoder,R"doc(
Base: :py:class:`~quicktex.BlockEncoder`
// region BC3Block
auto bc3_block = BindBlock<BC3Block>(bc3, "BC3Block");
bc3_block.doc() = "A single BC3 block.";
bc3_block.def(py::init<>());
bc3_block.def(py::init<BC4Block, BC1Block>(), "alpha_block"_a, "color_block"_a, R"doc(
Create a new BC3Block out of a BC4 block and a BC1 block.
:param BC4Block alpha_block: The BC4 block used for alpha data.
:param BC1Block color_block: The BC1 block used for RGB data.
)doc");
bc3_block.def_readwrite("alpha_block", &BC3Block::alpha_block, "The BC4 block used for alpha data.");
bc3_block.def_readwrite("color_block", &BC3Block::color_block, "The BC1 block used for rgb data.");
bc3_block.def_property("blocks", &BC3Block::GetBlocks, &BC3Block::SetBlocks, "The BC4 and BC1 blocks that make up this block as a 2-tuple.");
// endregion
// region BC3Texture
auto bc3_texture = BindBlockTexture<BC3Block>(bc3, "BC3Texture");
bc3_texture.doc() = "A texture comprised of BC3 blocks.";
// endregion
// region BC3Encoder
py::class_<BC3Encoder> bc3_encoder(bc3, "BC3Encoder", R"doc(
Encodes RGBA textures to BC3
)doc");
bc3_encoder.def(py::init<unsigned>(), "level"_a = 5);
bc3_encoder.def(py::init<unsigned, InterpolatorPtr>(), "level"_a, "interpolator"_a, R"doc(
__init__(self, level: int = 5, interpolator=Interpolator()) -> None
Create a new BC3 encoder with the specified preset level and interpolator.
:param int level: The preset level of the resulting encoder, between 0 and 18 inclusive.
@ -65,30 +81,42 @@ void InitBC3(py::module_ &s3tc) {
:param Interpolator interpolator: The interpolation mode to use for encoding. Default: :py:class:`~quicktex.s3tc.interpolator.Interpolator`.
)doc");
bc3_encoder.def("encode", &BC3Encoder::Encode, "texture"_a, R"doc(
Encode a raw texture into a new BC3Texture using the encoder's current settings.
:param RawTexture texture: Input texture to encode.
:returns: A new BC3Texture with the same dimension as the input.
)doc");
bc3_encoder.def_property_readonly("bc1_encoder", &BC3Encoder::GetBC1Encoder,
"Internal :py:class:`~quicktex.s3tc.bc1.BC1Encoder` used for RGB data. Readonly.");
bc3_encoder.def_property_readonly("bc4_encoder", &BC3Encoder::GetBC4Encoder,
"Internal :py:class:`~quicktex.s3tc.bc4.BC4Encoder` used for alpha data. Readonly.");
// endregion
// BC3Decoder
py::class_<BC3Decoder> bc3_decoder(bc3, "BC3Decoder", block_decoder, R"doc(
Base: :py:class:`~quicktex.BlockDecoder`
// region BC3Decoder
py::class_<BC3Decoder> bc3_decoder(bc3, "BC3Decoder", R"doc(
Decodes BC3 textures to RGBA
)doc");
bc3_decoder.def(py::init<>());
bc3_decoder.def(py::init<InterpolatorPtr>(), "interpolator"_a, R"doc(
__init__(interpolator = Interpolator()) -> None
Create a new BC3 decoder with the specified interpolator.
:param Interpolator interpolator: The interpolation mode to use for decoding. Default: :py:class:`~quicktex.s3tc.interpolator.Interpolator`.
)doc");
bc3_decoder.def("decode", &BC3Decoder::Decode, "texture"_a, R"doc(
Decode a BC3 texture into a new RawTexture using the decoder's current settings.
:param RawTexture texture: Input texture to encode.
:returns: A new RawTexture with the same dimensions as the input
)doc");
bc3_decoder.def_property_readonly("bc1_decoder", &BC3Decoder::GetBC1Decoder,
"Internal :py:class:`~quicktex.s3tc.bc1.BC1Decoder` used for RGB data. Readonly.");
bc3_decoder.def_property_readonly("bc4_decoder", &BC3Decoder::GetBC4Decoder,
"Internal :py:class:`~quicktex.s3tc.bc4.BC4Decoder` used for alpha data. Readonly.");
};
// endregion
}
} // namespace quicktex::bindings

View File

@ -0,0 +1,66 @@
/* Quicktex Texture Compression Library
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
Partially derived from rgbcx.h written by Richard Geldreich <richgel99@gmail.com>
and licenced under the public domain
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "BC4Block.h"
#include <stdexcept>
#include <algorithm>
#include "../../util.h"
namespace quicktex::s3tc {
BC4Block::SelectorArray BC4Block::GetSelectors() const {
auto packed = Pack<uint8_t, uint64_t, 8, SelectorSize>(_selectors);
auto rows = Unpack<uint64_t, uint16_t, SelectorBits * Width, Height>(packed);
return MapArray(rows, Unpack<uint16_t, uint8_t, SelectorBits, Width>);
}
void BC4Block::SetSelectors(const BC4Block::SelectorArray& unpacked) {
for (unsigned y = 0; y < (unsigned)Height; y++) {
if (std::any_of(unpacked[y].begin(), unpacked[y].end(), [](uint8_t i) { return i > SelectorMax; }))
throw std::invalid_argument("Selector value out of bounds.");
}
auto rows = MapArray(unpacked, Pack<uint8_t, uint16_t, SelectorBits, Width>);
auto packed = Pack<uint16_t, uint64_t, SelectorBits * Width, Height>(rows);
_selectors = Unpack<uint64_t, uint8_t, 8, SelectorSize>(packed);
}
std::array<uint8_t, 8> BC4Block::GetValues6() const {
return {alpha0,
alpha1,
static_cast<uint8_t>((alpha0 * 4 + alpha1) / 5),
static_cast<uint8_t>((alpha0 * 3 + alpha1 * 2) / 5),
static_cast<uint8_t>((alpha0 * 2 + alpha1 * 3) / 5),
static_cast<uint8_t>((alpha0 + alpha1 * 4) / 5),
0,
0xFF};
}
std::array<uint8_t, 8> BC4Block::GetValues8() const {
return {alpha0,
alpha1,
static_cast<uint8_t>((alpha0 * 6 + alpha1) / 7),
static_cast<uint8_t>((alpha0 * 5 + alpha1 * 2) / 7),
static_cast<uint8_t>((alpha0 * 4 + alpha1 * 3) / 7),
static_cast<uint8_t>((alpha0 * 3 + alpha1 * 4) / 7),
static_cast<uint8_t>((alpha0 * 2 + alpha1 * 5) / 7),
static_cast<uint8_t>((alpha0 + alpha1 * 6) / 7)};
}
} // namespace quicktex::s3tc

View File

@ -20,103 +20,90 @@
#pragma once
#include <array>
#include <cassert>
#include <cstdint>
#include <cstdlib>
#include <utility>
#include "../../Color.h"
#include "../../util.h"
#include "../bc1/BC1Block.h"
namespace quicktex::s3tc {
namespace quicktex::s3tc {
#pragma pack(push, 1)
class BC4Block {
class alignas(8) BC4Block {
public:
using UnpackedSelectors = std::array<std::array<uint8_t, 4>, 4>;
static constexpr size_t Width = 4;
static constexpr size_t Height = 4;
inline uint32_t GetLowAlpha() const { return low_alpha; }
inline uint32_t GetHighAlpha() const { return high_alpha; }
inline bool Is6Alpha() const { return GetLowAlpha() <= GetHighAlpha(); }
static constexpr size_t SelectorSize = 6; // size of selector array in bytes
static constexpr size_t SelectorBits = 3; // size of a selector in bits
static constexpr size_t SelectorMax = (1 << SelectorBits) - 1; // maximum value of a selector
inline uint64_t GetSelectorBits() const {
auto packed = Pack<uint8_t, uint64_t, 8, 6>(selectors);
assert(packed <= SelectorBitsMax);
return packed;
using SelectorArray = std::array<std::array<uint8_t, Width>, Height>;
using AlphaPair = std::pair<uint8_t, uint8_t>;
uint8_t alpha0; // first endpoint
uint8_t alpha1; // second endpoint
private:
std::array<uint8_t, SelectorSize> _selectors; // internal array of selector bytes
public:
// Constructors
/// Create a new BC4Block
constexpr BC4Block() {
static_assert(sizeof(BC4Block) == 8);
static_assert(sizeof(std::array<BC4Block, 10>) == 8 * 10);
static_assert(alignof(BC4Block) >= 8);
alpha0 = alpha1 = 0;
_selectors = {0, 0, 0, 0, 0, 0};
}
void SetSelectorBits(uint64_t packed) {
assert(packed <= SelectorBitsMax);
selectors = Unpack<uint64_t, uint8_t, 8, 6>(packed);
/**
* Create a new BC4Block
* @param valpha0 first endpoint value
* @param valpha1 second endpoint value
* @param selectors the selectors as a 4x4 array of integers, between 0 and 7 inclusive.
*/
BC4Block(uint8_t valpha0, uint8_t valpha1, const SelectorArray& selectors) {
alpha0 = valpha0;
alpha1 = valpha1;
SetSelectors(selectors);
}
UnpackedSelectors UnpackSelectors() const {
UnpackedSelectors unpacked;
auto rows = Unpack<uint64_t, uint16_t, 12, 4>(GetSelectorBits());
for (unsigned i = 0; i < 4; i++) {
auto row = Unpack<uint16_t, uint8_t, SelectorBits, 4>(rows[i]);
unpacked[i] = row;
}
return unpacked;
/**
* Create a new solid BC4Block
* @param alpha first endpoint value
*/
BC4Block(uint8_t alpha) {
alpha0 = alpha;
alpha1 = alpha;
_selectors.fill(0);
}
void PackSelectors(const UnpackedSelectors& unpacked) {
std::array<uint16_t, 4> rows;
for (unsigned i = 0; i < 4; i++) { rows[i] = Pack<uint8_t, uint16_t, SelectorBits, 4>(unpacked[i]); }
auto packed = Pack<uint16_t, uint64_t, 12, 4>(rows);
SetSelectorBits(packed);
/// Get a alpha0 and alpha1 as a pair
AlphaPair GetAlphas() const { return AlphaPair(alpha0, alpha1); }
/// Set alpha0 and alpha1 as a pair
void SetAlphas(AlphaPair as) {
alpha0 = as.first;
alpha1 = as.second;
}
void PackSelectors(const std::array<uint8_t, 16>& unpacked) {
auto packed = Pack<uint8_t, uint64_t, 3, 16>(unpacked);
SetSelectorBits(packed);
}
/// Get the block's selectors as a 4x4 array of integers between 0 and 7 inclusive.
SelectorArray GetSelectors() const;
inline uint32_t GetSelector(uint32_t x, uint32_t y, uint64_t selector_bits) const {
assert((x < 4U) && (y < 4U));
return (selector_bits >> (((y * 4) + x) * SelectorBits)) & (SelectorMask);
}
/// Get the block's selectors as a 4x4 array of integers between 0 and 7 inclusive.
void SetSelectors(const SelectorArray& unpacked);
static inline std::array<uint8_t, 8> GetValues6(uint32_t l, uint32_t h) {
return {static_cast<uint8_t>(l),
static_cast<uint8_t>(h),
static_cast<uint8_t>((l * 4 + h) / 5),
static_cast<uint8_t>((l * 3 + h * 2) / 5),
static_cast<uint8_t>((l * 2 + h * 3) / 5),
static_cast<uint8_t>((l + h * 4) / 5),
0,
255};
}
/// True if the block uses 6-value interpolation, i.e. alpha0 <= alpha1.
bool Is6Value() const { return alpha0 <= alpha1; }
static inline std::array<uint8_t, 8> GetValues8(uint32_t l, uint32_t h) {
return {static_cast<uint8_t>(l),
static_cast<uint8_t>(h),
static_cast<uint8_t>((l * 6 + h) / 7),
static_cast<uint8_t>((l * 5 + h * 2) / 7),
static_cast<uint8_t>((l * 4 + h * 3) / 7),
static_cast<uint8_t>((l * 3 + h * 4) / 7),
static_cast<uint8_t>((l * 2 + h * 5) / 7),
static_cast<uint8_t>((l + h * 6) / 7)};
}
/// The interpolated values of this block as an array of 8 integers.
std::array<uint8_t, 8> GetValues() const { return Is6Value() ? GetValues6() : GetValues8(); }
static inline std::array<uint8_t, 8> GetValues(uint32_t l, uint32_t h) {
if (l > h)
return GetValues8(l, h);
else
return GetValues6(l, h);
}
bool operator==(const BC4Block& other) const = default;
bool operator!=(const BC4Block& other) const = default;
constexpr static inline size_t EndpointSize = 1;
constexpr static inline size_t SelectorSize = 6;
constexpr static inline uint8_t SelectorBits = 3;
constexpr static inline uint8_t SelectorValues = 1 << SelectorBits;
constexpr static inline uint8_t SelectorMask = SelectorValues - 1;
constexpr static inline uint64_t SelectorBitsMax = (1ULL << (8U * SelectorSize)) - 1U;
uint8_t low_alpha;
uint8_t high_alpha;
std::array<uint8_t, SelectorSize> selectors;
private:
std::array<uint8_t, 8> GetValues6() const;
std::array<uint8_t, 8> GetValues8() const;
};
#pragma pack(pop)
} // namespace quicktex::s3tc

View File

@ -22,24 +22,31 @@
#include <array> // for array
#include <cassert> // for assert
#include "../../BlockView.h" // for ColorBlock
#include "../../ndebug.h" // for ndebug
#include "../../Color.h"
#include "../../ColorBlock.h"
#include "BC4Block.h"
namespace quicktex::s3tc {
void BC4Decoder::DecodeBlock(Byte4x4 dest, BC4Block *const block) const noexcept(ndebug) {
auto l = block->GetLowAlpha();
auto h = block->GetHighAlpha();
auto values = BC4Block::GetValues(l, h);
auto selectors = block->UnpackSelectors();
namespace quicktex::s3tc {
void BC4Decoder::DecodeInto(ColorBlock<4, 4> &dest, const BC4Block &block) const {
auto values = block.GetValues();
auto selectors = block.GetSelectors();
for (unsigned y = 0; y < 4; y++) {
for (unsigned x = 0; x < 4; x++) {
const auto selector = selectors[y][x];
assert(selector < 8);
dest.Set(x, y, values[selector]);
auto color = dest.Get(x, y);
color[_channel] = values[selector];
dest.Set(x, y, color);
}
}
}
ColorBlock<4, 4> BC4Decoder::DecodeBlock(const BC4Block &block) const {
auto output = ColorBlock<4, 4>();
DecodeInto(output, block);
return output;
}
} // namespace quicktex::s3tc

View File

@ -19,26 +19,26 @@
#pragma once
#include <cassert>
#include <cstdint>
#include <stdexcept>
#include "../../BlockDecoder.h"
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "../../Decoder.h"
#include "../../Texture.h"
#include "BC4Block.h"
namespace quicktex::s3tc {
class BC4Decoder : public BlockDecoderTemplate<BC4Block, 4, 4> {
class BC4Decoder : public BlockDecoder<BlockTexture<BC4Block>> {
public:
BC4Decoder(uint8_t channel = 3) {
if (channel >= 4U) throw std::invalid_argument("Channel out of range");
_channel = channel;
}
void DecodeBlock(Color4x4 dest, BC4Block *const block) const noexcept(ndebug) override { DecodeBlock(dest.GetChannel(_channel), block); }
void DecodeBlock(Byte4x4 dest, BC4Block *const block) const noexcept(ndebug);
ColorBlock<4, 4> DecodeBlock(const BC4Block &block) const override;
void DecodeInto(ColorBlock<4, 4> &dest, const BC4Block &block) const;
uint8_t GetChannel() const { return _channel; }

View File

@ -22,29 +22,28 @@
#include <algorithm>
#include <array>
#include <cstdint>
#include <utility>
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../Color.h"
#include "../../ColorBlock.h"
#include "../../util.h"
#include "BC4Block.h"
namespace quicktex::s3tc {
void BC4Encoder::EncodeBlock(Byte4x4 pixels, BC4Block *const dest) const noexcept(ndebug) {
auto flattened = pixels.Flatten();
auto minmax = std::minmax_element(flattened.begin(), flattened.end());
namespace quicktex::s3tc {
BC4Block BC4Encoder::EncodeBlock(const ColorBlock<4, 4> &pixels) const {
uint8_t min = UINT8_MAX;
uint8_t max = 0;
uint8_t min = *minmax.first;
uint8_t max = *minmax.second;
dest->high_alpha = min;
dest->low_alpha = max;
if (max == min) {
dest->SetSelectorBits(0);
return;
for (int i = 0; i < 16; i++) {
auto value = pixels.Get(i)[_channel];
min = std::min(min, value);
max = std::max(max, value);
}
std::array<uint8_t, 16> selectors = {};
if (max == min) {
return BC4Block(min); // solid block
}
auto selectors = BC4Block::SelectorArray();
const static std::array<uint8_t, 8> Levels = {1U, 7U, 6U, 5U, 4U, 3U, 2U, 0U}; // selector value options in linear order
// BC4 floors in its divisions, which we compensate for with the 4 bias.
@ -58,16 +57,19 @@ void BC4Encoder::EncodeBlock(Byte4x4 pixels, BC4Block *const dest) const noexcep
for (unsigned i = 0; i < 7; i++) thresholds[i] = delta * (1 + (2 * (int)i)) - bias;
// iterate over all values and calculate selectors
for (unsigned i = 0; i < 16; i++) {
int value = flattened[i] * 14; // multiply by demonimator
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
int value = (int)pixels.Get(x, y)[_channel] * 14; // multiply by demonimator
// level = number of thresholds this value is greater than
unsigned level = 0;
for (unsigned c = 0; c < 7; c++) level += value >= thresholds[c];
// level = number of thresholds this value is greater than
unsigned level = 0;
for (unsigned c = 0; c < 7; c++) level += value >= thresholds[c];
selectors[i] = Levels[level];
selectors[(unsigned)y][(unsigned)x] = Levels[level];
}
}
dest->PackSelectors(selectors);
return BC4Block(max, min, selectors);
}
} // namespace quicktex::s3tc

View File

@ -19,26 +19,24 @@
#pragma once
#include <cassert>
#include <cstdint>
#include <stdexcept>
#include "../../BlockEncoder.h"
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "../../Encoder.h"
#include "../../Texture.h"
#include "BC4Block.h"
namespace quicktex::s3tc {
class BC4Encoder : public BlockEncoderTemplate<BC4Block, 4, 4> {
class BC4Encoder : public BlockEncoder<BlockTexture<BC4Block>> {
public:
BC4Encoder(const uint8_t channel) {
if (channel >= 4) throw std::invalid_argument("Channel out of range");
_channel = channel;
}
void EncodeBlock(Color4x4 pixels, BC4Block *const dest) const override { EncodeBlock(pixels.GetChannel(_channel), dest); }
void EncodeBlock(Byte4x4 pixels, BC4Block *const dest) const noexcept(ndebug);
BC4Block EncodeBlock(const ColorBlock<4, 4> &pixels) const override;
uint8_t GetChannel() const { return _channel; }

View File

@ -17,7 +17,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../../_bindings.h"
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <array>
#include <cstddef>
@ -25,55 +28,98 @@
#include <stdexcept>
#include <string>
#include "../../BlockDecoder.h"
#include "../../BlockEncoder.h"
#include "../../Decoder.h"
#include "../../Encoder.h"
#include "BC4Decoder.h"
#include "BC4Encoder.h"
namespace py = pybind11;
namespace quicktex::bindings {
using namespace quicktex::s3tc;
using namespace quicktex::s3tc;
void InitBC4(py::module_ &s3tc) {
auto bc4 = s3tc.def_submodule("_bc4", "internal bc4 module");
auto block_encoder = py::type::of<BlockEncoder>();
auto block_decoder = py::type::of<BlockDecoder>();
py::options options;
options.disable_function_signatures();
// BC4Encoder
py::class_<BC4Encoder> bc4_encoder(bc4, "BC4Encoder", block_encoder, R"doc(
Base: :py:class:`~quicktex.BlockEncoder`
// region BC4Block
auto bc4_block = BindBlock<BC4Block>(bc4, "BC4Block");
bc4_block.doc() = "A single BC4 block.";
bc4_block.def(py::init<>());
bc4_block.def(py::init<uint8_t, uint8_t, BC4Block::SelectorArray>(), "endpoint0"_a, "endpoint1"_a, "selectors"_a, R"doc(
Create a new BC4Block with the specified endpoints and selectors.
:param int endpoint0: The first endpoint.
:param int endpoint1: The second endpoint.
:param selectors: the selectors as a 4x4 list of integers, between 0 and 7 inclusive.
)doc");
bc4_block.def_property("endpoints", &BC4Block::GetAlphas, &BC4Block::SetAlphas, "The block's endpoint values as a 2-tuple.");
bc4_block.def_property("selectors", &BC4Block::GetSelectors, &BC4Block::SetSelectors, R"doc(
The block's selectors as a 4x4 list of integers between 0 and 7 inclusive.
.. note::
This is a property, so directly modifying its value will not propogate back to the block.
Instead you must read, modify, then write the new list back to the property, like so::
selectors = block.selectors
selectors[0,0] = 0
block.selectors = selectors
)doc");
bc4_block.def_property_readonly("values", &BC4Block::GetValues, R"doc(
The interpolated values used to decode the block, coresponding with the indices in :py:attr:`selectors`.
)doc");
bc4_block.def_property_readonly("is_6value", &BC4Block::Is6Value, R"doc(
"True if the block uses 6-value interpolation, i.e. endpoint0 <= endpoint1. Readonly.
)doc");
// endregion
// region BC4Texture
auto bc4_texture = BindBlockTexture<BC4Block>(bc4, "BC4Texture");
bc4_texture.doc() = "A texture comprised of BC4 blocks.";
// endregion
// region BC4Encoder
py::class_<BC4Encoder> bc4_encoder(bc4, "BC4Encoder", R"doc(
Encodes single-channel textures to BC4.
)doc");
bc4_encoder.def(py::init<uint8_t>(), py::arg("channel") = 3, R"doc(
__init__(channel : int = 3) -> None
Create a new BC4 encoder with the specified channel
:param int channel: the channel that will be read from. 0 to 3 inclusive. Default: 3 (alpha).
)doc");
bc4_encoder.def("encode", &BC4Encoder::Encode, "texture"_a, R"doc(
Encode a raw texture into a new BC4Texture using the encoder's current settings.
:param RawTexture texture: Input texture to encode.
:returns: A new BC4Texture with the same dimension as the input.
)doc");
bc4_encoder.def_property_readonly("channel", &BC4Encoder::GetChannel, "The channel that will be read from. 0 to 3 inclusive. Readonly.");
// endregion
// BC4Decoder
py::class_<BC4Decoder> bc4_decoder(bc4, "BC4Decoder", block_decoder, R"doc(
Base: :py:class:`~quicktex.BlockDecoder`
// region BC4Decoder
py::class_<BC4Decoder> bc4_decoder(bc4, "BC4Decoder", R"doc(
Decodes BC4 textures to a single-channel.
)doc");
bc4_decoder.def(py::init<uint8_t>(), py::arg("channel") = 3, R"doc(
__init__(channel : int = 3) -> None
Create a new BC4 decoder with the specified channel
:param int channel: The channel that will be written to. 0 to 3 inclusive. Default: 3 (alpha).
)doc");
bc4_decoder.def("decode", &BC4Decoder::Decode, "texture"_a, R"doc(
Decode a BC4 texture into a new RawTexture using the decoder's current settings.
:param RawTexture texture: Input texture to encode.
:returns: A new RawTexture with the same dimensions as the input
)doc");
bc4_decoder.def_property_readonly("channel", &BC4Decoder::GetChannel, "The channel that will be written to. 0 to 3 inclusive. Readonly.");
// endregion
}
} // namespace quicktex::bindings

View File

@ -19,15 +19,42 @@
#pragma once
#include <utility>
#include "../bc4/BC4Block.h"
namespace quicktex::s3tc {
namespace quicktex::s3tc {
#pragma pack(push, 1)
class BC5Block {
class alignas(8) BC5Block {
public:
static constexpr int Width = 4;
static constexpr int Height = 4;
using BlockPair = std::pair<BC4Block, BC4Block>;
BC4Block chan0_block;
BC4Block chan1_block;
constexpr BC5Block() {
static_assert(sizeof(BC5Block) == 16);
static_assert(sizeof(std::array<BC5Block, 10>) == 16 * 10);
static_assert(alignof(BC5Block) >= 8);
chan0_block = chan1_block = BC4Block();
}
BC5Block(const BC4Block &chan0, const BC4Block &chan1) {
chan0_block = chan0;
chan1_block = chan1;
}
BlockPair GetBlocks() const { return BlockPair(chan0_block, chan1_block); }
void SetBlocks(const BlockPair &pair) {
chan0_block = pair.first;
chan1_block = pair.second;
}
bool operator==(const BC5Block& other) const = default;
bool operator!=(const BC5Block& other) const = default;
};
#pragma pack(pop)
} // namespace quicktex::s3tc

View File

@ -19,14 +19,14 @@
#include "BC5Decoder.h"
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "BC5Block.h"
namespace quicktex::s3tc {
void BC5Decoder::DecodeBlock(Color4x4 dest, BC5Block *const block) const noexcept(ndebug) {
_chan0_decoder->DecodeBlock(dest, &block->chan0_block);
_chan1_decoder->DecodeBlock(dest, &block->chan1_block);
namespace quicktex::s3tc {
ColorBlock<4, 4> BC5Decoder::DecodeBlock(const BC5Block &block) const {
auto output = ColorBlock<4, 4>();
_chan0_decoder->DecodeInto(output, block.chan0_block);
_chan1_decoder->DecodeInto(output, block.chan1_block);
return output;
}
} // namespace quicktex::s3tc

View File

@ -24,15 +24,15 @@
#include <tuple>
#include <type_traits>
#include "../../BlockDecoder.h"
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "../../Decoder.h"
#include "../../Texture.h"
#include "../bc4/BC4Decoder.h"
#include "BC5Block.h"
namespace quicktex::s3tc {
class BC5Decoder : public BlockDecoderTemplate<BC5Block, 4, 4> {
class BC5Decoder : public BlockDecoder<BlockTexture<BC5Block>> {
public:
using ChannelPair = std::tuple<uint8_t, uint8_t>;
using BC4DecoderPtr = std::shared_ptr<BC4Decoder>;
@ -41,7 +41,7 @@ class BC5Decoder : public BlockDecoderTemplate<BC5Block, 4, 4> {
BC5Decoder(uint8_t chan0 = 0, uint8_t chan1 = 1) : BC5Decoder(std::make_shared<BC4Decoder>(chan0), std::make_shared<BC4Decoder>(chan1)) {}
BC5Decoder(BC4DecoderPtr chan0_decoder, BC4DecoderPtr chan1_decoder) : _chan0_decoder(chan0_decoder), _chan1_decoder(chan1_decoder) {}
void DecodeBlock(Color4x4 dest, BC5Block *const block) const noexcept(ndebug) override;
ColorBlock<4, 4> DecodeBlock(const BC5Block &block) const override;
ChannelPair GetChannels() const { return ChannelPair(_chan0_decoder->GetChannel(), _chan1_decoder->GetChannel()); }

View File

@ -19,9 +19,14 @@
#include "BC5Encoder.h"
namespace quicktex::s3tc {
void BC5Encoder::EncodeBlock(Color4x4 pixels, BC5Block *dest) const {
_chan0_encoder->EncodeBlock(pixels, &(dest->chan0_block));
_chan1_encoder->EncodeBlock(pixels, &(dest->chan1_block));
#include "../../ColorBlock.h"
#include "../bc4/BC4Block.h"
namespace quicktex::s3tc {
BC5Block BC5Encoder::EncodeBlock(const ColorBlock<4, 4> &pixels) const {
auto output = BC5Block();
output.chan0_block = _chan0_encoder->EncodeBlock(pixels);
output.chan1_block = _chan1_encoder->EncodeBlock(pixels);
return output;
}
} // namespace quicktex::s3tc

View File

@ -24,15 +24,14 @@
#include <tuple>
#include <type_traits>
#include "../../BlockEncoder.h"
#include "../../BlockView.h"
#include "../../ndebug.h"
#include "../../ColorBlock.h"
#include "../../Encoder.h"
#include "../../Texture.h"
#include "../bc4/BC4Encoder.h"
#include "BC5Block.h"
namespace quicktex::s3tc {
class BC5Encoder : public BlockEncoderTemplate<BC5Block, 4, 4> {
class BC5Encoder : public BlockEncoder<BlockTexture<BC5Block>> {
public:
using ChannelPair = std::tuple<uint8_t, uint8_t>;
using BC4EncoderPtr = std::shared_ptr<BC4Encoder>;
@ -41,7 +40,7 @@ class BC5Encoder : public BlockEncoderTemplate<BC5Block, 4, 4> {
BC5Encoder(uint8_t chan0 = 0, uint8_t chan1 = 1) : BC5Encoder(std::make_shared<BC4Encoder>(chan0), std::make_shared<BC4Encoder>(chan1)) {}
BC5Encoder(BC4EncoderPtr chan0_encoder, BC4EncoderPtr chan1_encoder) : _chan0_encoder(chan0_encoder), _chan1_encoder(chan1_encoder) {}
void EncodeBlock(Color4x4 pixels, BC5Block *dest) const override;
BC5Block EncodeBlock(const ColorBlock<4, 4> &pixels) const override;
ChannelPair GetChannels() const { return ChannelPair(_chan0_encoder->GetChannel(), _chan1_encoder->GetChannel()); }

View File

@ -17,13 +17,15 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../../_bindings.h"
#include <pybind11/pybind11.h>
#include <array>
#include <cstdint>
#include "../../BlockDecoder.h"
#include "../../BlockEncoder.h"
#include "../../Decoder.h"
#include "../../Encoder.h"
#include "BC5Decoder.h"
#include "BC5Encoder.h"
@ -35,49 +37,75 @@ using namespace quicktex::s3tc;
void InitBC5(py::module_ &s3tc) {
auto bc5 = s3tc.def_submodule("_bc5", "internal bc5 module");
auto block_encoder = py::type::of<BlockEncoder>();
auto block_decoder = py::type::of<BlockDecoder>();
py::options options;
options.disable_function_signatures();
// BC5Encoder
py::class_<BC5Encoder> bc5_encoder(bc5, "BC5Encoder", block_encoder, R"doc(
Base: :py:class:`~quicktex.BlockEncoder`
// region BC5Block
auto bc5_block = BindBlock<BC5Block>(bc5, "BC5Block");
bc5_block.doc() = "A single BC5 block.";
bc5_block.def(py::init<>());
bc5_block.def(py::init<BC4Block, BC4Block>(), "chan0_block"_a, "chan1_block"_a, R"doc(
Create a new BC5Block out of two BC4 blocks.
:param BC4Block chan0_block: The BC4 block used for the first channel.
:param BC4Block chan1_block: The BC1 block used for the second channel.
)doc");
bc5_block.def_readwrite("chan0_block", &BC5Block::chan0_block, "The BC4 block used for the first channel.");
bc5_block.def_readwrite("chan1_block", &BC5Block::chan1_block, "The BC4 block used for the second channel.");
bc5_block.def_property("blocks", &BC5Block::GetBlocks, &BC5Block::SetBlocks, "The BC4 and BC1 blocks that make up this block as a 2-tuple.");
// endregion
// region BC5Texture
auto bc5_texture = BindBlockTexture<BC5Block>(bc5, "BC5Texture");
bc5_texture.doc() = "A texture comprised of BC5 blocks.";
// endregion
// region BC5Encoder
py::class_<BC5Encoder> bc5_encoder(bc5, "BC5Encoder", R"doc(
Encodes dual-channel textures to BC5.
)doc");
bc5_encoder.def(py::init<uint8_t, uint8_t>(), py::arg("chan0") = 0, py::arg("chan1") = 1, R"doc(
__init__(chan0 : int = 0, chan1 : int = 1) -> None
Create a new BC5 encoder with the specified channels
:param int chan0: the first channel that will be read from. 0 to 3 inclusive. Default: 0 (red).
:param int chan1: the second channel that will be read from. 0 to 3 inclusive. Default: 1 (green).
)doc");
bc5_encoder.def("encode", &BC5Encoder::Encode, "texture"_a, R"doc(
Encode a raw texture into a new BC5Texture using the encoder's current settings.
:param RawTexture texture: Input texture to encode.
:returns: A new BC5Texture with the same dimension as the input.
)doc");
bc5_encoder.def_property_readonly("channels", &BC5Encoder::GetChannels, "A 2-tuple of channels that will be read from. 0 to 3 inclusive. Readonly.");
bc5_encoder.def_property_readonly("bc4_encoders", &BC5Encoder::GetBC4Encoders,
"2-tuple of internal :py:class:`~quicktex.s3tc.bc4.BC4Encoder` s used for each channel. Readonly.");
// endregion
// BC5Decoder
py::class_<BC5Decoder> bc5_decoder(bc5, "BC5Decoder", block_decoder, R"doc(
Base: :py:class:`~quicktex.BlockDecoder`
// region BC5Decoder
py::class_<BC5Decoder> bc5_decoder(bc5, "BC5Decoder", R"doc(
Decodes BC4 textures to two channels.
)doc");
bc5_decoder.def(py::init<uint8_t, uint8_t>(), py::arg("chan0") = 0, py::arg("chan1") = 1, R"doc(
__init__(chan0 : int = 0, chan1 : int = 1) -> None
Create a new BC5 decoder with the specified channels
:param int chan0: the first channel that will be written to. 0 to 3 inclusive. Default: 0 (red).
:param int chan1: the second channel that will be written to. 0 to 3 inclusive. Default: 1 (green).
)doc");
bc5_decoder.def("decode", &BC5Decoder::Decode, "texture"_a, R"doc(
Decode a BC5 texture into a new RawTexture using the decoder's current settings.
:param RawTexture texture: Input texture to encode.
:returns: A new RawTexture with the same dimensions as the input
)doc");
bc5_decoder.def_property_readonly("channels", &BC5Decoder::GetChannels, "A 2-tuple of channels that will be written to. 0 to 3 inclusive. Readonly.");
bc5_decoder.def_property_readonly("bc4_decoders", &BC5Decoder::GetBC4Decoders,
"2-tuple of internal :py:class:`~quicktex.s3tc.bc4.BC4Decoder` s used for each channel. Readonly.");
// endregion
}
} // namespace quicktex::bindings

View File

@ -50,7 +50,7 @@ uint8_t Interpolator::Interpolate6(uint8_t v0, uint8_t v1) const { return Interp
uint8_t Interpolator::InterpolateHalf5(uint8_t v0, uint8_t v1) const { return InterpolateHalf8(scale5To8(v0), scale5To8(v1)); }
uint8_t Interpolator::InterpolateHalf6(uint8_t v0, uint8_t v1) const { return InterpolateHalf8(scale6To8(v0), scale6To8(v1)); }
std::array<Color, 4> Interpolator::InterpolateBC1(uint16_t low, uint16_t high, bool allow_3color) const {
std::array<Color, 4> Interpolator::Interpolate565BC1(uint16_t low, uint16_t high, bool allow_3color) const {
bool use_3color = allow_3color && (high >= low);
return InterpolateBC1(Color::Unpack565Unscaled(low), Color::Unpack565Unscaled(high), use_3color);
}

View File

@ -94,10 +94,18 @@ class Interpolator {
* Generates the 4 colors for a BC1 block from the given 5:6:5-packed colors
* @param low first 5:6:5 color for the block
* @param high second 5:6:5 color for the block
* @return and array of 4 Color values, with indices matching BC1 selectors
* @param allow_3color if true, a different interpolation mode will be used if high >= low
* @return an array of 4 Color values, with indices matching BC1 selectors
*/
virtual std::array<Color, 4> InterpolateBC1(uint16_t low, uint16_t high, bool allow_3color = true) const;
std::array<Color, 4> Interpolate565BC1(uint16_t low, uint16_t high, bool allow_3color = true) const;
/**
* Generates the 4 colors for a BC1 block from the given
* @param low the first color for the block, as a seperated 5:6:5 Color object
* @param high the second color for the block, as a seperated 5:6:5 Color object
* @param use_3color if the 3-color interpolation mode should be used
* @return an array of 4 Color values, with indices matching BC1 selectors
*/
virtual std::array<Color, 4> InterpolateBC1(Color low, Color high, bool use_3color) const;
/**
@ -127,27 +135,27 @@ class Interpolator {
}
};
class InterpolatorRound : public Interpolator {
class InterpolatorRound final : public Interpolator {
public:
uint8_t Interpolate5(uint8_t v0, uint8_t v1) const override;
uint8_t Interpolate6(uint8_t v0, uint8_t v1) const override;
uint8_t Interpolate8(uint8_t v0, uint8_t v1) const override;
virtual uint8_t Interpolate5(uint8_t v0, uint8_t v1) const override;
virtual uint8_t Interpolate6(uint8_t v0, uint8_t v1) const override;
virtual uint8_t Interpolate8(uint8_t v0, uint8_t v1) const override;
Type GetType() const noexcept override { return Type::IdealRound; }
virtual Type GetType() const noexcept override { return Type::IdealRound; }
};
class InterpolatorNvidia : public Interpolator {
class InterpolatorNvidia final : public Interpolator {
public:
uint8_t Interpolate5(uint8_t v0, uint8_t v1) const override;
uint8_t Interpolate6(uint8_t v0, uint8_t v1) const override;
virtual uint8_t Interpolate5(uint8_t v0, uint8_t v1) const override;
virtual uint8_t Interpolate6(uint8_t v0, uint8_t v1) const override;
uint8_t InterpolateHalf5(uint8_t v0, uint8_t v1) const override;
uint8_t InterpolateHalf6(uint8_t v0, uint8_t v1) const override;
virtual uint8_t InterpolateHalf5(uint8_t v0, uint8_t v1) const override;
virtual uint8_t InterpolateHalf6(uint8_t v0, uint8_t v1) const override;
std::array<Color, 4> InterpolateBC1(Color low, Color high, bool use_3color) const override;
virtual std::array<Color, 4> InterpolateBC1(Color low, Color high, bool use_3color) const override;
Type GetType() const noexcept override { return Type::Nvidia; }
bool CanInterpolate8Bit() const noexcept override { return false; }
virtual Type GetType() const noexcept override { return Type::Nvidia; }
virtual bool CanInterpolate8Bit() const noexcept override { return false; }
private:
Color InterpolateColor565(const Color &c0, const Color &c1) const {
@ -159,16 +167,16 @@ class InterpolatorNvidia : public Interpolator {
}
};
class InterpolatorAMD : public Interpolator {
class InterpolatorAMD final : public Interpolator {
public:
uint8_t Interpolate5(uint8_t v0, uint8_t v1) const override;
uint8_t Interpolate6(uint8_t v0, uint8_t v1) const override;
uint8_t Interpolate8(uint8_t v0, uint8_t v1) const override;
virtual uint8_t Interpolate5(uint8_t v0, uint8_t v1) const override;
virtual uint8_t Interpolate6(uint8_t v0, uint8_t v1) const override;
virtual uint8_t Interpolate8(uint8_t v0, uint8_t v1) const override;
uint8_t InterpolateHalf5(uint8_t v0, uint8_t v1) const override;
uint8_t InterpolateHalf6(uint8_t v0, uint8_t v1) const override;
uint8_t InterpolateHalf8(uint8_t v0, uint8_t v1) const override;
virtual uint8_t InterpolateHalf5(uint8_t v0, uint8_t v1) const override;
virtual uint8_t InterpolateHalf6(uint8_t v0, uint8_t v1) const override;
virtual uint8_t InterpolateHalf8(uint8_t v0, uint8_t v1) const override;
Type GetType() const noexcept override { return Type::AMD; }
virtual Type GetType() const noexcept override { return Type::AMD; }
};
} // namespace quicktex::s3tc

View File

@ -33,7 +33,7 @@ void InitInterpolator(py::module_ &s3tc) {
auto interpolator = s3tc.def_submodule("_interpolator", "internal interpolator module");
// Interpolator
py::class_<Interpolator> ideal(
py::class_<Interpolator, std::shared_ptr<Interpolator>> ideal(
interpolator, "Interpolator", R"doc(
Interpolator base class representing the ideal interpolation mode, with no rounding for colors 2 and 3.
This matches the `D3D10 docs`_ on BC1.
@ -42,7 +42,7 @@ void InitInterpolator(py::module_ &s3tc) {
)doc");
// InterpolatorRound
py::class_<InterpolatorRound> round(interpolator, "InterpolatorRound", ideal, R"doc(
py::class_<InterpolatorRound, std::shared_ptr<InterpolatorRound>> round(interpolator, "InterpolatorRound", ideal, R"doc(
Base: :py:class:`~quicktex.s3tc.interpolator.Interpolator`
Interpolator class representing the ideal rounding interpolation mode.
@ -52,14 +52,14 @@ void InitInterpolator(py::module_ &s3tc) {
)doc");
// InterpolatorNvidia
py::class_<InterpolatorNvidia> nvidia(interpolator, "InterpolatorNvidia", ideal, R"doc(
py::class_<InterpolatorNvidia, std::shared_ptr<InterpolatorNvidia>> nvidia(interpolator, "InterpolatorNvidia", ideal, R"doc(
Base: :py:class:`~quicktex.s3tc.interpolator.Interpolator`
Interpolator class representing the Nvidia GPU interpolation mode.
)doc");
// InterpolatorAMD
py::class_<InterpolatorAMD> amd(interpolator, "InterpolatorAMD", ideal, R"doc(
py::class_<InterpolatorAMD, std::shared_ptr<InterpolatorAMD>> amd(interpolator, "InterpolatorAMD", ideal, R"doc(
Base: :py:class:`~quicktex.s3tc.interpolator.Interpolator`
Interpolator class representing the AMD GPU interpolation mode.

View File

@ -21,9 +21,10 @@
#include <cassert>
#include <cstdint>
#include <limits>
#include <string>
#include <type_traits>
#include "ndebug.h"
#include <functional>
#include <vector>
#define UINT5_MAX 0x1FU // 31
#define UINT6_MAX 0x3FU // 63
@ -46,7 +47,7 @@ template <typename S> constexpr auto iabs(S i) {
* @param packed Packed integer input of type I.
* @return Unpacked std::array of type O and size C.
*/
template <typename I, typename O, size_t S, size_t C> constexpr auto Unpack(I packed) noexcept(ndebug) {
template <typename I, typename O, size_t S, size_t C> constexpr std::array<O, C> Unpack(I packed) {
// type checking
static_assert(std::is_unsigned<I>::value, "Packed input type must be unsigned");
static_assert(std::is_unsigned<O>::value, "Unpacked output type must be unsigned");
@ -73,7 +74,7 @@ template <typename I, typename O, size_t S, size_t C> constexpr auto Unpack(I pa
* @param vals Unpacked std::array of type I and size C.
* @return Packed integer input of type O.
*/
template <typename I, typename O, size_t S, size_t C> constexpr auto Pack(const std::array<I, C> &vals) noexcept(ndebug) {
template <typename I, typename O, size_t S, size_t C> constexpr O Pack(const std::array<I, C> &vals) {
// type checking
static_assert(std::is_unsigned<I>::value, "Unpacked input type must be unsigned");
static_assert(std::is_unsigned<O>::value, "Packed output type must be unsigned");
@ -97,6 +98,16 @@ template <size_t Size, int Op(int)> constexpr std::array<uint8_t, Size> ExpandAr
return res;
}
template <typename Seq, typename Fn> constexpr auto MapArray(const Seq &input, Fn op) {
using I = typename Seq::value_type;
using O = decltype(op(std::declval<I>()));
constexpr size_t N = std::tuple_size<Seq>::value;
std::array<O, N> output;
for (unsigned i = 0; i < N; i++) { output[i] = op(input[i]); }
return output;
}
template <typename S> constexpr S scale8To5(S v) {
auto v2 = v * 31 + 128;
return static_cast<S>((v2 + (v2 >> 8)) >> 8);
@ -147,3 +158,21 @@ constexpr int squarei(int a) { return a * a; }
constexpr int absi(int a) { return (a < 0) ? -a : a; }
template <typename F> constexpr F lerp(F a, F b, F s) { return a + (b - a) * s; }
template <typename... Args> std::string Format(const char *str, const Args &...args) {
auto output = std::string(str);
std::vector<std::string> values = {{args...}};
for (unsigned i = 0; i < values.size(); i++) {
auto key = "{" + std::to_string(i) + "}";
auto value = values[i];
while (true) {
size_t where = output.find(key);
if (where == output.npos) break;
output.replace(where, key.length(), value);
}
}
return output;
}

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
import sys
import glob
import subprocess
from setuptools import setup, Extension, find_packages
@ -99,6 +100,9 @@ class CMakeBuild(build_ext):
)
# Find stub files
stubs = [path.replace('quicktex/', '') for path in glob.glob('quicktex/**/*.pyi', recursive=True)]
# The information here can also be placed in setup.cfg - better separation of
# logic and declaration, and simpler if you include description/version in a file.
setup(
@ -113,10 +117,17 @@ setup(
cmdclass={"build_ext": CMakeBuild},
packages=find_packages('.'),
package_dir={'': '.'},
install_requires=["Pillow"],
package_data={'': ['py.typed'] + stubs},
include_package_data=True,
install_requires=["Pillow", "click"],
extras_require={
"tests": ["nose", "parameterized"],
"docs": ["sphinx", "myst-parser", "sphinx-rtd-theme"],
"stubs": ["pybind11-stubgen"],
},
entry_points='''
[console_scripts]
quicktex=quicktex.cli:cli
''',
zip_safe=False,
)

0
tests/__init__.py Normal file
View File

View File

@ -1,30 +0,0 @@
from s3tc import BC1Block
from color import Color
from PIL import Image
import os
image_path = os.path.dirname(os.path.realpath(__file__)) + "/images"
# A block that should always encode greyscale, where every row of pixels is identical, and the left side is lighter than the right side
greyscale = Image.open(image_path + "/blocks/greyscale.png").tobytes("raw", "RGBX")
# A block that should always encode 3-color when available.
# from left to right: red, yellow, yellow, green
three_color = Image.open(image_path + "/blocks/3color.png").tobytes("raw", "RGBX")
# A block that should always encode 3-color with black when available
# from left to right: black, red, yellow, green
three_color_black = Image.open(image_path + "/blocks/3color black.png").tobytes("raw", "RGBX")
bc1_test_blocks = [
# A block that should always encode greyscale, where every row of pixels is identical, and the left side is lighter than the right side
{"name": "greyscale",
"image": Image.open(image_path + "/blocks/greyscale.png").tobytes("raw", "RGBX"),
"expected": BC1Block(Color(0xFF, 0xFF, 0xFF), Color(0x44, 0x44, 0x44), [[0, 2, 3, 1]] * 4)},
# A block that should always encode 3-color when available.
# from left to right: red, yellow, yellow, green
{"name": "3color",
"image": Image.open(image_path + "/blocks/3color.png").tobytes("raw", "RGBX"),
"expected": BC1Block(Color(0x00, 0xFF, 0x00), Color(0xFF, 0x00, 0x00), [[0, 2, 3, 1]] * 4)}
]

View File

@ -1,81 +0,0 @@
class Color:
def __init__(self, r=0, g=0, b=0, a=0xFF):
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()
def error(self, other):
assert isinstance(other, Color)
return ((self.r - other.r) ** 2) + ((self.g - other.g) ** 2) + ((self.b - other.b) ** 2)
@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())

@ -1 +1 @@
Subproject commit 0ade526dd1070e6f5b43e0bd58b26828abff86b7
Subproject commit dd5ebb79412f097a80391ab9975f611cbb85d0e1

View File

@ -25,6 +25,7 @@
#include <string>
namespace py = pybind11;
using namespace pybind11::literals;
#define STRINGIFY(x) #x
#define MACRO_STRINGIFY(x) STRINGIFY(x)
@ -37,6 +38,7 @@ int main() {
site.attr("addsitedir")(CUSTOM_SYS_PATH);
py::module_ nose = py::module_::import("nose");
py::module_ tests = py::module_::import("test_BC1");
nose.attr("main")("test_BC1");
py::module_ tests = py::module_::import("tests");
py::list argv(1);
nose.attr("runmodule")("name"_a = "tests.test_bc1", "exit"_a = false);
}

View File

@ -1,89 +0,0 @@
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, color0=Color(), color1=Color(), selectors=[[0] * 4] * 4):
self.color0 = color0
self.color1 = color1
self.selectors = selectors
def __repr__(self):
return repr(self.__dict__)
def __str__(self):
return f'color0: {str(self.color0)} color1: {str(self.color1)}, selectors:{self.selectors}'
@staticmethod
def frombytes(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 tobytes(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()
def is_3color_black(self):
return self.is_3color() and any(3 in row for row in self.selectors)
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 frombytes(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 tobytes(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]))

View File

@ -1,74 +1,210 @@
import unittest
import nose
from parameterized import parameterized_class
from s3tc import BC1Block
from images import Blocks
import quicktex.s3tc.bc1 as bc1
import quicktex.s3tc.interpolator as interpolator
from parameterized import parameterized, parameterized_class
from quicktex.s3tc.bc1 import BC1Block, BC1Texture, BC1Encoder, BC1Decoder
from tests.images import BC1Blocks
from PIL import Image, ImageChops
ColorMode = bc1.BC1Encoder.ColorMode
in_endpoints = ((253, 254, 255), (65, 70, 67)) # has some small changes that should encode the same
out_endpoints = ((255, 255, 255, 255), (66, 69, 66, 255))
selectors = [[0, 2, 3, 1]] * 4
block_bytes = b'\xff\xff\x28\x42\x78\x78\x78\x78'
class TestBC1Block(unittest.TestCase):
"""Tests for the BC1Block class"""
def test_size(self):
"""Test the size and dimensions of BC1Block"""
self.assertEqual(BC1Block.size, 8, 'incorrect block size')
self.assertEqual(BC1Block.width, 4, 'incorrect block width')
self.assertEqual(BC1Block.height, 4, 'incorrect block width')
self.assertEqual(BC1Block.dimensions, (4, 4), 'incorrect block dimensions')
def test_buffer(self):
"""Test the buffer protocol of BC1Block"""
block = BC1Block()
mv = memoryview(block)
self.assertFalse(mv.readonly, 'buffer is readonly')
self.assertTrue(mv.c_contiguous, 'buffer is not contiguous')
self.assertEqual(mv.ndim, 1, 'buffer is multidimensional')
self.assertEqual(mv.nbytes, BC1Block.size, 'buffer is the wrong size')
self.assertEqual(mv.format, 'B', 'buffer has the wrong format')
mv[:] = block_bytes
self.assertEqual(mv.tobytes(), block_bytes, 'incorrect buffer data')
def test_constructor(self):
"""Test constructing a block out of endpoints and selectors"""
block = BC1Block(*in_endpoints, selectors)
self.assertEqual(block.tobytes(), block_bytes, 'incorrect block bytes')
self.assertEqual(block.selectors, selectors, 'incorrect selectors')
self.assertEqual(block.endpoints, out_endpoints, 'incorrect endpoints')
self.assertFalse(block.is_3color, 'incorrect color mode')
def test_frombytes(self):
"""Test constructing a block out of raw data"""
block = BC1Block.frombytes(block_bytes)
self.assertEqual(block.tobytes(), block_bytes, 'incorrect block bytes')
self.assertEqual(block.selectors, selectors, 'incorrect selectors')
self.assertEqual(block.endpoints, out_endpoints, 'incorrect endpoints')
self.assertFalse(block.is_3color, 'incorrect color mode')
def test_eq(self):
"""Test equality between two identical blocks"""
block1 = BC1Block.frombytes(block_bytes)
block2 = BC1Block.frombytes(block_bytes)
self.assertEqual(block1, block2, 'identical blocks not equal')
@parameterized_class(
("name", "w", "h", "wb", "hb"), [
("8x8", 8, 8, 2, 2),
("9x9", 9, 9, 3, 3),
("7x7", 7, 7, 2, 2),
("7x9", 7, 9, 2, 3)
])
class TestBC1Texture(unittest.TestCase):
def setUp(self):
self.tex = BC1Texture(self.w, self.h)
self.size = self.wb * self.hb * BC1Block.size
def test_size(self):
"""Test size of BC1Texture in bytes"""
self.assertEqual(self.tex.size, self.size, 'incorrect texture size')
self.assertEqual(len(self.tex.tobytes()), self.size, 'incorrect texture size from tobytes')
def test_dimensions(self):
"""Test dimensions of BC1Texture in pixels"""
self.assertEqual(self.tex.width, self.w, 'incorrect texture width')
self.assertEqual(self.tex.height, self.h, 'incorrect texture height')
self.assertEqual(self.tex.dimensions, (self.w, self.h), 'incorrect texture dimensions')
def test_dimensions_blocks(self):
"""Test dimensions of BC1Texture in blocks"""
self.assertEqual(self.tex.width_blocks, self.wb, 'incorrect texture width_blocks')
self.assertEqual(self.tex.height_blocks, self.hb, 'incorrect texture width_blocks')
self.assertEqual(self.tex.dimensions_blocks, (self.wb, self.hb), 'incorrect texture dimensions_blocks')
def test_blocks(self):
"""Test getting and setting blocks to BC1Texture"""
blocks = [[BC1Block.frombytes(bytes([x, y] + [0] * 6)) for x in range(self.wb)] for y in range(self.hb)]
for x in range(self.wb):
for y in range(self.hb):
self.tex[x, y] = blocks[y][x]
b = self.tex.tobytes()
for x in range(self.wb):
for y in range(self.hb):
index = (x + (y * self.wb)) * BC1Block.size
tb = self.tex[x, y]
fb = BC1Block.frombytes(b[index:index + BC1Block.size])
self.assertEqual(tb, blocks[y][x], 'incorrect block read from texture')
self.assertEqual(fb, blocks[y][x], 'incorrect block read from texture bytes')
self.assertEqual(self.tex[-1, -1], self.tex[self.wb - 1, self.hb - 1], 'incorrect negative subscripting')
with self.assertRaises(IndexError):
thing = self.tex[self.wb, self.hb]
with self.assertRaises(IndexError):
thing = self.tex[-1 - self.wb, -1 - self.hb]
def test_buffer(self):
"""Test the buffer protocol of BC1Texture"""
mv = memoryview(self.tex)
self.assertFalse(mv.readonly, 'buffer is readonly')
self.assertTrue(mv.c_contiguous, 'buffer is not contiguous')
self.assertEqual(mv.nbytes, self.size, 'buffer is the wrong size')
self.assertEqual(mv.format, 'B', 'buffer has the wrong format')
data = block_bytes * self.wb * self.hb
mv[:] = data
self.assertEqual(mv.tobytes(), data, 'incorrect buffer data')
@parameterized_class(
("name", "color_mode"), [
("4Color", BC1Encoder.ColorMode.FourColor),
("3Color", BC1Encoder.ColorMode.ThreeColor),
("3Color_Black", BC1Encoder.ColorMode.ThreeColorBlack),
])
class TestBC1Encoder(unittest.TestCase):
"""Test BC1 Encoder"""
"""Test BC1Encoder"""
def setUp(self):
self.bc1_encoder = bc1.BC1Encoder(5)
def test_block_size(self):
"""Ensure encoded block size is 8 bytes."""
out = self.bc1_encoder.encode_image(Blocks.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 get_class_name_blocks(cls, num, params_dict):
return "%s%s" % (cls.__name__, params_dict['color_mode'].name,)
@parameterized_class([
{"color_mode": ColorMode.FourColor},
{"color_mode": ColorMode.ThreeColor},
{"color_mode": ColorMode.ThreeColorBlack},
], class_name_func=get_class_name_blocks)
class TestBC1EncoderBlocks(unittest.TestCase):
"""Test BC1 encoder with a variety of inputs with 3 color blocks disabled."""
def setUp(self):
self.bc1_encoder = bc1.BC1Encoder(5, self.color_mode)
@classmethod
def setUpClass(cls):
cls.bc1_encoder = BC1Encoder(5, cls.color_mode)
def test_block_4color(self):
"""Test encoder output with 4 color greyscale testblock."""
out = BC1Block.frombytes(self.bc1_encoder.encode_image(Blocks.greyscale, 4, 4))
selectors = [[0, 2, 3, 1]] * 4
"""Test encoder output with 4 color greyscale test block"""
out_tex = self.bc1_encoder.encode(BC1Blocks.greyscale.texture)
self.assertFalse(out.is_3color(), "returned block color mode for greyscale test block")
self.assertEqual(selectors, out.selectors, "block has incorrect selectors for greyscale test block")
self.assertEqual(out_tex.dimensions_blocks, (1, 1), 'encoded texture has multiple blocks')
out_block = out_tex[0, 0]
self.assertFalse(out_block.is_3color, 'returned 3color mode for greyscale test block')
self.assertEqual(out_block, BC1Blocks.greyscale.block, 'encoded block is incorrect')
def test_block_3color(self):
"""Test encoder output with 3 color test block."""
out = BC1Block.frombytes(self.bc1_encoder.encode_image(Blocks.three_color, 4, 4))
selectors = [[1, 2, 2, 0]] * 4
"""Test encoder output with 3 color test block"""
out_tex = self.bc1_encoder.encode(BC1Blocks.three_color.texture)
if self.color_mode != ColorMode.FourColor: # we only care about the selectors if we are in 3 color mode
self.assertTrue(out.is_3color(), "returned 4-color block for 3 color test block")
self.assertEqual(selectors, out.selectors, "block has incorrect selectors for 3 color test block")
self.assertEqual(out_tex.dimensions_blocks, (1, 1), 'encoded texture has multiple blocks')
out_block = out_tex[0, 0]
if self.color_mode != BC1Encoder.ColorMode.FourColor: # we only care about the selectors if we are in 3 color mode
self.assertTrue(out_block.is_3color, 'returned 4-color block for 3 color test block')
self.assertEqual(out_block, BC1Blocks.three_color.block, 'encoded block is incorrect')
else:
self.assertFalse(out.is_3color(), "return 3-color block in 4-color mode")
self.assertFalse(out_block.is_3color, 'returned 3-color block in 4-color mode')
def test_block_3color_black(self):
"""Test encoder output with 3 color test block with black pixels."""
out = BC1Block.frombytes(self.bc1_encoder.encode_image(Blocks.three_color_black, 4, 4))
selectors = [[3, 1, 2, 0]] * 4
"""Test encoder output with 3 color test block with black pixels"""
out_tex = self.bc1_encoder.encode(BC1Blocks.three_color_black.texture)
if self.color_mode == ColorMode.ThreeColorBlack: # we only care about the selectors if we are in 3 color black mode
self.assertTrue(out.is_3color_black(), "returned 4-color block for 3 color test block with black")
self.assertEqual(selectors, out.selectors, "block has incorrect selectors for 3 color with black test block")
self.assertEqual(out_tex.dimensions_blocks, (1, 1), 'encoded texture has multiple blocks')
out_block = out_tex[0, 0]
has_black = 3 in [j for row in out_block.selectors for j in row]
if self.color_mode == BC1Encoder.ColorMode.ThreeColorBlack: # we only care about the selectors if we are in 3 color black mode
self.assertTrue(out_block.is_3color, 'returned 4-color block for 3 color test block with black')
self.assertTrue(has_black, 'block does not have black pixels as expected')
self.assertEqual(out_block, BC1Blocks.three_color_black.block, "encoded block is incorrect")
elif self.color_mode == BC1Encoder.ColorMode.ThreeColor:
self.assertFalse(has_black and out_block.is_3color, 'returned 3color block with black pixels')
else:
self.assertFalse(out.is_3color_black(), "returned incorrect block color mode for 3 color with black test block")
self.assertFalse(out_block.is_3color, 'returned 3-color block in 4-color mode')
class TestBC1Decoder(unittest.TestCase):
"""Test BC1Decoder"""
@classmethod
def setUpClass(cls):
cls.bc1_decoder = BC1Decoder()
@parameterized.expand([
("4color", BC1Blocks.greyscale.block, BC1Blocks.greyscale.image),
("3color", BC1Blocks.three_color.block, BC1Blocks.three_color.image),
("3color_black", BC1Blocks.three_color_black.block, BC1Blocks.three_color_black.image)
])
def test_block(self, _, block, image):
"""Test decoder output for a single block"""
in_tex = BC1Texture(4, 4)
in_tex[0, 0] = block
out_tex = self.bc1_decoder.decode(in_tex)
self.assertEqual(out_tex.dimensions, (4, 4), 'decoded texture has incorrect dimensions')
out_img = Image.frombytes('RGBA', (4, 4), out_tex.tobytes())
img_diff = ImageChops.difference(out_img, image).convert('L')
img_hist = img_diff.histogram()
self.assertEqual(16, img_hist[0], 'decoded block is incorrect')
if __name__ == '__main__':

183
tests/test_bc4.py Normal file
View File

@ -0,0 +1,183 @@
import unittest
import nose
from parameterized import parameterized, parameterized_class
from quicktex.s3tc.bc4 import BC4Block, BC4Texture, BC4Encoder, BC4Decoder
from tests.images import BC4Blocks
from PIL import Image, ImageChops
if __name__ == '__main__':
nose.main()
class TestBC4Block(unittest.TestCase):
"""Tests for the BC1Block class"""
block_bytes = b'\xF0\x10\x88\x86\x68\xAC\xCF\xFA'
selectors = [[0, 1, 2, 3]] * 2 + [[4, 5, 6, 7]] * 2
endpoints = (240, 16)
def test_size(self):
"""Test the size and dimensions of BC4Block"""
self.assertEqual(BC4Block.size, 8, 'incorrect block size')
self.assertEqual(BC4Block.width, 4, 'incorrect block width')
self.assertEqual(BC4Block.height, 4, 'incorrect block width')
self.assertEqual(BC4Block.dimensions, (4, 4), 'incorrect block dimensions')
def test_buffer(self):
"""Test the buffer protocol of BC4Block"""
block = BC4Block()
mv = memoryview(block)
self.assertFalse(mv.readonly, 'buffer is readonly')
self.assertTrue(mv.c_contiguous, 'buffer is not contiguous')
self.assertEqual(mv.ndim, 1, 'buffer is multidimensional')
self.assertEqual(mv.nbytes, BC4Block.size, 'buffer is the wrong size')
self.assertEqual(mv.format, 'B', 'buffer has the wrong format')
mv[:] = self.block_bytes
self.assertEqual(mv.tobytes(), self.block_bytes, 'incorrect buffer data')
def test_constructor(self):
"""Test constructing a block out of endpoints and selectors"""
block = BC4Block(*self.endpoints, self.selectors)
self.assertEqual(block.tobytes(), self.block_bytes, 'incorrect block bytes')
self.assertEqual(block.selectors, self.selectors, 'incorrect selectors')
self.assertEqual(block.endpoints, self.endpoints, 'incorrect endpoints')
def test_frombytes(self):
"""Test constructing a block out of raw data"""
block = BC4Block.frombytes(self.block_bytes)
self.assertEqual(block.tobytes(), self.block_bytes, 'incorrect block bytes')
self.assertEqual(block.selectors, self.selectors, 'incorrect selectors')
self.assertEqual(block.endpoints, self.endpoints, 'incorrect endpoints')
def test_eq(self):
"""Test equality between two identical blocks"""
block1 = BC4Block.frombytes(self.block_bytes)
block2 = BC4Block.frombytes(self.block_bytes)
self.assertEqual(block1, block2, 'identical blocks not equal')
def test_values_6(self):
"""Test values of a 6-value block"""
block = BC4Block(8, 248, [[0] * 4] * 4)
self.assertEqual(block.values, [8, 248, 56, 104, 152, 200, 0, 255], 'incorrect values')
self.assertTrue(block.is_6value, 'incorrect is_6value')
def test_values_8(self):
"""Test values of an 8-value block"""
block = BC4Block(240, 16, [[0] * 4] * 4)
self.assertEqual(block.values, [240, 16, 208, 176, 144, 112, 80, 48], 'incorrect values')
self.assertFalse(block.is_6value, 'incorrect is_6value')
@parameterized_class(
("name", "w", "h", "wb", "hb"), [
("8x8", 8, 8, 2, 2),
("9x9", 9, 9, 3, 3),
("7x7", 7, 7, 2, 2),
("7x9", 7, 9, 2, 3)
])
class TestBC4Texture(unittest.TestCase):
def setUp(self):
self.tex = BC4Texture(self.w, self.h)
self.size = self.wb * self.hb * BC4Block.size
def test_size(self):
"""Test size of BC4Texture in bytes"""
self.assertEqual(self.tex.size, self.size, 'incorrect texture size')
self.assertEqual(len(self.tex.tobytes()), self.size, 'incorrect texture size from tobytes')
def test_dimensions(self):
"""Test dimensions of BC4Texture in pixels"""
self.assertEqual(self.tex.width, self.w, 'incorrect texture width')
self.assertEqual(self.tex.height, self.h, 'incorrect texture height')
self.assertEqual(self.tex.dimensions, (self.w, self.h), 'incorrect texture dimensions')
def test_dimensions_blocks(self):
"""Test dimensions of BC4Texture in blocks"""
self.assertEqual(self.tex.width_blocks, self.wb, 'incorrect texture width_blocks')
self.assertEqual(self.tex.height_blocks, self.hb, 'incorrect texture width_blocks')
self.assertEqual(self.tex.dimensions_blocks, (self.wb, self.hb), 'incorrect texture dimensions_blocks')
def test_blocks(self):
"""Test getting and setting blocks to BC4Texture"""
blocks = [[BC4Block.frombytes(bytes([x, y] + [0] * 6)) for x in range(self.wb)] for y in range(self.hb)]
for x in range(self.wb):
for y in range(self.hb):
self.tex[x, y] = blocks[y][x]
b = self.tex.tobytes()
for x in range(self.wb):
for y in range(self.hb):
index = (x + (y * self.wb)) * BC4Block.size
tb = self.tex[x, y]
fb = BC4Block.frombytes(b[index:index + BC4Block.size])
self.assertEqual(tb, blocks[y][x], 'incorrect block read from texture')
self.assertEqual(fb, blocks[y][x], 'incorrect block read from texture bytes')
self.assertEqual(self.tex[-1, -1], self.tex[self.wb - 1, self.hb - 1], 'incorrect negative subscripting')
with self.assertRaises(IndexError):
thing = self.tex[self.wb, self.hb]
with self.assertRaises(IndexError):
thing = self.tex[-1 - self.wb, -1 - self.hb]
def test_buffer(self):
"""Test the buffer protocol of BC4Texture"""
mv = memoryview(self.tex)
self.assertFalse(mv.readonly, 'buffer is readonly')
self.assertTrue(mv.c_contiguous, 'buffer is not contiguous')
self.assertEqual(mv.nbytes, self.size, 'buffer is the wrong size')
self.assertEqual(mv.format, 'B', 'buffer has the wrong format')
data = b'\xF0\x10\x88\x86\x68\xAC\xCF\xFA' * self.wb * self.hb
mv[:] = data
self.assertEqual(mv.tobytes(), data, 'incorrect buffer data')
class TestBC4Encoder(unittest.TestCase):
"""Test BC4Encoder"""
# 6-value blocks are not yet supported by the encoder so we only run one test
@classmethod
def setUpClass(cls):
cls.bc4_encoder = BC4Encoder(0)
def test_block(self):
"""Test encoder output with 8 value test block"""
out_tex = self.bc4_encoder.encode(BC4Blocks.eight_value.texture)
self.assertEqual(out_tex.dimensions_blocks, (1, 1), 'encoded texture has multiple blocks')
out_block = out_tex[0, 0]
self.assertFalse(out_block.is_6value, 'returned 6value mode')
self.assertEqual(out_block, BC4Blocks.eight_value.block, 'encoded block is incorrect')
class TestBC4Decoder(unittest.TestCase):
"""Test BC4Decoder"""
@classmethod
def setUpClass(cls):
cls.bc4_decoder = BC4Decoder(0)
@parameterized.expand([
("8value", BC4Blocks.eight_value.block, BC4Blocks.eight_value.image),
("6value", BC4Blocks.six_value.block, BC4Blocks.six_value.image),
])
def test_block(self, _, block, image):
"""Test decoder output for a single block"""
in_tex = BC4Texture(4, 4)
in_tex[0, 0] = block
out_tex = self.bc4_decoder.decode(in_tex)
self.assertEqual(out_tex.dimensions, (4, 4), 'decoded texture has incorrect dimensions')
out_img = Image.frombytes('RGBA', (4, 4), out_tex.tobytes())
img_diff = ImageChops.difference(out_img, image).convert('L')
img_hist = img_diff.histogram()
self.assertEqual(16, img_hist[0], 'decoded block is incorrect')

63
tests/test_texture.py Normal file
View File

@ -0,0 +1,63 @@
import unittest
import nose
import os.path
from tests.images import image_path
from quicktex import RawTexture
from PIL import Image
class TestRawTexture(unittest.TestCase):
boilerplate = Image.open(os.path.join(image_path, 'Boilerplate.png'))
boilerplate_bytes = boilerplate.tobytes('raw', 'RGBX')
width, height = boilerplate.size
size = width * height * 4
def setUp(self):
self.tex = RawTexture(self.width, self.height)
def test_size(self):
"""Test byte size and image dimensions"""
self.assertEqual(self.tex.size, self.size, "incorrect texture byte size")
self.assertEqual(self.tex.width, self.width, "incorrect texture width")
self.assertEqual(self.tex.height, self.height, "incorrect texture height")
self.assertEqual(self.tex.dimensions, (self.width, self.height), "incorrect texture dimensions")
def test_pixels(self):
"""Test getting and setting pixel values"""
color1 = (69, 13, 12, 0) # totally random color
color2 = (19, 142, 93, 44)
self.tex[0, 0] = color1
self.tex[-1, -1] = color2
data = self.tex.tobytes()
self.assertEqual(self.tex[0, 0], color1)
self.assertEqual(self.tex[-1, -1], color2)
self.assertEqual(tuple(data[0:4]), color1)
self.assertEqual(tuple(data[-4:]), color2)
with self.assertRaises(IndexError):
thing = self.tex[self.width, self.height]
with self.assertRaises(IndexError):
thing = self.tex[-1 - self.width, -1 - self.height]
def test_buffer(self):
"""Test the Buffer protocol implementation for RawTexture"""
mv = memoryview(self.tex)
self.assertFalse(mv.readonly, 'buffer is readonly')
self.assertTrue(mv.c_contiguous, 'buffer is not contiguous')
self.assertEqual(mv.nbytes, self.size, 'buffer is the wrong size')
self.assertEqual(mv.format, 'B', 'buffer has the wrong format')
mv[:] = self.boilerplate_bytes
self.assertEqual(mv.tobytes(), self.boilerplate_bytes, 'incorrect buffer data')
def test_frombytes(self):
"""Test the frombytes factory function"""
bytetex = RawTexture.frombytes(self.boilerplate_bytes, *self.boilerplate.size)
self.assertEqual(self.boilerplate_bytes, bytetex.tobytes(), 'Incorrect bytes after writing to buffer')
if __name__ == '__main__':
nose.main()

View File

@ -53,8 +53,8 @@ function(set_project_warnings project_name)
-Woverloaded-virtual # warn if you overload (not override) a virtual
# function
-Wpedantic # warn if non-standard C++ is used
-Wconversion # warn on type conversions that may lose data
-Wsign-conversion # warn on sign conversions
#-Wconversion # warn on type conversions that may lose data
#-Wsign-conversion # warn on sign conversions
-Wnull-dereference # warn if a null dereference is detected
-Wdouble-promotion # warn if float is implicit promoted to double
-Wformat=2 # warn on security issues around functions that format output

58
tools/stubgen.py Normal file
View File

@ -0,0 +1,58 @@
import inspect
import os.path
import tempfile
import pybind11_stubgen as sg
package = 'quicktex'
prefix = '_'
modules = set()
def find_submodules(pkg):
modules.add(pkg.__name__.split('.')[-1])
for element_name in dir(pkg):
element = getattr(pkg, element_name)
if inspect.ismodule(element):
find_submodules(element)
if __name__ == "__main__":
find_submodules(__import__(prefix + package))
pkgdir = os.path.abspath(os.curdir)
with tempfile.TemporaryDirectory() as out:
# generate stubs using mypy Stubgen
sg.main(['-o', out, '--root-module-suffix', "", prefix + package])
os.curdir = pkgdir
# walk resulting stubs and move them to their new location
for root, dirs, files in os.walk(out):
for stub_name in files:
# location of the extension module's stub file
ext_module = os.path.relpath(root, out)
if stub_name.split('.')[-1] != 'pyi':
continue
if stub_name != '__init__.pyi':
ext_module = os.path.join(ext_module, os.path.splitext(stub_name)[0])
# open and read the stub file and replace all extension module names with their python counterparts
with open(os.path.join(root, stub_name), 'r') as fp:
contents = fp.read()
for mod in modules:
new_mod = mod.replace(prefix, '')
contents = contents.replace(mod, new_mod)
# write out to the new location
py_module = ext_module.replace(prefix, '')
with open(os.path.join(os.curdir, *py_module.split('.'), '__init__.pyi'), 'w') as fp:
fp.write(contents)
print(ext_module + ' -> ' + fp.name)