Last active
July 16, 2024 13:34
-
-
Save martinstarkov/5d812cfe4052c29f38a8adb84d78e8a1 to your computer and use it in GitHub Desktop.
Automatic OpenGL Vertex Buffer Layout Determination (Reflection) using Luple
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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