diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3e649942 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.vscode/ +.idea/ + +bits/ +cmake-build-*/ +bin/ +build/ + +CMakeUserPresets.json diff --git a/CMakeGraphVizOptions.cmake b/CMakeGraphVizOptions.cmake new file mode 100644 index 00000000..c05a2f01 --- /dev/null +++ b/CMakeGraphVizOptions.cmake @@ -0,0 +1,5 @@ +set(GRAPHVIZ_GRAPH_NAME "Draconic Engine dependency graph") +set(GRAPHVIZ_GRAPH_HEADER "node [ fontsize = \"10\" ];") +set(GRAPHVIZ_EXECUTABLES FALSE) +set(GRAPHVIZ_EXTERNAL_LIBS FALSE) +set(GRAPHVIZ_IGNORE_TARGETS "CMAKE_.*;test_.*|.*_test$") diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..6697d689 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 4.2) +#4.0: set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "a9e1cf81-9932-4810-974b-6eccaf14e457") +#4.2: set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") +project(DraconicEngine LANGUAGES C CXX) + +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +include(CTest) + +add_subdirectory(engine/native) \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..664e41eb --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,50 @@ +{ + "version": 10, + "cmakeMinimumRequired": { + "major": 4, + "minor": 0, + "patch": 3 + }, + "configurePresets": [ + { + "name": "default", + "hidden": true, + "displayName": "Default Config", + "description": "Base configuration", + "generator": "Ninja", + "graphviz": "graph/deps.dot", + "warnings": { + "unusedCli": false, + "dev": false + }, + "cacheVariables": { + "CMAKE_EXPERIMENTAL_CXX_IMPORT_STD": "d0edc3af-4c50-42ea-a356-e2862fe7a444", + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_STANDARD_REQUIRED": "ON", + "CMAKE_CXX_EXTENSIONS": "OFF", + "CMAKE_CXX_MODULE_STD": "1", + "CMAKE_CXX_FLAGS_INIT": "-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0" + } + }, + { + "name": "release", + "inherits": "default", + "displayName": "Release", + "description": "Release configuration, including C# support", + "binaryDir": "${sourceDir}/build/release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "debug", + "inherits": "default", + "displayName": "Debug", + "description": "Debug configuration, including C# support", + "binaryDir": "${sourceDir}/build/debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + } + ] +} diff --git a/README.md b/README.md index e48bea7d..815a7f52 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Draconic Engine

- - Draconic Engine Logo + + Draconic Engine Logo

diff --git a/icon.png b/assets/draconic_logo_no_text.png similarity index 100% rename from icon.png rename to assets/draconic_logo_no_text.png diff --git a/icon.svg b/assets/draconic_logo_no_text.svg similarity index 100% rename from icon.svg rename to assets/draconic_logo_no_text.svg diff --git a/logo.png b/assets/draconic_logo_text.png similarity index 100% rename from logo.png rename to assets/draconic_logo_text.png diff --git a/cmake/Compiler.cmake b/cmake/Compiler.cmake new file mode 100644 index 00000000..45258856 --- /dev/null +++ b/cmake/Compiler.cmake @@ -0,0 +1,23 @@ +include_guard(GLOBAL) + +include(CheckIPOSupported) +check_ipo_supported(RESULT IPO_SUPPORTED OUTPUT ERROR) + +if (CMAKE_BUILD_TYPE STREQUAL "Release") + if(IPO_SUPPORTED) + message(STATUS "IPO / LTO enabled") + add_link_options(-flto) + else() + message(STATUS "IPO / LTO not supported: <${ERROR}>") + endif() +else() + message(STATUS "IPO / LTO disabled") + add_compile_definitions(DEBUG) +endif() + +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + if (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + # TODO: Make SIMD level configurable or detect at runtime + add_compile_options(-mavx2 -mfma) + endif() +endif() \ No newline at end of file diff --git a/cmake/Modules.cmake b/cmake/Modules.cmake new file mode 100644 index 00000000..277065da --- /dev/null +++ b/cmake/Modules.cmake @@ -0,0 +1,110 @@ +include_guard(GLOBAL) + +set(NATIVE_SOURCE_DIR "${PROJECT_SOURCE_DIR}/engine/native") + +set(NATIVE_THIRD_PARTY_DIR "${NATIVE_SOURCE_DIR}/thirdparty") + +if (BUILD_TESTING) + message(STATUS "Bootstrapping unit tests module boost.ut") + add_library(boost_ut_main "${NATIVE_THIRD_PARTY_DIR}/boost/ut_main.cpp") + target_sources(boost_ut_main + PUBLIC + FILE_SET CXX_MODULES + BASE_DIRS "${NATIVE_THIRD_PARTY_DIR}/boost" + FILES "${NATIVE_THIRD_PARTY_DIR}/boost/ut.cppm" + ) + target_compile_features(boost_ut_main PUBLIC cxx_std_23) +endif() + +function(add_modules_library) + cmake_parse_arguments( + MOD_LIB # prefix for all variables + "STATIC;SHARED" # tags for flags (only defined ones will be true) + "" # tags for single values + "" # tags for lists + "${ARGN}" + ) + + set(LIB_PATH ${ARGV0}) + + if (NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${LIB_PATH}") + message(FATAL_ERROR "Library directory ${LIB_PATH} not found") + endif() + + set(LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/${LIB_PATH}") + + if (EXISTS "${LIB_DIR}/CMakeLists.txt") # allow recursion + add_subdirectory(${LIB_DIR}) + endif() + + string(MAKE_C_IDENTIFIER ${LIB_PATH} LIB_TARGET) + file(GLOB CPP_MODULE_FILES CONFIGURE_DEPENDS "${LIB_PATH}/*.cppm") + file(GLOB CPP_UNIT_TESTS CONFIGURE_DEPENDS "${LIB_PATH}/*.test.cpp") + file(GLOB CPP_SRC_FILES CONFIGURE_DEPENDS "${LIB_PATH}/*.cpp") + if (CPP_UNIT_TESTS) + list(REMOVE_ITEM CPP_SRC_FILES ${CPP_UNIT_TESTS}) + endif() + + if (MOD_LIB_SHARED) + message(STATUS "Adding shared modules library ${LIB_TARGET}") + add_library(${LIB_TARGET} SHARED) + else() + message(STATUS "Adding static modules library ${LIB_TARGET}") + add_library(${LIB_TARGET} STATIC) + endif() + target_compile_features(${LIB_TARGET} PUBLIC cxx_std_23) + target_include_directories(${LIB_TARGET} PUBLIC ${NATIVE_SOURCE_DIR}) + + target_sources(${LIB_TARGET} + PUBLIC + FILE_SET CXX_MODULES + BASE_DIRS ${LIB_DIR} + FILES ${CPP_MODULE_FILES} + ) + + target_sources(${LIB_TARGET} PRIVATE ${CPP_SRC_FILES}) + + if(CMAKE_TESTING_ENABLED) + foreach(UNIT_TEST_FILE ${CPP_UNIT_TESTS}) + string(REPLACE "${LIB_DIR}/" "" UNIT_TEST_TARGET "${UNIT_TEST_FILE}") + string(REPLACE ".test.cpp" "_test" UNIT_TEST_TARGET ${UNIT_TEST_TARGET}) + string(MAKE_C_IDENTIFIER ${UNIT_TEST_TARGET} UNIT_TEST_TARGET) + if (NOT UNIT_TEST_TARGET MATCHES ".*${LIB_TARGET}.*") + string(PREPEND UNIT_TEST_TARGET "${LIB_TARGET}_") + endif() + add_executable(${UNIT_TEST_TARGET} ${UNIT_TEST_FILE}) + target_compile_features(${UNIT_TEST_TARGET} PUBLIC cxx_std_23) + target_link_libraries(${UNIT_TEST_TARGET} PRIVATE boost_ut_main ${LIB_TARGET}) + message(STATUS "Unit test ${UNIT_TEST_TARGET}") + add_test(NAME ${UNIT_TEST_TARGET} COMMAND ${UNIT_TEST_TARGET} --reporter junit --out "Testing/${UNIT_TEST_TARGET}.xml") + endforeach() + endif() + +endfunction() + +function(target_link_modules) + cmake_parse_arguments( + MOD_LINK # prefix for all variables + "" # tags for flags (only defined ones will be true) + "" # tags for single values + "PRIVATE;PUBLIC" # tags for lists + "${ARGN}" + ) + + if (MOD_LINK_PUBLIC) + foreach(NAME ${MOD_LINK_PUBLIC}) + set(DIR "${CMAKE_CURRENT_SOURCE_DIR}/${NAME}") + string(MAKE_C_IDENTIFIER ${NAME} TARGET) + target_link_libraries(${ARGV0} PUBLIC ${TARGET}) + endforeach() + endif() + + if (MOD_LINK_PRIVATE) + foreach(NAME ${MOD_LINK_PRIVATE}) + set(DIR "${CMAKE_CURRENT_SOURCE_DIR}/${NAME}") + string(MAKE_C_IDENTIFIER ${NAME} TARGET) + target_link_libraries(${ARGV0} PRIVATE ${TARGET}) + endforeach() + endif() + +endfunction() \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..e69de29b diff --git a/engine/native/CMakeLists.txt b/engine/native/CMakeLists.txt new file mode 100644 index 00000000..65a4a5a7 --- /dev/null +++ b/engine/native/CMakeLists.txt @@ -0,0 +1,5 @@ +include(Compiler) +include(Modules) + +add_modules_library(core SHARED) +target_link_libraries(core PUBLIC definitions math) diff --git a/engine/native/core/CMakeLists.txt b/engine/native/core/CMakeLists.txt new file mode 100644 index 00000000..4474e103 --- /dev/null +++ b/engine/native/core/CMakeLists.txt @@ -0,0 +1,3 @@ +add_modules_library(definitions) +add_modules_library(math) +target_link_libraries(math PUBLIC definitions) \ No newline at end of file diff --git a/engine/native/core/core.cppm b/engine/native/core/core.cppm new file mode 100644 index 00000000..3b208b31 --- /dev/null +++ b/engine/native/core/core.cppm @@ -0,0 +1,3 @@ +export module core; +export import core.defs; +export import core.math; diff --git a/engine/native/core/definitions/definitions.cppm b/engine/native/core/definitions/definitions.cppm new file mode 100644 index 00000000..c1860c26 --- /dev/null +++ b/engine/native/core/definitions/definitions.cppm @@ -0,0 +1,32 @@ +export module core.defs; +export import core.version; +import std; + +static_assert(__cplusplus >= 202302L, "Minimum of C++23 required."); + +export namespace draco { + + // Traits and Concepts + + template + concept arithmetic = std::is_arithmetic_v; + + template + concept trivial = std::is_trivial_v; + + // Whether the default value of a type is just all-0 bytes. + // This can most commonly be exploited by using memset for these types instead of loop-construct. + // Must be explicitly specialized to mark a type as such. + template + struct is_zero_constructible : std::false_type {}; + + template + constexpr bool is_zero_constructible_v = is_zero_constructible::value; + + template + concept zero_constructible = is_zero_constructible_v; + + // Limit the depth of recursive algorithms when dealing with Array/Dictionary + constexpr int MAX_RECURSION = 100; + +} \ No newline at end of file diff --git a/engine/native/core/definitions/version.cppm b/engine/native/core/definitions/version.cppm new file mode 100644 index 00000000..ab9742e3 --- /dev/null +++ b/engine/native/core/definitions/version.cppm @@ -0,0 +1,26 @@ +export module core.version; +export import std; +import std.compat; + +export namespace draco { + + struct Version { + uint16_t major; + uint16_t minor; + uint16_t patch; + }; + + constexpr Version VERSION { .major = 2026, .minor = 0, .patch = 0 }; +} + +export namespace std { + template<> struct formatter { + constexpr auto parse(std::format_parse_context& ctx) { + return ctx.begin(); // Accept any format spec (or parse custom ones) + } + + auto format(const draco::Version& v, std::format_context& ctx) const { + return std::format_to(ctx.out(), "{}.{}.{}", v.major, v.minor, v.patch); + } + }; +} \ No newline at end of file diff --git a/engine/native/core/math/constants.cppm b/engine/native/core/math/constants.cppm new file mode 100644 index 00000000..875501cd --- /dev/null +++ b/engine/native/core/math/constants.cppm @@ -0,0 +1,26 @@ +export module core.math.constants; +import std; + +export namespace draco::math { + constexpr double SQRT2 = std::numbers::sqrt2_v; + constexpr double SQRT3 = std::numbers::sqrt3_v; + constexpr double SQRT12 = 1. / SQRT2; + constexpr double SQRT13 = 1. / SQRT3; + constexpr double LN2 = std::numbers::ln2_v; + constexpr double LN10 = std::numbers::ln10_v; + constexpr double PI = std::numbers::pi_v; + constexpr double TAU = 2. * PI; + constexpr double E = std::numbers::e_v; + constexpr double INF = std::numeric_limits::infinity(); + constexpr double NaN = std::numeric_limits::quiet_NaN(); + constexpr double DB_CONVERSION_GAIN = 8.6858896380650365530225783783321; + constexpr double GAIN_CONVERSION_DB = 0.11512925464970228420089957273422; + constexpr double UINT32_MAX_D = 1. / static_cast(std::numeric_limits::max()); + constexpr float UINT32_MAX_F = 1.f / static_cast(std::numeric_limits::max()); + + template constexpr T CMP_EPSILON = T{0.000001}; + template constexpr T CMP_EPSILON2 = CMP_EPSILON * CMP_EPSILON; + + template constexpr T CMP_NORMALIZE_TOLERANCE = T{0.000001}; + template constexpr T CMP_POINT_IN_PLANE_EPSILON = T{0.00001}; +} diff --git a/engine/native/core/math/math.cppm b/engine/native/core/math/math.cppm new file mode 100644 index 00000000..50282e59 --- /dev/null +++ b/engine/native/core/math/math.cppm @@ -0,0 +1,147 @@ +export module core.math; +export import core.math.constants; +export import core.math.vector4; +export import core.defs; +import std; + +export namespace draco::math { + template + constexpr T sqr(T x) noexcept { return x*x; } + + template + [[nodiscard]] constexpr bool is_nan(T val) noexcept { + // Only NaN does not equal itself. + return val != val; + } + + template + [[nodiscard]] constexpr bool is_inf(T val) noexcept { + return std::isinf(val); + } + + template + [[nodiscard]] constexpr bool is_finite(T val) noexcept { + return std::isfinite(val); + } + + template + constexpr T abs(T value) noexcept { + // Manually compute abs for signed types. + // Also avoids potential int8_t -> int issues. + if constexpr (std::floating_point) { + return value < T{0} ? -value : value; + } else if constexpr (std::signed_integral) { + if (value == std::numeric_limits::min()) { + return std::numeric_limits::max(); // define saturating behavior explicitly + } + return value < T{0} ? -value : value; + } else { + // unsigned is always positive! :^) + return value; + } + } + + template + constexpr T deg_to_rad(T y) noexcept { + return y * (std::numbers::pi_v / T{180.}); + } + + template + constexpr T rad_to_deg(T y) noexcept { + return y * (T{180.} / std::numbers::pi_v); + } + + template + T pow(T x, T y) { + return static_cast(std::pow(x, y)); + } + + template + constexpr T lerp(T from, T to, T weight) noexcept { + return std::lerp(from, to, weight); + } + + template + constexpr T cubic_interpolate(T from, T to, T before, T after, T weight) noexcept { + // weight squared. + T w2 = weight * weight; + // weight cubed. + T w3 = weight * w2; + + // calculate coefficients. + T a = -before + to; + T b = T{2} * before - T{5} * from + T{4} * to - after; + T c = -before + T{3} * from - T{3} * to + after; + + // Catmull-Rom Interpolation: + // 0.5 * ((2 * p_from) + (a * w) + (b * w^2) + (c * w^3)) + + if consteval { + // compile time + return T{0.5} * (T{2.}*from + a*weight + b*w2 + c*w3); + } else { + // runtime + return T{0.5} * std::fma(c, w3, std::fma(b, w2, std::fma(a, weight, T{2} * from))); + } + } + + template + constexpr T cubic_interpolate_in_time( + T from, T to, + T before, T after, T weight, + T to_t, T before_t, T after_t) noexcept { + /* Barry-Goldman method */ + T t = lerp(T{0.}, to_t, weight); + + // At least try to make this easier to parse for others. + T pre_scale = before_t == T{0.} ? T{0.} : (t - before_t) / -before_t; + T to_scale = (to_t == T{0.}) ? T{.5} : t / to_t; + T post_range = after_t - to_t; + T post_scale = (post_range == T{0.}) ? T{1.} : (t - to_t) / post_range; + + // First layer. + T a1 = lerp(before, from, pre_scale); + T a2 = lerp(from, to, to_scale); + T a3 = lerp(to, after, post_scale); + + // More parsing. + T mid_range = to_t - before_t; + T from_to_scale = (mid_range == T{0.}) ? T{0.} : (t - before_t) / mid_range; + T to_post_scale = (after_t == T{0.}) ? T{1.} : t / after_t; + + // Second layer. + T b1 = lerp(a1, a2, from_to_scale); + T b2 = lerp(a2, a3, to_post_scale); + + // One more for the road. + T final_scale = (to_t == T{0.}) ? T{.5} : t / to_t; + + return lerp(b1, b2, final_scale); + } + + template + constexpr T bezier_interpolate(T start, T control_1, T control_2, T end, T t) noexcept { + /* Formula from Wikipedia article on Bezier curves. */ + // one minus t. + T omt = T{1.} - t; + T omt2 = omt * omt; + T omt3 = omt2 * omt; + T t2 = t * t; + T t3 = t2 * t; + + // B(t) = (1-t)^3 * P_0 + 3(1 - t)^2 * t * P_1 + 3(1 - t) * t^2 * P_2 + t^3 * P_3 + T d = start * omt3 + control_1 * omt2 * t * T{3.} + control_2 * omt * t2 * T{3.} + end * t3; + return d; + } + + template + constexpr T bezier_derivative(T start, T control_1, T control_2, T end, T t) noexcept { + /* Formula from Wikipedia article on Bezier curves. */ + T omt = T{1.} - t; + T omt2 = omt * omt; + T t2 = t * t; + + T d = (control_1 - start) * T{3.} * omt2 + (control_2 - control_1) * T{6.} * omt * t + (end - control_2) * T{3.} * t2; + return d; + } +} \ No newline at end of file diff --git a/engine/native/core/math/math.test.cpp b/engine/native/core/math/math.test.cpp new file mode 100644 index 00000000..b425529f --- /dev/null +++ b/engine/native/core/math/math.test.cpp @@ -0,0 +1,87 @@ +import boost.ut; +import core.math; + +using namespace boost::ut; + +suite<"core.math"> core_math_test = [] { + "pow"_test = [] { + double result = draco::math::pow(2., .5); + constexpr double expected = std::numbers::sqrt2_v; + expect(result == expected); + }; + + "abs"_test = [] { + using draco::math::abs; + + expect(abs(-1.f) == 1.f); + expect(abs(4.56f) == 4.56f); + expect(abs(-1.) == 1.); + expect(abs(4.56) == 4.56); + expect(abs(-5) == 5); + expect(abs(3L) == 3L); + expect(abs(-32L) == 32L); + expect(abs(5000ULL) == 5000ULL); + }; +}; + +suite<"core.math.vector4"> vector4_tests = [] { + "construct_and_access"_test = [] { + using draco::math::Vector4; + static constexpr Vector4 v{1.0f, 2.0f, 3.0f, 4.0f}; + static_assert(v[0] == 1.0f); + static_assert(v[1] == 2.0f); + static_assert(v[2] == 3.0f); + static_assert(v[3] == 4.0f); + expect(v[0] == 1.0f); + expect(v[1] == 2.0f); + expect(v[2] == 3.0f); + expect(v[3] == 4.0f); + }; + + "swap"_test = [] { + using draco::math::Vector4; + + Vector4 a{1.f, 2.f, 3.f, 4.f}; + Vector4 b{4.f, 3.f, 2.f, 1.f}; + + std::swap(a, b); + + expect(a == Vector4{4.f, 3.f, 2.f, 1.f}); + expect(b == Vector4{1.f, 2.f, 3.f, 4.f}); + }; + + "dot_basic"_test = [] { + using draco::math::Vector4; + + static constexpr Vector4 a{1.0f, 2.0f, 3.0f, 4.0f}; + static constexpr Vector4 b{5.0f, 6.0f, 7.0f, 8.0f}; + + const float result = draco::math::dot(a, b); + // 1 * 5 + 2 * 6 + 3 * 7 + 4 * 8 + const float expected = 70.0f; + + expect(result == expected); + }; + + "dot_zero"_test = [] { + using draco::math::Vector4; + using draco::math::dot; + + Vector4 a{0.0f, 0.0f, 0.0f, 0.0f}; + Vector4 b{1.0f, 2.0f, 3.0f, 4.0f}; + + expect(dot(a, b) == 0.0f); + }; + + "dot_self"_test = [] { + using draco::math::Vector4; + using draco::math::dot; + + Vector4 v{1.0f, 2.0f, 3.0f, 4.0f}; + + const float result = dot(v, v); + constexpr float expected = 30.0f; + + expect(result == expected); + }; +}; \ No newline at end of file diff --git a/engine/native/core/math/vector4.cppm b/engine/native/core/math/vector4.cppm new file mode 100644 index 00000000..d268d01a --- /dev/null +++ b/engine/native/core/math/vector4.cppm @@ -0,0 +1,150 @@ +module; + +#include "platform/simd.h" + +#if ARCH_X64 + #include +#elif ARCH_ARM64 + #include +#endif + +export module core.math.vector4; +import core.defs; +import std; + +export namespace draco::math { + + struct alignas(16) Vector4 { + float x, y, z, w; + + // constructors. + constexpr Vector4() noexcept = default; + constexpr Vector4(const float x, const float y, const float z, const float w) noexcept + : x{x}, y{y}, z{z}, w{w} { } + + // element access. + constexpr float& operator[](const int i) noexcept { + if consteval { + switch (i) { + case 0: return x; + case 1: return y; + case 2: return z; + default: + case 3: return w; + } + } else { return (&x)[i]; } + } + + constexpr const float& operator[](const int i) const noexcept { + if consteval { + switch (i) { + case 0: return x; + case 1: return y; + case 2: return z; + default: + case 3: return w; + } + } else { return (&x)[i]; } + } + + [[nodiscard]] constexpr bool operator==(const Vector4& other) const noexcept = default; + + constexpr Vector4& operator+=(const Vector4& other) noexcept { + x += other.x; + y += other.y; + z += other.z; + w += other.w; + return *this; + } + + constexpr Vector4& operator-=(const Vector4& other) noexcept { + x -= other.x; + y -= other.y; + z -= other.z; + w -= other.w; + return *this; + } + + constexpr Vector4& operator*=(const Vector4& other) noexcept { + x *= other.x; + y *= other.y; + z *= other.z; + w *= other.w; + return *this; + } + + constexpr Vector4& operator*=(const float s) noexcept { + x *= s; + y *= s; + z *= s; + w *= s; + return *this; + } + + constexpr Vector4& operator/=(const float s) noexcept { + const float inv = 1.0f / s; + x *= inv; + y *= inv; + z *= inv; + w *= inv; + return *this; + } + }; + + // safety features go brr + static_assert(sizeof(Vector4) == 16, "Vector4 must be 16 bytes"); + static_assert(alignof(Vector4) == 16, "Vector4 must be 16-byte aligned"); + static_assert(trivial, "Vector4 must be trivial"); + static_assert(std::is_standard_layout_v, "Vector4 must be standard layout"); + + [[nodiscard]] constexpr Vector4 operator+(const Vector4& a, const Vector4& b) noexcept { + return { a.x+b.x, a.y+b.y, a.z+b.z, a.w+b.w }; + } + + [[nodiscard]] constexpr Vector4 operator-(const Vector4& a, const Vector4& b) noexcept { + return { a.x-b.x, a.y-b.y, a.z-b.z, a.w-b.w }; + } + + [[nodiscard]] constexpr Vector4 operator*(const Vector4& a, const Vector4& b) noexcept { + return { a.x*b.x, a.y*b.y, a.z*b.z, a.w*b.w }; + } + + [[nodiscard]] constexpr Vector4 operator*(const Vector4& a, const float b) noexcept { + return { a.x*b, a.y*b, a.z*b, a.w*b }; + } + + [[nodiscard]] constexpr Vector4 operator*(const float s, const Vector4& v) noexcept { + return v*s; + } + + [[nodiscard]] constexpr Vector4 operator/(const Vector4& v, const float s) noexcept { + return v * (1.f/s); + } + + [[nodiscard]] FORCEINLINE float dot(const Vector4 &a, const Vector4 &b) noexcept { + #if ARCH_X64 + // There's only 4 floats, so SSE is what we will use. + // If there's a situation with multiple dot calls, we can setup a + // way to call 8 / 16 / 32 floats, but over-head could upset gains. + // Be sure it occurs commonly enough to matter here. + // Shuffle-first reduction worked best here. + __m128 va = _mm_load_ps(&a.x); + __m128 vb = _mm_load_ps(&b.x); + + __m128 m = _mm_mul_ps(va, vb); + + __m128 shuf = _mm_movehdup_ps(m); + __m128 sum = _mm_add_ps(m, shuf); + + shuf = _mm_movehl_ps(shuf, sum); + sum = _mm_add_ss(sum, shuf); + + return _mm_cvtss_f32(sum); + #elif ARCH_ARM64 + #error "ARM64 NEON support not yet implemented." + #else + // scalar. + return a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w; + #endif + } +} // namespace draco::math diff --git a/engine/native/platform/cpu/cpu_info.h b/engine/native/platform/cpu/cpu_info.h new file mode 100644 index 00000000..4ce187b0 --- /dev/null +++ b/engine/native/platform/cpu/cpu_info.h @@ -0,0 +1,13 @@ +// platform/cpu/cpu_info.h + +#pragma once +namespace draconic::platform::cpu { + enum class CpuFeature : unsigned char { + NONE, + AVX2, + AVX512F, + NEON, + }; + + void validate_cpu() noexcept; +} diff --git a/engine/native/platform/cpu/cpu_info_neon.cpp b/engine/native/platform/cpu/cpu_info_neon.cpp new file mode 100644 index 00000000..e24dc8be --- /dev/null +++ b/engine/native/platform/cpu/cpu_info_neon.cpp @@ -0,0 +1,14 @@ +// platform/cpu/cpu_info_neon.cpp + +#include "platform/cpu/cpu_info.h" + +#if !defined(__aarch64__) + #error cpu_info_neon.cpp compiled on non-ARM64 platform +#endif + +namespace draconic::platform::cpu { + void validate_cpu() noexcept { + // NEON is mandatory on AArch64. + // So, if this compiles - It's valid. + } +} diff --git a/engine/native/platform/cpu/cpu_info_x64.cpp b/engine/native/platform/cpu/cpu_info_x64.cpp new file mode 100644 index 00000000..443093c5 --- /dev/null +++ b/engine/native/platform/cpu/cpu_info_x64.cpp @@ -0,0 +1,101 @@ +// platform/cpu/cpu_info_x64.cpp + +#include "platform/cpu/cpu_info.h" + +#include // std::abort + +#if defined(_MSC_VER) + #include +#else + #include // for mingw. + #include +#endif + +#if !defined(__x86_64__) && !defined(_M_X64) + #error cpu_info_x64.cpp compiled on non-x86-64 platform +#endif + +namespace draconic::platform::cpu { + + namespace { + // Checks if OS has enabled save/restore YMM states. Required for AVX. + unsigned long long get_xcr0() noexcept { + #if defined(_MSC_VER) || defined(__GNUC__) || defined(__clang__) + return _xgetbv(0); + #else + return 0; + #endif + } + + // XCR0[1] is XMM, XCR0[2] is YMM. + bool os_has_ymm() noexcept { + return (get_xcr0() & 0x6ULL) == 0x6ULL; + } + + // full ZMM state is required for AVX512. + bool os_has_zmm() noexcept { + return (get_xcr0() & 0xE6ULL) == 0xE6ULL; + } + + void cpuid(unsigned int leaf, unsigned int subleaf, unsigned int& eax, unsigned int& ebx, unsigned int& ecx, unsigned int& edx) noexcept { + #if defined(_MSC_VER) + int regs[4]; + __cpuidex(regs, leaf, subleaf); + eax = regs[0]; ebx = regs[1]; ecx = regs[2]; edx = regs[3]; + #else + __cpuid_count(leaf, subleaf, eax, ebx, ecx, edx); + #endif + } + + CpuFeature detect_cpu_feature() noexcept { + unsigned int eax = 0; + unsigned int ebx = 0; + unsigned int ecx = 0; + unsigned int edx = 0; + + // leaf 1. + cpuid(1, 0, eax, ebx, ecx, edx); + + constexpr unsigned int OSXSAVE = 1u << 27; + constexpr unsigned int AVX = 1u << 28; + constexpr unsigned int FMA = 1u << 12; + + if ((ecx & (OSXSAVE | AVX | FMA)) != (OSXSAVE | AVX | FMA)) { + return CpuFeature::NONE; + } + + if (!os_has_ymm()) { + return CpuFeature::NONE; + } + + // leaf 7 + cpuid(7, 0, eax, ebx, ecx, edx); + + constexpr unsigned int AVX2 = 1u << 5; + constexpr unsigned int AVX512F = 1u << 16; + + if (!(ebx & AVX2)) { + return CpuFeature::NONE; + } + + if ((ebx & AVX512F) && os_has_zmm()) { + return CpuFeature::AVX512F; + } + + return CpuFeature::AVX2; + } + + } // anonymous namespace. + + void validate_cpu() noexcept { + #if defined(__x86_64__) || defined(_M_X64) + const CpuFeature level = detect_cpu_feature(); + + if(level == CpuFeature::NONE) { + std::abort(); + } + #else + #error Unsupported architecture. + #endif + } +} diff --git a/engine/native/platform/simd.h b/engine/native/platform/simd.h new file mode 100644 index 00000000..830b96d6 --- /dev/null +++ b/engine/native/platform/simd.h @@ -0,0 +1,72 @@ +// platform/simd.h + +#pragma once + +// Compiler detection. +#if defined(__clang__) + #define USING_COMPILER_CLANG 1 +#elif defined(_MSC_VER) + #define USING_COMPILER_MSVC 1 +#elif defined(__GNUC__) + #define USING_COMPILER_GCC 1 +#else + #error Unsupported compiler +#endif + +// Architecture detection +#if defined(__x86_64__) || defined(_M_X64) + #define ARCH_X64 1 +#elif defined(__aarch64__) + #define ARCH_ARM64 1 +#else + #error Unsupported architecture +#endif + +// SIMD level +#if ARCH_X64 + #define SIMD_AVX2 1 + + // We MAY remove this and have it auto-detect later. + #if defined(ENABLE_AVX512) + #define SIMD_AVX512 1 + #endif + +#elif ARCH_ARM64 + #define SIMD_NEON 1 +#endif + +// Force inline. +#if USING_COMPILER_MSVC + #define FORCEINLINE __forceinline +#else + #define FORCEINLINE inline __attribute__((always_inline)) +#endif + +// Restrict +#if USING_COMPILER_MSVC + #define RESTRICT __restrict +#else + #define RESTRICT __restrict__ +#endif + +// Alignment helpers. +#if USING_COMPILER_MSVC + #define ALIGN(N) __declspec(align(N)) +#else + #define ALIGN(N) __attribute__((aligned(N))) +#endif + +// assume and unreachable. +#ifdef DEBUG + #if USING_COMPILER_MSVC + #define ASSUME(x) do { if (!(x)) __debugbreak(); } while (0) + #define UNREACHABLE() __debugbreak() + #else + #define ASSUME(x) do { if (!(x)) __builtin_trap(); } while (0) + #define UNREACHABLE() __builtin_trap() + #endif +#else + // TODO: just use [[assume]] in the code + #define ASSUME(x) [[assume(x)]] // C++23 — GCC≥13, Clang≥19, MSVC≥17.3 + #define UNREACHABLE() ASSUME(false) +#endif diff --git a/engine/native/thirdparty/boost/ut.cppm b/engine/native/thirdparty/boost/ut.cppm new file mode 100644 index 00000000..100ac890 --- /dev/null +++ b/engine/native/thirdparty/boost/ut.cppm @@ -0,0 +1,22 @@ +module; + +#if __has_include() and __has_include() +#include +#include +#endif + +export module boost.ut; +export import std; + +#define BOOST_UT_CXX_MODULES 1 +#include "ut.hpp" + +template class boost::ut::reporter_junit; +template void boost::ut::reporter_junit::on(boost::ut::events::log); +template void boost::ut::reporter_junit::on(boost::ut::events::assertion_pass); +template void boost::ut::reporter_junit::on(boost::ut::events::assertion_fail); +template auto boost::ut::detail::test::operator=<>(test_location _test); +template auto boost::ut::expect(const bool&expr,const reflection::source_location&); +template void boost::ut::reporter_junit<>::on>(events::assertion_fail>); +template void boost::ut::reporter_junit<>::on>(events::assertion_pass>); +template void boost::ut::reporter_junit<>::on>(events::log>); diff --git a/engine/native/thirdparty/boost/ut.hpp b/engine/native/thirdparty/boost/ut.hpp new file mode 100644 index 00000000..0d059224 --- /dev/null +++ b/engine/native/thirdparty/boost/ut.hpp @@ -0,0 +1,3351 @@ +// +// Copyright (c) 2019-2021 Kris Jusiak (kris at jusiak dot net) +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#if defined(BOOST_UT_CXX_MODULES) +#define BOOST_UT_EXPORT export +#else +#pragma once +#define BOOST_UT_EXPORT +#endif + +#if !defined(BOOST_UT_CXX_MODULES) +#include +#endif + +#if defined(_MSC_VER) +#pragma push_macro("min") +#pragma push_macro("max") +#undef min +#undef max +#endif +// Before libc++ 17 had experimental support for format and it required a +// special build flag. Currently libc++ has not implemented all C++20 chrono +// improvements. Therefore doesn't define __cpp_lib_format, instead query the +// library version to detect the support status. +// +// MSVC STL and libstdc++ provide __cpp_lib_format. +#if defined(__cpp_lib_format) or \ + (defined(_LIBCPP_VERSION) and _LIBCPP_VERSION >= 170000) +#define BOOST_UT_HAS_FORMAT +#endif + +#if not defined(__cpp_rvalue_references) +#error "[Boost::ext].UT requires support for rvalue references"; +#elif not defined(__cpp_decltype) +#error "[Boost::ext].UT requires support for decltype"; +#elif not defined(__cpp_return_type_deduction) +#error "[Boost::ext].UT requires support for return type deduction"; +#elif not defined(__cpp_deduction_guides) +#error "[Boost::ext].UT requires support for return deduction guides"; +#elif not defined(__cpp_generic_lambdas) +#error "[Boost::ext].UT requires support for generic lambdas"; +#elif not defined(__cpp_constexpr) +#error "[Boost::ext].UT requires support for constexpr"; +#elif not defined(__cpp_alias_templates) +#error "[Boost::ext].UT requires support for alias templates"; +#elif not defined(__cpp_variadic_templates) +#error "[Boost::ext].UT requires support for variadic templates"; +#elif not defined(__cpp_fold_expressions) +#error "[Boost::ext].UT requires support for return fold expressions"; +#elif not defined(__cpp_static_assert) +#error "[Boost::ext].UT requires support for static assert"; +#else +#define BOOST_UT_VERSION 2'3'1 + +#if defined(__has_builtin) and defined(__GNUC__) and (__GNUC__ < 10) and \ + not defined(__clang__) +#undef __has_builtin +#endif + +#if not defined(__has_builtin) +#if defined(__GNUC__) and (__GNUC__ >= 9) +#define __has___builtin_FILE 1 +#define __has___builtin_LINE 1 +#endif +#define __has_builtin(...) __has_##__VA_ARGS__ +#endif + +#if !defined(BOOST_UT_CXX_MODULES) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if __has_include() and __has_include() +#include +#include +#endif +#if defined(__cpp_exceptions) +#include +#endif + +#if __has_include() +#include +#endif +#if __has_include() +#include +#endif +#endif // cxx modules + +struct unique_name_for_auto_detect_prefix_and_suffix_length_0123456789_struct_ { +}; + +BOOST_UT_EXPORT +namespace boost::inline ext::ut::inline v2_3_1 { +namespace utility { +template +class function; +template +class function { + public: + constexpr function() = default; + template + constexpr /*explicit(false)*/ function(T data) + : invoke_{invoke_impl}, + destroy_{destroy_impl}, + data_{new T{static_cast(data)}} {} + constexpr function(function&& other) noexcept + : invoke_{static_cast(other.invoke_)}, + destroy_{static_cast(other.destroy_)}, + data_{static_cast(other.data_)} { + other.data_ = {}; + } + constexpr function(const function&) = delete; + ~function() { destroy_(data_); } + + constexpr function& operator=(const function&) = delete; + constexpr function& operator=(function&&) = delete; + [[nodiscard]] constexpr auto operator()(TArgs... args) -> R { + return invoke_(data_, args...); + } + [[nodiscard]] constexpr auto operator()(TArgs... args) const -> R { + return invoke_(data_, args...); + } + + private: + template + [[nodiscard]] static auto invoke_impl(void* data, TArgs... args) -> R { + return (*static_cast(data))(args...); + } + + template + static auto destroy_impl(void* data) -> void { + delete static_cast(data); + } + + R (*invoke_)(void*, TArgs...){}; + void (*destroy_)(void*){}; + void* data_{}; +}; + +[[nodiscard]] inline auto is_match(std::string_view input, + std::string_view pattern) -> bool { + if (std::empty(pattern)) { + return std::empty(input); + } + + if (std::empty(input)) { + return pattern[0] == '*' ? is_match(input, pattern.substr(1)) : false; + } + + if (pattern[0] != '?' and pattern[0] != '*' and pattern[0] != input[0]) { + return false; + } + + if (pattern[0] == '*') { + for (decltype(std::size(input)) i = 0u; i <= std::size(input); ++i) { + if (is_match(input.substr(i), pattern.substr(1))) { + return true; + } + } + return false; + } + + return is_match(input.substr(1), pattern.substr(1)); +} + +template +[[nodiscard]] constexpr auto match(const TPattern& pattern, const TStr& str) + -> std::vector { + std::vector groups{}; + auto pi = 0u; + auto si = 0u; + + const auto matcher = [&](char b, char e, char c = 0) { + const auto match = si; + while (str[si] and str[si] != b and str[si] != c) { + ++si; + } + groups.emplace_back(str.substr(match, si - match)); + while (pattern[pi] and pattern[pi] != e) { + ++pi; + } + pi++; + }; + + while (pi < std::size(pattern) && si < std::size(str)) { + if (pattern[pi] == '\'' and str[si] == '\'' and pattern[pi + 1] == '{') { + ++si; + matcher('\'', '}'); + } else if (pattern[pi] == '{') { + matcher(' ', '}', ','); + } else if (pattern[pi] != str[si]) { + return {}; + } + ++pi; + ++si; + } + + if (si < str.size() or pi < std::size(pattern)) { + return {}; + } + + return groups; +} + +template +[[nodiscard]] inline auto split(T input, TDelim delim) -> std::vector { + std::vector output{}; + std::size_t first{}; + while (first < std::size(input)) { + const auto second = input.find_first_of(delim, first); + if (first != second) { + output.emplace_back(input.substr(first, second - first)); + } + if (second == T::npos) { + break; + } + first = second + 1; + } + return output; +} +constexpr auto regex_match(const char* str, const char* pattern) -> bool { + if (*pattern == '\0' && *str == '\0') { + return true; + } + if (*pattern == '\0' && *str != '\0') { + return false; + } + if (*str == '\0' && *pattern != '\0') { + return false; + } + if (*pattern == '.') { + return regex_match(str + 1, pattern + 1); + } + if (*pattern == *str) { + return regex_match(str + 1, pattern + 1); + } + return false; +} +} // namespace utility + +namespace reflection { +#if defined(__cpp_lib_source_location) && !defined(_LIBCPP_APPLE_CLANG_VER) +using source_location = std::source_location; +#else +class source_location { + public: + [[nodiscard]] static constexpr auto current( +#if (__has_builtin(__builtin_FILE) and __has_builtin(__builtin_LINE)) + const char* file = __builtin_FILE(), int line = __builtin_LINE() +#else + const char* file = "unknown", int line = {} +#endif + ) noexcept { + source_location sl{}; + sl.file_ = file; + sl.line_ = line; + return sl; + } + [[nodiscard]] constexpr auto file_name() const noexcept { return file_; } + [[nodiscard]] constexpr auto line() const noexcept { return line_; } + + private: + const char* file_{"unknown"}; + int line_{}; +}; +#endif +namespace detail { +template +[[nodiscard]] constexpr auto get_template_function_name_use_type() + -> std::string_view { +// for over compiler need over macros +#if defined(_MSC_VER) && !defined(__clang__) + return {&__FUNCSIG__[0], sizeof(__FUNCSIG__)}; +#else + return {&__PRETTY_FUNCTION__[0], sizeof(__PRETTY_FUNCTION__)}; +#endif +} + +// decay allows you to highlight a cleaner name +template +[[nodiscard]] constexpr auto get_template_function_name_use_decay_type() + -> std::string_view { + return get_template_function_name_use_type>(); +} + +inline constexpr const std::string_view raw_type_name = + get_template_function_name_use_decay_type< + unique_name_for_auto_detect_prefix_and_suffix_length_0123456789_struct_>(); + +inline constexpr const std::size_t raw_length = raw_type_name.length(); +inline constexpr const std::string_view need_name = +#if defined(_MSC_VER) and not defined(__clang__) + "struct " + "unique_name_for_auto_detect_prefix_and_suffix_length_0123456789_struct_"; +#else + "unique_name_for_auto_detect_prefix_and_suffix_length_0123456789_struct_"; +#endif +inline constexpr const std::size_t need_length = need_name.length(); +static_assert(need_length <= raw_length, + "Auto find prefix and suffix length broken error 1"); +inline constexpr const std::size_t prefix_length = + raw_type_name.find(need_name); +static_assert(prefix_length != std::string_view::npos, + "Auto find prefix and suffix length broken error 2"); +static_assert(prefix_length <= raw_length, + "Auto find prefix and suffix length broken error 3"); +inline constexpr const std::size_t tail_length = raw_length - prefix_length; +static_assert(need_length <= tail_length, + "Auto find prefix and suffix length broken error 4"); +inline constexpr const std::size_t suffix_length = tail_length - need_length; + +} // namespace detail + +template +[[nodiscard]] constexpr auto type_name() -> std::string_view { + const std::string_view raw_type_name = + detail::get_template_function_name_use_type(); + const std::size_t end = raw_type_name.length() - detail::suffix_length; + const std::size_t len = end - detail::prefix_length; + std::string_view result = raw_type_name.substr(detail::prefix_length, len); + return result; +} + +// decay allows you to highlight a cleaner name +template +[[nodiscard]] constexpr auto decay_type_name() -> std::string_view { + const std::string_view raw_type_name = + detail::get_template_function_name_use_decay_type(); + const std::size_t end = raw_type_name.length() - detail::suffix_length; + const std::size_t len = end - detail::prefix_length; + std::string_view result = raw_type_name.substr(detail::prefix_length, len); + return result; +} +} // namespace reflection + +namespace math { +template +[[nodiscard]] constexpr auto abs(const T t) -> T { + return t < T{} ? -t : t; +} + +template +[[nodiscard]] constexpr auto abs_diff(const T t, const U u) + -> decltype(t < u ? u - t : t - u) { + return t < u ? u - t : t - u; +} + +template +[[nodiscard]] constexpr auto min_value(const T& lhs, const T& rhs) -> const T& { + return (rhs < lhs) ? rhs : lhs; +} + +template +[[nodiscard]] constexpr auto pow(const T base, const TExp exp) -> T { + return exp ? T(base * pow(base, exp - TExp(1))) : T(1); +} + +template +[[nodiscard]] constexpr auto num() -> T { + static_assert( + ((Cs == '.' or Cs == '\'' or (Cs >= '0' and Cs <= '9')) and ...)); + T result{}; + for (const char c : std::array{Cs...}) { + if (c == '.') { + break; + } + if (c >= '0' and c <= '9') { + result = result * T(10) + T(c - '0'); + } + } + return result; +} + +template +[[nodiscard]] constexpr auto den() -> T { + constexpr const std::array cs{Cs...}; + T result{}; + auto i = 0u; + while (cs[i++] != '.') { + } + + for (auto j = i; j < sizeof...(Cs); ++j) { + result += pow(T(10), sizeof...(Cs) - j) * T(cs[j] - '0'); + } + return result; +} + +template +[[nodiscard]] constexpr auto den_size() -> T { + constexpr const std::array cs{Cs...}; + T i{}; + while (cs[i++] != '.') { + } + + return T(sizeof...(Cs)) - i + T(1); +} + +template +[[nodiscard]] constexpr auto den_size(TValue value) -> T { + constexpr auto precision = TValue(1e-7); + T result{}; + TValue tmp{}; + do { + value *= 10; + tmp = value - T(value); + ++result; + } while (tmp > precision); + + return result; +} + +} // namespace math + +namespace type_traits { +template +struct list {}; + +template +struct identity { + using type = T; +}; + +template +struct function_traits : function_traits {}; + +template +struct function_traits { + using result_type = R; + using args = list; +}; + +template +struct function_traits { + using result_type = R; + using args = list; +}; + +template +struct function_traits { + using result_type = R; + using args = list; +}; + +template +struct function_traits { + using result_type = R; + using args = list; +}; + +template +struct has_static_member_object_value : std::false_type {}; + +template +struct has_static_member_object_value< + T, std::void_t().value)>> + : std::bool_constant && + !std::is_function_v> {}; + +template +inline constexpr bool has_static_member_object_value_v = + has_static_member_object_value::value; + +template +struct has_static_member_object_epsilon : std::false_type {}; + +template +struct has_static_member_object_epsilon< + T, std::void_t().epsilon)>> + : std::bool_constant && + !std::is_function_v> {}; + +template +inline constexpr bool has_static_member_object_epsilon_v = + has_static_member_object_epsilon::value; + +} // namespace type_traits + +namespace concepts { + +// std::convertible_to also requires implicit conversion to work +// See https://stackoverflow.com/a/76547623 +template +concept explicitly_convertible_to = + requires { static_cast(std::declval()); }; + +template +concept ostreamable = requires(std::ostringstream& os, T t) { os << t; }; + +} // namespace concepts + +template +struct fixed_string { + constexpr static std::size_t N = SIZE; + CharT _data[N + 1] = {}; + + constexpr explicit(false) fixed_string(const CharT (&str)[N + 1]) noexcept { + if constexpr (N != 0) { + for (std::size_t i = 0; i < N; ++i) { + _data[i] = str[i]; + } + } + } + + [[nodiscard]] constexpr std::size_t size() const noexcept { return N; } + [[nodiscard]] constexpr bool empty() const noexcept { return N == 0; } + [[nodiscard]] constexpr explicit operator std::string_view() const noexcept { + return {_data, N}; + } + [[nodiscard]] explicit operator std::string() const noexcept { + return {_data, N}; + } + [[nodiscard]] operator const char*() const noexcept { return _data; } + [[nodiscard]] constexpr bool operator==( + const fixed_string& other) const noexcept { + return std::string_view{_data, N} == std::string_view(other); + } + + template + [[nodiscard]] friend constexpr bool operator==( + const fixed_string&, const fixed_string&) { + return false; + } +}; + +template +fixed_string(const CharT (&str)[N]) -> fixed_string; + +struct none {}; + +namespace events { +struct run_begin { + int argc{}; + const char** argv{}; +}; +struct test_begin { + std::string_view type{}; + std::string_view name{}; + reflection::source_location location{}; +}; +struct suite_begin { + std::string_view type{}; + std::string_view name{}; + reflection::source_location location{}; +}; +struct suite_end { + std::string_view type{}; + std::string_view name{}; + reflection::source_location location{}; +}; +template +struct test { + std::string_view type{}; + std::string name{}; /// might be dynamic + std::vector tag{}; + reflection::source_location location{}; + TArg arg{}; + Test run{}; + + constexpr auto operator()() { run_impl(static_cast(run), arg); } + constexpr auto operator()() const { run_impl(static_cast(run), arg); } + + private: + static constexpr auto run_impl(Test test, const none&) { test(); } + + template + static constexpr auto run_impl(T test, const TArg& arg) + -> decltype(test(arg), void()) { + test(arg); + } + + template + static constexpr auto run_impl(T test, const TArg&) + -> decltype(test.template operator()(), void()) { + test.template operator()(); + } +}; +template +test(std::string_view, std::string_view, std::string_view, + reflection::source_location, TArg, Test) -> test; +template +struct suite { + TSuite run{}; + std::string_view name{}; + constexpr auto operator()() { run(); } + constexpr auto operator()() const { run(); } +}; +template +suite(TSuite) -> suite; +struct test_run { + std::string_view type{}; + std::string_view name{}; +}; +struct test_finish { + std::string_view type{}; + std::string_view name{}; +}; +template +struct skip { + std::string_view type{}; + std::string_view name{}; + TArg arg{}; +}; +template +skip(std::string_view, std::string_view, TArg) -> skip; +struct test_skip { + std::string_view type{}; + std::string_view name{}; +}; +template +struct assertion { + TExpr expr{}; + reflection::source_location location{}; +}; +template +assertion(TExpr, reflection::source_location) -> assertion; +template +struct assertion_pass { + TExpr expr{}; + reflection::source_location location{}; +}; +template +assertion_pass(TExpr) -> assertion_pass; +template +struct assertion_fail { + TExpr expr{}; + reflection::source_location location{}; +}; +template +assertion_fail(TExpr) -> assertion_fail; +struct test_end { + std::string_view type{}; + std::string_view name{}; +}; +template +struct log { + TMsg msg{}; +}; +template +log(TMsg) -> log; +struct fatal_assertion : std::exception {}; +struct exception { + const char* msg{}; + [[nodiscard]] auto what() const -> const char* { return msg; } +}; +struct summary {}; +} // namespace events + +namespace detail { +struct op {}; + +template +struct fatal_; + +struct fatal { + template + [[nodiscard]] auto operator()(const T& t, const reflection::source_location& sl = reflection::source_location::current()) const { + return detail::fatal_{t, sl}; + } +}; +struct cfg { + using value_ref = std::variant, + std::reference_wrapper, + std::reference_wrapper>; + using option = std::tuple; + static inline reflection::source_location location{}; + static inline bool wip{}; + +#if defined(_MSC_VER) + static inline int largc = __argc; + static inline const char** largv = const_cast(__argv); +#else + static inline int largc = 0; + static inline const char** largv = nullptr; +#endif + + static inline std::string executable_name = "unknown executable"; + static inline std::string query_pattern; // <- done + static inline bool invert_query_pattern = false; // <- done + static inline std::string query_regex_pattern; // <- done + static inline bool show_help = false; // <- done + static inline bool show_tests = false; // <- done + static inline bool list_tags = false; // <- done + static inline bool show_successful_tests = false; // <- done + static inline std::string output_filename; + static inline std::string use_reporter = "console"; // <- done + static inline std::string suite_name; + static inline bool abort_early = false; // <- done + static inline std::size_t abort_after_n_failures = + std::numeric_limits::max(); // <- done + static inline bool show_duration = false; // <- done + static inline std::size_t show_min_duration = 0; + static inline std::string input_filename; + static inline bool show_test_names = false; // <- done + static inline bool show_reporters = false; // <- done + static inline std::string sort_order = "decl"; + static inline std::size_t rnd_seed = 0; // 0: use time + static inline std::string use_colour = "yes"; // <- done + static inline bool show_lib_identity = false; // <- done + static inline std::string wait_for_keypress = "never"; + + static inline const std::vector