Browse Source

python/magum/math: switch to our own buffer protocol implementation.

Also don't needlessly define buffer protocol again for subclasses (such
as Matrix3 or Color4) as the base can handle that just fine.
pull/2/head
Vladimír Vondruš 7 years ago
parent
commit
d1d6cb9ec0
  1. 50
      src/python/magnum/math.cpp
  2. 35
      src/python/magnum/math.h
  3. 83
      src/python/magnum/math.matrix.h
  4. 9
      src/python/magnum/math.matrixdouble.cpp
  5. 9
      src/python/magnum/math.matrixfloat.cpp
  6. 121
      src/python/magnum/math.vector.h
  7. 9
      src/python/magnum/math.vectorfloat.cpp
  8. 56
      src/python/magnum/test/test_math.py

50
src/python/magnum/math.cpp

@ -37,6 +37,56 @@
namespace magnum { namespace magnum {
/* Keep in sync with math.h */
const char* const FormatStrings[]{
/* 0. Representing bytes as unsigned. Not using 'c' because then it behaves
differently from bytes/bytearray, where you can do `a[0] = ord('A')`. */
"B",
"b", /* 1 -- std::int8_t */
"B", /* 2 -- std::uint8_t */
"i", /* 3 -- std::int32_t */
"I", /* 4 -- std::uint32_t */
"f", /* 5 -- float */
"d" /* 6 -- double */
};
/* Flipped as numpy expects row-major */
const Py_ssize_t MatrixShapes[][2]{
{2, 2}, /* 0 -- 2 cols, 2 rows */
{3, 2}, /* 1 -- 2 cols, 3 rows */
{4, 2}, /* 2 -- 2 cols, 4 rows */
{2, 3}, /* 3 -- 3 cols, 2 rows */
{3, 3}, /* 4 -- 3 cols, 3 rows */
{4, 3}, /* 5 -- 3 cols, 4 rows */
{2, 4}, /* 6 -- 4 cols, 2 rows */
{3, 4}, /* 7 -- 4 cols, 3 rows */
{4, 4} /* 8 -- 4 cols, 4 rows */
};
const Py_ssize_t MatrixStridesFloat[][2]{
{4, 4*2}, /* 0 -- 2 cols, 2 rows */
{4, 4*3}, /* 1 -- 2 cols, 3 rows */
{4, 4*4}, /* 2 -- 2 cols, 4 rows */
{4, 4*2}, /* 3 -- 3 cols, 2 rows */
{4, 4*3}, /* 4 -- 3 cols, 3 rows */
{4, 4*4}, /* 5 -- 3 cols, 4 rows */
{4, 4*2}, /* 6 -- 4 cols, 2 rows */
{4, 4*3}, /* 7 -- 4 cols, 3 rows */
{4, 4*4} /* 8 -- 4 cols, 4 rows */
};
const Py_ssize_t MatrixStridesDouble[][2]{
{8, 8*2}, /* 0 -- 2 cols, 2 rows */
{8, 8*3}, /* 1 -- 2 cols, 3 rows */
{8, 8*4}, /* 2 -- 2 cols, 4 rows */
{8, 8*2}, /* 3 -- 3 cols, 2 rows */
{8, 8*3}, /* 4 -- 3 cols, 3 rows */
{8, 8*4}, /* 5 -- 3 cols, 4 rows */
{8, 8*2}, /* 6 -- 4 cols, 2 rows */
{8, 8*3}, /* 7 -- 4 cols, 3 rows */
{8, 8*4} /* 8 -- 4 cols, 4 rows */
};
namespace { namespace {
template<class T> void angle(py::class_<T>& c) { template<class T> void angle(py::class_<T>& c) {

35
src/python/magnum/math.h

@ -26,6 +26,7 @@
*/ */
#include <sstream> #include <sstream>
#include <Python.h>
#include <Corrade/Utility/Debug.h> #include <Corrade/Utility/Debug.h>
#include <Magnum/Magnum.h> #include <Magnum/Magnum.h>
@ -33,6 +34,40 @@
namespace magnum { namespace magnum {
/* Keep in sync with math.cpp */
extern const char* const FormatStrings[];
template<class> constexpr std::size_t formatIndex();
template<> constexpr std::size_t formatIndex<char>() { return 0; }
template<> constexpr std::size_t formatIndex<Byte>() { return 1; }
template<> constexpr std::size_t formatIndex<UnsignedByte>() { return 2; }
template<> constexpr std::size_t formatIndex<Int>() { return 3; }
template<> constexpr std::size_t formatIndex<UnsignedInt>() { return 4; }
template<> constexpr std::size_t formatIndex<Float>() { return 5; }
template<> constexpr std::size_t formatIndex<Double>() { return 6; }
extern const Py_ssize_t MatrixShapes[][2];
template<UnsignedInt cols, UnsignedInt rows> constexpr std::size_t matrixShapeStrideIndex();
template<> constexpr std::size_t matrixShapeStrideIndex<2, 2>() { return 0; }
template<> constexpr std::size_t matrixShapeStrideIndex<2, 3>() { return 1; }
template<> constexpr std::size_t matrixShapeStrideIndex<2, 4>() { return 2; }
template<> constexpr std::size_t matrixShapeStrideIndex<3, 2>() { return 3; }
template<> constexpr std::size_t matrixShapeStrideIndex<3, 3>() { return 4; }
template<> constexpr std::size_t matrixShapeStrideIndex<3, 4>() { return 5; }
template<> constexpr std::size_t matrixShapeStrideIndex<4, 2>() { return 6; }
template<> constexpr std::size_t matrixShapeStrideIndex<4, 3>() { return 7; }
template<> constexpr std::size_t matrixShapeStrideIndex<4, 4>() { return 8; }
extern const Py_ssize_t MatrixStridesFloat[][2];
extern const Py_ssize_t MatrixStridesDouble[][2];
template<class> constexpr const Py_ssize_t* matrixStridesFor(std::size_t i);
template<> constexpr const Py_ssize_t* matrixStridesFor<Float>(std::size_t i) {
return MatrixStridesFloat[i];
}
template<> constexpr const Py_ssize_t* matrixStridesFor<Double>(std::size_t i) {
return MatrixStridesDouble[i];
}
template<class T> std::string repr(const T& value) { template<class T> std::string repr(const T& value) {
std::ostringstream out; std::ostringstream out;
Debug{&out, Debug::Flag::NoNewlineAtTheEnd} << value; Debug{&out, Debug::Flag::NoNewlineAtTheEnd} << value;

83
src/python/magnum/math.matrix.h

@ -27,11 +27,13 @@
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <pybind11/operators.h> #include <pybind11/operators.h>
#include <Corrade/Containers/ScopeGuard.h>
#include <Corrade/Utility/FormatStl.h> #include <Corrade/Utility/FormatStl.h>
#include <Magnum/Math/Matrix3.h> #include <Magnum/Math/Matrix3.h>
#include <Magnum/Math/Matrix4.h> #include <Magnum/Math/Matrix4.h>
#include "corrade/PybindExtras.h" #include "corrade/PybindExtras.h"
#include "corrade/PyBuffer.h"
#include "magnum/math.h" #include "magnum/math.h"
@ -44,10 +46,10 @@ template<class T> struct VectorTraits<2, T> { typedef Math::Vector2<T> Type; };
template<class T> struct VectorTraits<3, T> { typedef Math::Vector3<T> Type; }; template<class T> struct VectorTraits<3, T> { typedef Math::Vector3<T> Type; };
template<class T> struct VectorTraits<4, T> { typedef Math::Vector4<T> Type; }; template<class T> struct VectorTraits<4, T> { typedef Math::Vector4<T> Type; };
template<class U, class T> void initFromBuffer(T& out, const py::buffer_info& info) { template<class U, class T> void initFromBuffer(T& out, const Py_buffer& buffer) {
for(std::size_t i = 0; i != T::Cols; ++i) for(std::size_t i = 0; i != T::Cols; ++i)
for(std::size_t j = 0; j != T::Rows; ++j) for(std::size_t j = 0; j != T::Rows; ++j)
out[i][j] = static_cast<typename T::Type>(*reinterpret_cast<const U*>(static_cast<const char*>(info.ptr) + i*info.strides[1] + j*info.strides[0])); out[i][j] = static_cast<typename T::Type>(*reinterpret_cast<const U*>(static_cast<const char*>(buffer.buf) + i*buffer.strides[1] + j*buffer.strides[0]));
} }
/* Called for both Matrix3x3 and Matrix3 in order to return a proper type / /* Called for both Matrix3x3 and Matrix3 in order to return a proper type /
@ -67,24 +69,30 @@ template<class T, class ...Args> void everyRectangularMatrix(py::class_<T, Args.
.def(py::init(), "Default constructor") .def(py::init(), "Default constructor")
.def(py::init<typename T::Type>(), "Construct a matrix with one value for all components") .def(py::init<typename T::Type>(), "Construct a matrix with one value for all components")
/* Buffer protocol, needed in order to make numpy treat the matric /* Buffer protocol, needed in order to properly detect row-major
correctly as column-major. Has to be defined *before* the from-tuple layouts. Has to be defined *before* the from-tuple constructor so it
constructor so it gets precedence for types that implement the gets precedence for types that implement the buffer protocol. */
buffer protocol. */ .def(py::init([](py::buffer other) {
.def(py::init([](py::buffer buffer) { Py_buffer buffer{};
py::buffer_info info = buffer.request(); if(PyObject_GetBuffer(other.ptr(), &buffer, PyBUF_FORMAT|PyBUF_STRIDES) != 0)
throw py::error_already_set{};
if(info.ndim != 2) Containers::ScopeGuard e{&buffer, PyBuffer_Release};
throw py::buffer_error{Utility::formatString("expected 2 dimensions but got {}", info.ndim)};
if(info.shape[0] != T::Rows ||info.shape[1] != T::Cols) if(buffer.ndim != 2)
throw py::buffer_error{Utility::formatString("expected {}x{} elements but got {}x{}", T::Cols, T::Rows, info.shape[1], info.shape[0])}; throw py::buffer_error{Utility::formatString("expected 2 dimensions but got {}", buffer.ndim)};
if(buffer.shape[0] != T::Rows || buffer.shape[1] != T::Cols)
throw py::buffer_error{Utility::formatString("expected {}x{} elements but got {}x{}", T::Cols, T::Rows, buffer.shape[1], buffer.shape[0])};
T out{Math::NoInit}; T out{Math::NoInit};
if(info.format == "f") initFromBuffer<Float>(out, info); /* Expecting just an one-letter format */
else if(info.format == "d") initFromBuffer<Double>(out, info); if(buffer.format[0] == 'f' && !buffer.format[1])
else throw py::buffer_error{Utility::formatString("expected format f or d but got {}", info.format)}; initFromBuffer<Float>(out, buffer);
else if(buffer.format[0] == 'd' && !buffer.format[1])
initFromBuffer<Double>(out, buffer);
else throw py::buffer_error{Utility::formatString("expected format f or d but got {}", buffer.format)};
return out; return out;
}), "Construct from a buffer") }), "Construct from a buffer")
@ -113,6 +121,31 @@ template<class T, class ...Args> void everyRectangularMatrix(py::class_<T, Args.
}, "Values on diagonal"); }, "Values on diagonal");
} }
template<class T> bool rectangularMatrixBufferProtocol(T& self, Py_buffer& buffer, int flags) {
/* I hate the const_casts but I assume this is to make editing easier, NOT
to make it possible for users to stomp on these values. */
buffer.ndim = 2;
buffer.itemsize = sizeof(typename T::Type);
buffer.len = sizeof(T);
buffer.buf = self.data();
buffer.readonly = false;
if((flags & PyBUF_FORMAT) == PyBUF_FORMAT)
buffer.format = const_cast<char*>(FormatStrings[formatIndex<typename T::Type>()]);
if(flags != PyBUF_SIMPLE) {
/* Reusing shape definitions from matrices because I don't want to
create another useless array for that and reinterpret_cast on the
buffer.internal is UGLY. It's flipped from column-major to
row-major, so adjusting the row instead. */
buffer.shape = const_cast<Py_ssize_t*>(MatrixShapes[matrixShapeStrideIndex<T::Cols, T::Rows>()]);
CORRADE_INTERNAL_ASSERT(buffer.shape[0] == T::Rows);
CORRADE_INTERNAL_ASSERT(buffer.shape[1] == T::Cols);
if((flags & PyBUF_STRIDES) == PyBUF_STRIDES)
buffer.strides = const_cast<Py_ssize_t*>(matrixStridesFor<typename T::Type>(matrixShapeStrideIndex<T::Cols, T::Rows>()));
}
return true;
}
template<class T> void rectangularMatrix(py::class_<T>& c) { template<class T> void rectangularMatrix(py::class_<T>& c) {
/* /*
Missing APIs: Missing APIs:
@ -128,21 +161,6 @@ template<class T> void rectangularMatrix(py::class_<T>& c) {
*/ */
c c
/* Buffer protocol, needed in order to make numpy treat the matrix
correctly as column-major. The constructor is defined in
everyRectangularMatrix(). */
.def_buffer([](const T& self) -> py::buffer_info {
// TODO: ownership?
return py::buffer_info{
const_cast<typename T::Type*>(self.data()),
sizeof(typename T::Type),
py::format_descriptor<typename T::Type>::format(),
2,
{T::Rows, T::Cols},
{sizeof(typename T::Type), sizeof(typename T::Type)*T::Rows}
};
})
/* Comparison */ /* Comparison */
.def(py::self == py::self, "Equality comparison") .def(py::self == py::self, "Equality comparison")
.def(py::self != py::self, "Non-equality comparison") .def(py::self != py::self, "Non-equality comparison")
@ -170,6 +188,11 @@ template<class T> void rectangularMatrix(py::class_<T>& c) {
.def("__repr__", repr<T>, "Object representation"); .def("__repr__", repr<T>, "Object representation");
/* Buffer protocol, needed in order to make numpy treat the matrix
correctly as column-major. The constructor is defined in
everyRectangularMatrix(). */
corrade::enableBetterBufferProtocol<T, rectangularMatrixBufferProtocol>(c);
/* Matrix column count */ /* Matrix column count */
char lenDocstring[] = "Matrix column count. Returns _."; char lenDocstring[] = "Matrix column count. Returns _.";
lenDocstring[sizeof(lenDocstring) - 3] = '0' + T::Cols; lenDocstring[sizeof(lenDocstring) - 3] = '0' + T::Cols;

9
src/python/magnum/math.matrixdouble.cpp

@ -40,8 +40,13 @@ void mathMatrixDouble(py::module& root) {
py::class_<Matrix4x3d> matrix4x3d{root, "Matrix4x3d", "4x3 double matrix", py::buffer_protocol{}}; py::class_<Matrix4x3d> matrix4x3d{root, "Matrix4x3d", "4x3 double matrix", py::buffer_protocol{}};
py::class_<Matrix4x4d> matrix4x4d{root, "Matrix4x4d", "4x4 double matrix", py::buffer_protocol{}}; py::class_<Matrix4x4d> matrix4x4d{root, "Matrix4x4d", "4x4 double matrix", py::buffer_protocol{}};
py::class_<Matrix3d, Matrix3x3d> matrix3d{root, "Matrix3d", "2D double transformation matrix", py::buffer_protocol{}}; /* The subclasses don't have buffer protocol enabled, as that's already
py::class_<Matrix4d, Matrix4x4d> matrix4d{root, "Matrix4d", "3D double transformation matrix", py::buffer_protocol{}}; done by the base classes. Moreover, just adding py::buffer_protocol{}
would cause it to not find the buffer functions as we don't add them
anywhere, thus failing with `pybind11_getbuffer(): Internal error`. */
py::class_<Matrix3d, Matrix3x3d> matrix3d{root, "Matrix3d", "2D double transformation matrix"};
py::class_<Matrix4d, Matrix4x4d> matrix4d{root, "Matrix4d", "3D double transformation matrix"};
matrices<Double>( matrices<Double>(
matrix2x2d, matrix2x3d, matrix2x4d, matrix2x2d, matrix2x3d, matrix2x4d,

9
src/python/magnum/math.matrixfloat.cpp

@ -40,8 +40,13 @@ void mathMatrixFloat(py::module& root) {
py::class_<Matrix4x3> matrix4x3{root, "Matrix4x3", "4x3 float matrix", py::buffer_protocol{}}; py::class_<Matrix4x3> matrix4x3{root, "Matrix4x3", "4x3 float matrix", py::buffer_protocol{}};
py::class_<Matrix4x4> matrix4x4{root, "Matrix4x4", "4x4 float matrix", py::buffer_protocol{}}; py::class_<Matrix4x4> matrix4x4{root, "Matrix4x4", "4x4 float matrix", py::buffer_protocol{}};
py::class_<Matrix3, Matrix3x3> matrix3{root, "Matrix3", "2D float transformation matrix", py::buffer_protocol{}}; /* The subclasses don't have buffer protocol enabled, as that's already
py::class_<Matrix4, Matrix4x4> matrix4{root, "Matrix4", "3D float transformation matrix", py::buffer_protocol{}}; done by the base classes. Moreover, just adding py::buffer_protocol{}
would cause it to not find the buffer functions as we don't add them
anywhere, thus failing with `pybind11_getbuffer(): Internal error`. */
py::class_<Matrix3, Matrix3x3> matrix3{root, "Matrix3", "2D float transformation matrix"};
py::class_<Matrix4, Matrix4x4> matrix4{root, "Matrix4", "3D float transformation matrix"};
matrices<Float>( matrices<Float>(
matrix2x2, matrix2x3, matrix2x4, matrix2x2, matrix2x3, matrix2x4,

121
src/python/magnum/math.vector.h

@ -26,53 +26,55 @@
*/ */
#include <pybind11/operators.h> #include <pybind11/operators.h>
#include <Corrade/Containers/ScopeGuard.h>
#include <Corrade/Utility/FormatStl.h> #include <Corrade/Utility/FormatStl.h>
#include <Magnum/Math/Color.h> #include <Magnum/Math/Color.h>
#include <Magnum/Math/Vector4.h> #include <Magnum/Math/Vector4.h>
#include "corrade/PybindExtras.h" #include "corrade/PybindExtras.h"
#include "corrade/PyBuffer.h"
#include "magnum/math.h" #include "magnum/math.h"
namespace magnum { namespace magnum {
template<class> bool isTypeCompatible(const std::string&); template<class> constexpr bool isTypeCompatible(char);
template<> inline bool isTypeCompatible<Float>(const std::string& format) { template<> constexpr bool isTypeCompatible<Float>(char format) {
return format == "f" || format == "d"; return format == 'f' || format == 'd';
} }
template<> inline bool isTypeCompatible<Double>(const std::string& format) { template<> constexpr bool isTypeCompatible<Double>(char format) {
return format == "f" || format == "d"; return format == 'f' || format == 'd';
} }
template<> inline bool isTypeCompatible<Int>(const std::string& format) { template<> constexpr bool isTypeCompatible<Int>(char format) {
return format == "i" || format == "l"; return format == 'i' || format == 'l';
} }
template<> inline bool isTypeCompatible<UnsignedInt>(const std::string& format) { template<> constexpr bool isTypeCompatible<UnsignedInt>(char format) {
return format == "I" || format == "L"; return format == 'I' || format == 'L';
} }
template<class U, class T> void initFromBuffer(T& out, const py::buffer_info& info) { template<class U, class T> void initFromBuffer(T& out, const Py_buffer& buffer) {
for(std::size_t i = 0; i != T::Size; ++i) for(std::size_t i = 0; i != T::Size; ++i)
out[i] = static_cast<typename T::Type>(*reinterpret_cast<const U*>(static_cast<const char*>(info.ptr) + i*info.strides[0])); out[i] = static_cast<typename T::Type>(*reinterpret_cast<const U*>(static_cast<const char*>(buffer.buf) + i*buffer.strides[0]));
} }
/* Floating-point init */ /* Floating-point init */
template<class T> void initFromBuffer(T& out, const py::buffer_info& info, std::true_type, std::true_type) { template<class T> void initFromBuffer(typename std::enable_if<std::is_floating_point<typename T::Type>::value, T>::type& out, const Py_buffer& buffer) {
if(info.format == "f") initFromBuffer<Float>(out, info); if(buffer.format[0] == 'f') initFromBuffer<Float>(out, buffer);
else if(info.format == "d") initFromBuffer<Double>(out, info); else if(buffer.format[0] == 'd') initFromBuffer<Double>(out, buffer);
else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
} }
/* Signed integeral init */ /* Signed integral init */
template<class T> void initFromBuffer(T& out, const py::buffer_info& info, std::false_type, std::true_type) { template<class T> void initFromBuffer(typename std::enable_if<std::is_integral<typename T::Type>::value && std::is_signed<typename T::Type>::value, T>::type& out, const Py_buffer& buffer) {
if(info.format == "i") initFromBuffer<Int>(out, info); if(buffer.format[0] == 'i') initFromBuffer<Int>(out, buffer);
else if(info.format == "l") initFromBuffer<Long>(out, info); else if(buffer.format[0] == 'l') initFromBuffer<Long>(out, buffer);
else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
} }
/* Unsigned integeral init */ /* Unsigned integral init */
template<class T> void initFromBuffer(T& out, const py::buffer_info& info, std::false_type, std::false_type) { template<class T> void initFromBuffer(typename std::enable_if<std::is_integral<typename T::Type>::value && std::is_unsigned<typename T::Type>::value, T>::type& out, const Py_buffer& buffer) {
if(info.format == "I") initFromBuffer<UnsignedInt>(out, info); if(buffer.format[0] == 'I') initFromBuffer<UnsignedInt>(out, buffer);
else if(info.format == "L") initFromBuffer<UnsignedLong>(out, info); else if(buffer.format[0] == 'L') initFromBuffer<UnsignedLong>(out, buffer);
else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
} }
@ -91,25 +93,6 @@ template<class T, class ...Args> void everyVector(py::class_<T, Args...>& c) {
}, "Construct a zero vector") }, "Construct a zero vector")
.def(py::init(), "Default constructor") .def(py::init(), "Default constructor")
/* Ideally, only the constructor (in vectorBuffer()) would be needed
(and thus also no py::buffer_protocol() specified for the class),
but conversion of vectors to lists is extremely slow due to pybind
exceptions being somehow extra heavy compared to native python ones,
so in order to have acceptable performance we need the buffer
protocol on the other side as well. See test/benchmark_math.py for
more information. */
.def_buffer([](const T& self) -> py::buffer_info {
// TODO: ownership?
return py::buffer_info{
const_cast<typename T::Type*>(self.data()),
sizeof(typename T::Type),
py::format_descriptor<typename T::Type>::format(),
1,
{T::Size},
{sizeof(typename T::Type)}
};
})
/* Operators */ /* Operators */
.def(-py::self, "Negated vector") .def(-py::self, "Negated vector")
.def(py::self += py::self, "Add and assign a vector") .def(py::self += py::self, "Add and assign a vector")
@ -135,24 +118,53 @@ template<class T, class ...Args> void vectorBuffer(py::class_<T, Args...>& c) {
/* Buffer protocol. If not present, implicit conversion from numpy /* Buffer protocol. If not present, implicit conversion from numpy
arrays of non-default types somehow doesn't work. There's also the arrays of non-default types somehow doesn't work. There's also the
other part in vectorBuffer(). */ other part in vectorBuffer(). */
.def(py::init([](py::buffer buffer) { .def(py::init([](py::buffer other) {
py::buffer_info info = buffer.request(); Py_buffer buffer{};
if(PyObject_GetBuffer(other.ptr(), &buffer, PyBUF_FORMAT|PyBUF_STRIDES) != 0)
throw py::error_already_set{};
Containers::ScopeGuard e{&buffer, PyBuffer_Release};
if(info.ndim != 1) if(buffer.ndim != 1)
throw py::buffer_error{Utility::formatString("expected 1 dimension but got {}", info.ndim)}; throw py::buffer_error{Utility::formatString("expected 1 dimension but got {}", buffer.ndim)};
if(info.shape[0] != T::Size) if(buffer.shape[0] != T::Size)
throw py::buffer_error{Utility::formatString("expected {} elements but got {}", T::Size, info.shape[0])}; throw py::buffer_error{Utility::formatString("expected {} elements but got {}", T::Size, buffer.shape[0])};
if(!isTypeCompatible<typename T::Type>(info.format)) /* Expecting just an one-letter format */
throw py::buffer_error{Utility::formatString("unexpected format {} for a {} vector", info.format, py::format_descriptor<typename T::Type>::format())}; if(!buffer.format[0] || buffer.format[1] || !isTypeCompatible<typename T::Type>(buffer.format[0]))
throw py::buffer_error{Utility::formatString("unexpected format {} for a {} vector", buffer.format, FormatStrings[formatIndex<typename T::Type>()])};
T out{Math::NoInit}; T out{Math::NoInit};
initFromBuffer(out, info, std::is_floating_point<typename T::Type>{}, std::is_signed<typename T::Type>{}); initFromBuffer<T>(out, buffer);
return out; return out;
}), "Construct from a buffer"); }), "Construct from a buffer");
} }
template<class T> bool vectorBufferProtocol(T& self, Py_buffer& buffer, int flags) {
/* I hate the const_casts but I assume this is to make editing easier, NOT
to make it possible for users to stomp on these values. */
buffer.ndim = 1;
buffer.itemsize = sizeof(typename T::Type);
buffer.len = sizeof(T);
buffer.buf = self.data();
buffer.readonly = false;
if((flags & PyBUF_FORMAT) == PyBUF_FORMAT)
buffer.format = const_cast<char*>(FormatStrings[formatIndex<typename T::Type>()]);
if(flags != PyBUF_SIMPLE) {
/* Reusing shape definitions from matrices because I don't want to
create another useless array for that and reinterpret_cast on the
buffer.internal is UGLY. It's flipped from column-major to
row-major, so adjusting the row instead. */
buffer.shape = const_cast<Py_ssize_t*>(MatrixShapes[matrixShapeStrideIndex<2, T::Size>()]);
CORRADE_INTERNAL_ASSERT(buffer.shape[0] == T::Size);
if((flags & PyBUF_STRIDES) == PyBUF_STRIDES)
buffer.strides = &buffer.itemsize;
}
return true;
}
/* Things common for vectors of all sizes and types */ /* Things common for vectors of all sizes and types */
template<class T> void vector(py::module& m, py::class_<T>& c) { template<class T> void vector(py::module& m, py::class_<T>& c) {
/* /*
@ -203,6 +215,15 @@ template<class T> void vector(py::module& m, py::class_<T>& c) {
.def("__repr__", repr<T>, "Object representation"); .def("__repr__", repr<T>, "Object representation");
/* Ideally, only the constructor (in vectorBuffer()) would be needed
(and thus also no py::buffer_protocol() specified for the class),
but conversion of vectors to lists is extremely slow due to pybind
exceptions being somehow extra heavy compared to native python ones,
so in order to have acceptable performance we need the buffer
protocol on the other side as well. See test/benchmark_math.py for more
information. */
corrade::enableBetterBufferProtocol<T, vectorBufferProtocol>(c);
/* Vector length */ /* Vector length */
char lenDocstring[] = "Vector size. Returns _."; char lenDocstring[] = "Vector size. Returns _.";
lenDocstring[sizeof(lenDocstring) - 3] = '0' + T::Size; lenDocstring[sizeof(lenDocstring) - 3] = '0' + T::Size;

9
src/python/magnum/math.vectorfloat.cpp

@ -89,12 +89,17 @@ void mathVectorFloat(py::module& root, py::module& m) {
vectorsFloat<Float>(m, vector2, vector3, vector4); vectorsFloat<Float>(m, vector2, vector3, vector4);
vectorsFloat<Double>(m, vector2d, vector3d, vector4d); vectorsFloat<Double>(m, vector2d, vector3d, vector4d);
py::class_<Color3, Vector3> color3_{root, "Color3", "Color in linear RGB color space", py::buffer_protocol{}}; /* The subclasses don't have buffer protocol enabled, as that's already
done by the base classes. Moreover, just adding py::buffer_protocol{}
would cause it to not find the buffer functions as we don't add them
anywhere, thus failing with `pybind11_getbuffer(): Internal error`. */
py::class_<Color3, Vector3> color3_{root, "Color3", "Color in linear RGB color space"};
everyVector(color3_); everyVector(color3_);
color(color3_); color(color3_);
color3(color3_); color3(color3_);
py::class_<Color4, Vector4> color4_{root, "Color4", "Color in linear RGBA color space", py::buffer_protocol{}}; py::class_<Color4, Vector4> color4_{root, "Color4", "Color in linear RGBA color space"};
everyVector(color4_); everyVector(color4_);
color(color4_); color(color4_);
color4(color4_); color4(color4_);

56
src/python/magnum/test/test_math.py

@ -23,6 +23,7 @@
# DEALINGS IN THE SOFTWARE. # DEALINGS IN THE SOFTWARE.
# #
import array
import unittest import unittest
from magnum import * from magnum import *
@ -263,6 +264,14 @@ class Vector(unittest.TestCase):
def test_repr(self): def test_repr(self):
self.assertEqual(repr(Vector3(1.0, 3.14, -13.37)), 'Vector(1, 3.14, -13.37)') self.assertEqual(repr(Vector3(1.0, 3.14, -13.37)), 'Vector(1, 3.14, -13.37)')
def test_from_buffer(self):
a = Vector3i(array.array('i', [2, 3, 5]))
self.assertEqual(a, Vector3i(2, 3, 5))
def test_to_buffer(self):
a = memoryview(Vector4(1.0, 2.0, 3.0, 4.0))
self.assertEqual(a.tolist(), [1.0, 2.0, 3.0, 4.0])
class Color3_(unittest.TestCase): class Color3_(unittest.TestCase):
def test_init(self): def test_init(self):
a1 = Color3() a1 = Color3()
@ -350,6 +359,16 @@ class Color4_(unittest.TestCase):
self.assertIsInstance(Color4().rgb, Color3) self.assertIsInstance(Color4().rgb, Color3)
self.assertIsInstance(Color4().xyz, Color3) self.assertIsInstance(Color4().xyz, Color3)
def test_from_buffer(self):
a = Color3(array.array('f', [2.0, 3.0, 5.0]))
self.assertEqual(a, Color3(2.0, 3.0, 5.0))
def test_to_buffer(self):
# Color4 doesn't define py::buffer_protocol(), the one from base should
# "just work"
a = memoryview(Color4(1.0, 2.0, 3.0, 4.0))
self.assertEqual(a.tolist(), [1.0, 2.0, 3.0, 4.0])
class Matrix(unittest.TestCase): class Matrix(unittest.TestCase):
def test_init(self): def test_init(self):
a = Matrix3x2() a = Matrix3x2()
@ -582,6 +601,13 @@ class Matrix(unittest.TestCase):
' 2, 5,\n' ' 2, 5,\n'
' 3, 6)') ' 3, 6)')
# conversion from buffer is tested in test_math_numpy, array.array is
# one-dimensional and I don't want to drag numpy here just for one test
def test_to_buffer(self):
a = memoryview(Matrix2x2((1.0, 2.0), (3.0, 4.0)))
self.assertEqual(a.tolist(), [[1.0, 3.0], [2.0, 4.0]])
class Matrix3_(unittest.TestCase): class Matrix3_(unittest.TestCase):
def test_init(self): def test_init(self):
a = Matrix3() a = Matrix3()
@ -673,6 +699,20 @@ class Matrix3_(unittest.TestCase):
self.assertIsInstance(Matrix3().transposed(), Matrix3) self.assertIsInstance(Matrix3().transposed(), Matrix3)
self.assertIsInstance(Matrix3().inverted(), Matrix3) self.assertIsInstance(Matrix3().inverted(), Matrix3)
# conversion from buffer is tested in test_math_numpy, array.array is
# one-dimensional and I don't want to drag numpy here just for one test
def test_to_buffer(self):
# Matrix3 doesn't define py::buffer_protocol(), the one from base
# should "just work"
a = memoryview(Matrix3((1.0, 2.0, 3.0),
(4.0, 5.0, 6.0),
(7.0, 8.0, 9.0)))
self.assertEqual(a.tolist(), [
[1.0, 4.0, 7.0],
[2.0, 5.0, 8.0],
[3.0, 6.0, 9.0]])
class Matrix4_(unittest.TestCase): class Matrix4_(unittest.TestCase):
def test_init(self): def test_init(self):
a = Matrix4() a = Matrix4()
@ -778,6 +818,22 @@ class Matrix4_(unittest.TestCase):
self.assertIsInstance(Matrix4().transposed(), Matrix4) self.assertIsInstance(Matrix4().transposed(), Matrix4)
self.assertIsInstance(Matrix4().inverted(), Matrix4) self.assertIsInstance(Matrix4().inverted(), Matrix4)
# conversion from buffer is tested in test_math_numpy, array.array is
# one-dimensional and I don't want to drag numpy here just for one test
def test_to_buffer(self):
# Matrix3 doesn't define py::buffer_protocol(), the one from base
# should "just work"
a = memoryview(Matrix4((1.0, 2.0, 3.0, 4.0),
(5.0, 6.0, 7.0, 8.0),
(9.0, 10.0, 11.0, 12.0),
(13.0, 14.0, 15.0, 16.0)))
self.assertEqual(a.tolist(), [
[1.0, 5.0, 9.0, 13.0],
[2.0, 6.0, 10.0, 14.0],
[3.0, 7.0, 11.0, 15.0],
[4.0, 8.0, 12.0, 16.0]])
class Quaternion_(unittest.TestCase): class Quaternion_(unittest.TestCase):
def test_init(self): def test_init(self):
a = Quaternion() a = Quaternion()

Loading…
Cancel
Save