Skip to content

Instantly share code, notes, and snippets.

@martinstarkov
Last active July 16, 2024 13:34
Show Gist options
  • Select an option

  • Save martinstarkov/5d812cfe4052c29f38a8adb84d78e8a1 to your computer and use it in GitHub Desktop.

Select an option

Save martinstarkov/5d812cfe4052c29f38a8adb84d78e8a1 to your computer and use it in GitHub Desktop.
Automatic OpenGL Vertex Buffer Layout Determination (Reflection) using Luple
#include <array>
#include <cstdint>
#include <iostream> // for debugging only
#include <type_traits>
#include <utility>
#include <vector>
// Luple from: https://github.com/alexpolt/luple
template <typename... TT>
struct type_list {
static const int size = sizeof...(TT);
template <typename... UU>
struct add {
using type = type_list<TT..., UU...>;
};
};
template <typename T, int N, int M = 0>
struct tlist_get;
template <int N, int M, typename T, typename... TT>
struct tlist_get<type_list<T, TT...>, N, M> {
static_assert(N < (int)sizeof...(TT) + 1 + M, "type index out of bounds");
using type =
std::conditional_t<N == M, T,
typename tlist_get<type_list<TT...>, N, M + 1>::type>;
};
template <int N, int M>
struct tlist_get<type_list<>, N, M> {
using type = void;
};
template <int N>
struct tlist_get<type_list<>, N, 0> {};
template <typename T, int N>
using tlist_get_t = typename tlist_get<T, N>::type;
template <typename T, typename U, int N = 0>
struct tlist_get_n;
template <typename U, int N, typename T, typename... TT>
struct tlist_get_n<type_list<T, TT...>, U, N> {
static const int value = std::is_same<T, U>::value
? N
: tlist_get_n<type_list<TT...>, U, N + 1>::value;
};
template <typename U, int N>
struct tlist_get_n<type_list<>, U, N> {
static const int value = -1;
};
template <typename... TT>
struct has_reference;
template <typename T, typename... TT>
struct has_reference<T, TT...> : has_reference<TT...> {};
template <typename T, typename... TT>
struct has_reference<T&, TT...> {
static const bool value = true;
};
template <typename T, typename... TT>
struct has_reference<T&&, TT...> {
static const bool value = true;
};
template <>
struct has_reference<> {
static const bool value = false;
};
template <typename T>
struct luple_t;
template <typename T>
struct is_luple {
static const bool value = false;
};
template <typename T>
struct is_luple<luple_t<T>> {
static const bool value = true;
};
template <typename T, int N>
struct luple_element {
using value_type = tlist_get_t<T, N>;
value_type _value;
};
template <typename T, typename U>
struct luple_base;
template <typename... TT, int... NN>
struct luple_base<type_list<TT...>, std::integer_sequence<int, NN...>>
: luple_element<type_list<TT...>, NN>... {
using tlist = type_list<TT...>;
constexpr luple_base() {}
template <typename... UU>
constexpr luple_base(UU&&... args)
: luple_element<tlist, NN>{std::forward<UU>(args)}... {}
template <typename U>
constexpr luple_base(luple_t<U> const& o)
: luple_element<tlist, NN>{TT(o.template get<NN>())}... {}
template <typename U>
constexpr luple_base(luple_t<U>&& o)
: luple_element<tlist, NN>{TT(std::move(o.template get<NN>()))}... {
static_assert(!has_reference<TT...>::value,
"a converting constructor can't be used with reference "
"template parameters");
}
};
template <typename T>
struct luple_t : luple_base<T, std::make_integer_sequence<int, T::size>> {
using type_list = T;
using base = luple_base<T, std::make_integer_sequence<int, T::size>>;
static const int size = T::size;
constexpr luple_t() {}
template <typename... UU>
constexpr luple_t(UU&&... args) : base{std::forward<UU>(args)...} {
static_assert(sizeof...(UU) == size, "wrong number of arguments");
}
template <typename U>
constexpr luple_t(luple_t<U>& o)
: luple_t{const_cast<luple_t<U> const&>(o)} {}
template <typename U>
constexpr luple_t(luple_t<U> const& o) : base{o} {
static_assert(U::size == size, "sizes of luples do not match");
}
template <typename U>
constexpr luple_t(luple_t<U>&& o) : base{std::move(o)} {
static_assert(U::size == size, "sizes of luples do not match");
}
template <int N>
constexpr auto& get() {
static_assert(N < size, "luple::get -> out of bounds access");
return luple_element<T, N>::_value;
}
template <typename U>
constexpr auto& get() {
static_assert(tlist_get_n<T, U>::value != -1, "no such type in type list");
return luple_element<T, tlist_get_n<T, U>::value>::_value;
}
template <int N>
constexpr auto& get() const {
static_assert(N < T::size, "luple::get -> out of bounds access");
return luple_element<T, N>::_value;
}
template <typename U>
constexpr auto& get() const {
static_assert(tlist_get_n<T, U>::value != -1, "no such type in type list");
return luple_element<T, tlist_get_n<T, U>::value>::_value;
}
};
template <typename... TT>
using luple = luple_t<type_list<TT...>>;
template <int N, typename T>
constexpr auto& get(luple_t<T>& t) {
return t.template get<N>();
}
template <typename U, typename T>
constexpr auto& get(luple_t<T>& t) {
return t.template get<U>();
}
template <int N, typename T>
constexpr auto& get(luple_t<T> const& t) {
return t.template get<N>();
}
template <typename U, typename T>
constexpr auto& get(luple_t<T> const& t) {
return t.template get<U>();
}
template <typename T, int N>
using element_t = tlist_get_t<typename T::type_list, N>;
template <int... N, typename T0, typename T1>
constexpr void luple_do_impl(std::integer_sequence<int, N...>, T0& t, T1 fn) {
char dummy[] = {(fn(get<N>(t)), char{})...};
(void)dummy;
}
template <typename T0, typename T1>
constexpr void luple_do(T0& t, T1 fn) {
luple_do_impl(std::make_integer_sequence<int, T0::type_list::size>{}, t, fn);
}
template <typename... TT>
constexpr auto as_luple(TT... args) {
return luple<TT...>{std::move(args)...};
}
template <typename T, int N>
struct tag {
friend auto loophole(tag<T, N>);
constexpr friend int cloophole(tag<T, N>);
};
template <typename T, typename U, int N, bool B>
struct fn_def {
friend auto loophole(tag<T, N>) { return U{}; }
constexpr friend int cloophole(tag<T, N>) { return 0; }
};
template <typename T, typename U, int N>
struct fn_def<T, U, N, true> {};
template <typename T, int N>
struct c_op {
template <typename U, int M>
static auto ins(...) -> int;
template <typename U, int M, int = cloophole(tag<T, M>{})>
static auto ins(int) -> char;
template <typename U,
int = sizeof(fn_def<T, U, N, sizeof(ins<U, N>(0)) == sizeof(char)>)>
operator U();
};
template <typename T, int... NN>
constexpr int fields_number(...) {
return sizeof...(NN) - 1;
}
template <typename T, int... NN>
constexpr auto fields_number(int) -> decltype(T{c_op<T, NN>{}...}, 0) {
return fields_number<T, NN..., sizeof...(NN)>(0);
}
template <typename T, typename U>
struct loophole_type_list;
template <typename T, int... NN>
struct loophole_type_list<T, std::integer_sequence<int, NN...>> {
using type = type_list<decltype(loophole(tag<T, NN>{}))...>;
};
template <typename T>
using as_type_list = typename loophole_type_list<
T, std::make_integer_sequence<int, fields_number<T>(0)>>::type;
// End of Luple ------------------------------------------------------
// Predefined Vertex Types (must be used in Vertex struct)
namespace glsl {
using float_ = std::array<float, 1>;
using vec2 = std::array<float, 2>;
using vec3 = std::array<float, 3>;
using vec4 = std::array<float, 4>;
using double_ = std::array<double, 1>;
using dvec2 = std::array<double, 2>;
using dvec3 = std::array<double, 3>;
using dvec4 = std::array<double, 4>;
using bool_ = std::array<bool, 1>;
using bvec2 = std::array<bool, 2>;
using bvec3 = std::array<bool, 3>;
using bvec4 = std::array<bool, 4>;
using int_ = std::array<int, 1>;
using ivec2 = std::array<int, 2>;
using ivec3 = std::array<int, 3>;
using ivec4 = std::array<int, 4>;
using uint_ = std::array<unsigned int, 1>;
using uvec2 = std::array<unsigned int, 2>;
using uvec3 = std::array<unsigned int, 3>;
using uvec4 = std::array<unsigned int, 4>;
} // namespace glsl
enum class GLSLType : std::uint32_t {
None = 0,
Byte = 0x1400, // GL_BYTE
UnsignedByte = 0x1401, // GL_UNSIGNED_BYTE
Short = 0x1402, // GL_SHORT
UnsignedShort = 0x1403, // GL_UNSIGNED_SHORT
Int = 0x1404, // GL_INT
UnsignedInt = 0x1405, // GL_UNSIGNED_INT
Float = 0x1406, // GL_FLOAT
Double = 0x140A, // GL_DOUBLE
};
template <typename T, typename... Ts>
inline constexpr bool is_one_of_v{(std::is_same_v<T, Ts> || ...)};
template <typename T>
[[nodiscard]] GLSLType GetType() {
static_assert(
is_one_of_v<T, float, double, std::int32_t, std::uint32_t, std::int16_t,
std::uint16_t, std::int8_t, std::uint8_t, bool>,
"Cannot retrieve type which is not supported by OpenGL");
if constexpr (std::is_same_v<T, float>) {
return GLSLType::Float;
} else if constexpr (std::is_same_v<T, double>) {
return GLSLType::Double;
} else if constexpr (std::is_same_v<T, std::int32_t>) {
return GLSLType::Int;
} else if constexpr (std::is_same_v<T, std::uint32_t>) {
return GLSLType::UnsignedInt;
} else if constexpr (std::is_same_v<T, std::int16_t>) {
return GLSLType::Short;
} else if constexpr (std::is_same_v<T, std::uint16_t>) {
return GLSLType::UnsignedShort;
} else if constexpr (std::is_same_v<T, std::int8_t> ||
std::is_same_v<T, bool>) {
return GLSLType::Byte;
} else if constexpr (std::is_same_v<T, std::uint8_t>) {
return GLSLType::UnsignedByte;
}
}
template <typename T>
inline constexpr bool is_vertex_data_type{
is_one_of_v<T, glsl::float_, glsl::vec2, glsl::vec3, glsl::vec4,
glsl::double_, glsl::dvec2, glsl::dvec3, glsl::dvec4,
glsl::bool_, glsl::bvec2, glsl::bvec3, glsl::bvec4, glsl::int_,
glsl::ivec2, glsl::ivec3, glsl::ivec4, glsl::uint_, glsl::uvec2,
glsl::uvec3, glsl::uvec4>};
template <bool>
struct vertex_data : std::false_type {};
template <>
struct vertex_data<true> : std::true_type {};
template <typename TupleT, std::size_t... Is>
constexpr bool are_vertex_data_types(std::index_sequence<Is...>) {
return (is_vertex_data_type<element_t<TupleT, Is>> && ...);
}
template <typename T>
struct is_valid_vertex_type {
using vertex_type_list = as_type_list<T>;
using vertex_luple = luple_t<vertex_type_list>;
static constexpr bool value{are_vertex_data_types<vertex_luple>(
std::make_index_sequence<vertex_type_list::size>{})};
};
template <typename T>
inline constexpr bool is_valid_vertex{is_valid_vertex_type<T>::value};
template <typename T>
using valid_vertex = std::enable_if_t<is_valid_vertex<T>, bool>;
class BufferElement {
public:
BufferElement(std::uint16_t size_of_element, std::uint16_t count,
GLSLType type, bool normalized = false)
: size_{static_cast<std::uint16_t>(size_of_element * count)},
count_{count},
type_{type},
normalized_{normalized} {}
[[nodiscard]] std::uint16_t GetSize() const { return size_; }
[[nodiscard]] std::uint16_t GetCount() const { return count_; }
[[nodiscard]] GLSLType GetType() const { return type_; }
[[nodiscard]] bool IsNormalized() const { return normalized_; }
[[nodiscard]] std::size_t GetOffset() const { return offset_; }
void SetOffset(std::size_t new_offset) { offset_ = new_offset; }
private:
std::uint16_t size_{0}; // Number of elements x Size of element.
std::uint16_t count_{0}; // Number of elements
GLSLType type_{0}; // Type of buffer element (i.e. GL_FLOAT)
std::size_t offset_{0}; // Number of bytes from start of buffer.
bool normalized_{false}; // Whether or not the buffer elements are
// normalized. See here for more info:
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glVertexAttribPointer.xhtml
};
template <typename T, valid_vertex<T> = true>
std::pair<std::vector<BufferElement>, std::int32_t> DeduceLayoutAndStride() {
using data_luple = luple_t<as_type_list<T>>;
T v{};
auto& l = reinterpret_cast<data_luple&>(v);
std::vector<BufferElement> elements;
luple_do(l, [&](auto& value) {
using glsl_type = typename std::remove_const_t<
typename std::remove_reference_t<decltype(value)>>;
static_assert(is_vertex_data_type<glsl_type>,
"Buffer element type must be a valid vertex data type");
using element_type = typename glsl_type::value_type;
elements.emplace_back(static_cast<std::uint16_t>(sizeof(element_type)),
static_cast<std::uint16_t>(value.size()),
GetType<element_type>());
});
std::int32_t offset = 0;
std::int32_t stride = 0;
for (BufferElement& element : elements) {
element.SetOffset(offset);
offset += element.GetSize();
}
stride = offset;
return {elements, stride};
}
void SetLayout(std::vector<BufferElement>& elements, std::int32_t stride) {
for (std::uint32_t i = 0; i < elements.size(); ++i) {
const BufferElement& element{elements[i]};
std::cout << "#" << i << ": ";
std::cout << "(count=" << element.GetCount() << ")";
std::cout << "(type=" << static_cast<std::uint32_t>(element.GetType())
<< ")";
std::cout << "(normalized=" << element.IsNormalized() << ")";
std::cout << "(stride=" << stride << ")";
std::cout << "(offset=" << element.GetOffset() << ")";
std::cout << std::endl;
// Won't compile unless you have OpenGL:
// glEnableVertexAttribArray(i);
// glVertexAttribPointer(i, element.GetCount(),
// static_cast<GLenum>(element.GetType()), element.IsNormalized() ? GL_TRUE :
// GL_FALSE, stride, (const void*)element.GetOffset());
// Note: glDisableVertexAttribArray(i) not required according to:
// https://stackoverflow.com/a/12428035
}
}
struct TestVertex {
glsl::float_ a;
glsl::ivec3 pos;
glsl::dvec4 color;
};
int main() {
auto [elements, stride] = DeduceLayoutAndStride<TestVertex>();
SetLayout(elements, stride);
// Output:
// #0: (count=1)(type=5126)(normalized=0)(stride=48)(offset=0)
// #1: (count=3)(type=5124)(normalized=0)(stride=48)(offset=4)
// #2: (count=4)(type=5130)(normalized=0)(stride=48)(offset=16)
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment