mirror of
https://github.com/drewcassidy/quicktex.git
synced 2024-09-13 06:37:34 +00:00
Merge branch 'feature-cli'
This commit is contained in:
commit
6578729e1c
8
.gitignore
vendored
8
.gitignore
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
6
docs/reference/dds.rst
Normal file
@ -0,0 +1,6 @@
|
||||
dds module
|
||||
==========
|
||||
|
||||
.. automodule:: quicktex.dds
|
||||
:members:
|
||||
|
5
docs/reference/image_utils.rst
Normal file
5
docs/reference/image_utils.rst
Normal file
@ -0,0 +1,5 @@
|
||||
image_utils module
|
||||
==================
|
||||
|
||||
.. automodule:: quicktex.image_utils
|
||||
:members:
|
@ -5,4 +5,6 @@ Reference
|
||||
:maxdepth: 2
|
||||
|
||||
conversion.rst
|
||||
dds.rst
|
||||
image_utils.rst
|
||||
formats/index.rst
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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;
|
||||
|
@ -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
124
quicktex/ColorBlock.h
Normal 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
68
quicktex/Decoder.h
Normal 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
72
quicktex/Encoder.h
Normal 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
187
quicktex/Texture.h
Normal 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
|
@ -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
236
quicktex/_bindings.h
Normal 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
15
quicktex/cli/__init__.py
Normal 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
16
quicktex/cli/decode.py
Normal 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
6
quicktex/cli/encode.py
Normal 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
315
quicktex/dds.py
Normal 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
64
quicktex/image_utils.py
Normal 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
|
@ -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
0
quicktex/py.typed
Normal 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
|
||||
|
44
quicktex/s3tc/bc1/BC1Block.cpp
Normal file
44
quicktex/s3tc/bc1/BC1Block.cpp
Normal 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
|
@ -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
|
@ -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
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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; }
|
||||
|
@ -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
|
@ -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; }
|
||||
|
@ -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
|
66
quicktex/s3tc/bc4/BC4Block.cpp
Normal file
66
quicktex/s3tc/bc4/BC4Block.cpp
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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
|
@ -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; }
|
||||
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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()); }
|
||||
|
||||
|
@ -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
|
@ -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()); }
|
||||
|
||||
|
@ -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
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
13
setup.py
13
setup.py
@ -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
0
tests/__init__.py
Normal 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)}
|
||||
]
|
@ -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
|
@ -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);
|
||||
}
|
@ -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]))
|
@ -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
183
tests/test_bc4.py
Normal 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
63
tests/test_texture.py
Normal 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()
|
@ -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
58
tools/stubgen.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user