Skip to content

Add more vector types, functions, and tests#18

Open
Apeirobyte wants to merge 8 commits into
Redot-Engine:masterfrom
Apeirobyte:mathtypes
Open

Add more vector types, functions, and tests#18
Apeirobyte wants to merge 8 commits into
Redot-Engine:masterfrom
Apeirobyte:mathtypes

Conversation

@Apeirobyte
Copy link
Copy Markdown

@Apeirobyte Apeirobyte commented May 5, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced Vector2, Vector3, and Vector4 types with comprehensive operations including dot products, normalization, projection, reflection, interpolation, and component-wise functions
    • Added math utility functions: rounding operations (floor, ceil, round, trunc), floating-point predicates (NaN/infinity checks), angle conversion, and Bezier interpolation
    • Established float-based mathematical constants (PI, E, square roots) and precision tolerance values
  • Tests

    • Significantly expanded test coverage for vector operations and mathematical functions to ensure reliability

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc4e8781-47f1-40cf-8351-cbff2c3ebfb5

📥 Commits

Reviewing files that changed from the base of the PR and between cdf0326 and 7f88291.

📒 Files selected for processing (6)
  • engine/native/core/math/constants.cppm
  • engine/native/core/math/math.test.cpp
  • engine/native/core/math/types_common.cppm
  • engine/native/core/math/vector2.cppm
  • engine/native/core/math/vector3.cppm
  • engine/native/core/math/vector4.cppm
🚧 Files skipped from review as they are similar to previous changes (6)
  • engine/native/core/math/constants.cppm
  • engine/native/core/math/types_common.cppm
  • engine/native/core/math/math.test.cpp
  • engine/native/core/math/vector2.cppm
  • engine/native/core/math/vector4.cppm
  • engine/native/core/math/vector3.cppm

📝 Walkthrough

Walkthrough

This PR modernizes the draco::math subsystem from double-based monolithic constants and inline functions into a modular C++20 architecture. Math constants migrate to float with simplified includes. A new core.math.functions module provides generic arithmetic utilities, consteval-optimized rounding, conversions, and interpolation helpers. Vector types (Vector2, Vector3, Vector4) are refactored into separate module units with complete implementations, SIMD alignment, swizzle support, and comprehensive geometric and arithmetic operations. Extensive test coverage validates all functionality across math utilities and vector types.

Math Module Refactoring and Vector Implementation

Layer / File(s) Summary
Math Constants and Functions Foundation
engine/native/core/math/constants.cppm, engine/native/core/math/functions.cppm
Math constants migrate from double to float with simplified includes. New core.math.functions module exports generic templates (sqr, abs, sign), consteval-based float rounding with NaN/overflow guards, conversions (deg_to_rad, rad_to_deg), and interpolation utilities (lerp, cubic_interpolate, cubic_interpolate_in_time, Bezier helpers).
Math Module Refactoring to Aggregator
engine/native/core/math/math.cppm
core.math module is converted to a thin interface that re-exports core.math.constants, core.math.functions, core.math.types, and core.defs, removing previously inline function implementations.
Vector Type Module Contracts and Infrastructure
engine/native/core/math/types.cppm, engine/native/core/math/types_common.cppm
core.math.types aggregates vector submodules. core.math.types:common declares SIMD-aligned Vector2/Vector3/Vector4 interfaces with constructors, swizzle access via if consteval branching, and compile-time select helpers.
Vector2 Implementation
engine/native/core/math/vector2.cppm
Implements Vector2 with full arithmetic operators (compound, unary, binary with scalar variants) and free functions for geometric operations (dot, length, distance, normalize, project, reflect, angle), interpolation, min/max/clamp with length bounds, and component-wise utilities plus formatting.
Vector3 Implementation
engine/native/core/math/vector3.cppm
Implements Vector3 with static layout assertions, constructors from scalars and cross-vector composition, axis/spherical/cylindrical factories, and arithmetic/geometric operations matching Vector2 scope plus cross product and formatted output.
Vector4 Refactoring to Module Implementation
engine/native/core/math/vector4.cppm
Refactors Vector4 into module unit with expanded constructors, axis factories, scalar-aware arithmetic operators, platform-aware dot (consteval fast path, x64 SSE intrinsics), and comprehensive geometric/utility functions with formatted output.
Comprehensive Test Coverage
engine/native/core/math/math.test.cpp
Adds doctest main and type-enforcing macros with compile-time assertions. Comprehensive TEST_SUITE organization covers math utilities and all vector types with extensive cases for constructors, access, swizzling, geometric operations, interpolation, min/max/clamp variants, and numeric helpers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Redot-Engine/DraconicEngine#14: Expands Vector4 with length_sq, length, normalize, and normalize_fast, which directly overlaps the math utilities added in this PR.
  • Redot-Engine/DraconicEngine#19: Modifies core math modules (constants.cppm and core.math interfaces) for header simplification and numeric constants/tolerances, sharing refactoring scope with this PR.

Suggested reviewers

  • mcdubhghlas
  • Arctis-Fireblight
  • JoltedJon
  • AR-DEV-1

Poem

🐰 Floats and vectors dance with grace,
Constants lean and find their place,
Modules split with purpose clear,
Math subsystem reappear!
Tests abound to prove it right,
Modular magic shining bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: addition of vector types (Vector2, Vector3 type variants), math functions (sqr, is_nan, lerp, bezier_interpolate, etc.), and comprehensive test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Apeirobyte Apeirobyte changed the title Add more vector types and functions Add more vector types, functions, and tests May 17, 2026
@Apeirobyte Apeirobyte marked this pull request as ready for review May 17, 2026 06:29
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
engine/native/core/math/functions.cppm (1)

104-112: ⚡ Quick win

Precision loss for double specializations.

PI is a float constant, so when T is double, T{PI} widens the float to double but retains only float precision (~7 digits instead of ~15). Use std::numbers::pi_v<T> for type-appropriate precision:

Proposed fix
     template <std::floating_point T>
     constexpr T deg_to_rad(T y) noexcept {
-        return y * (T{PI} / T{180.});
+        return y * (std::numbers::pi_v<T> / T{180});
     }

     template <std::floating_point T>
     constexpr T rad_to_deg(T y) noexcept {
-        return y * (T{180.} / T{PI});
+        return y * (T{180} / std::numbers::pi_v<T>);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/native/core/math/functions.cppm` around lines 104 - 112, deg_to_rad
and rad_to_deg currently use the float PI constant causing precision loss for
double specializations; replace T{PI} with std::numbers::pi_v<T> so the pi value
is generated at the correct floating-point type for both template functions
(deg_to_rad and rad_to_deg) and ensure you include <numbers> where these
templates are defined.
engine/native/core/math/constants.cppm (2)

27-27: 💤 Low value

Clarify the reciprocal naming.

UINT32_MAX_F suggests it holds the float representation of UINT32_MAX, but it actually stores the reciprocal (1.f / max). Consider a clearer name like INV_UINT32_MAX_F or UINT32_MAX_RECIP.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/native/core/math/constants.cppm` at line 27, The constant UINT32_MAX_F
is misnamed because it stores the reciprocal (1.f /
std::numeric_limits<uint32_t>::max()); rename it to a clearer identifier such as
INV_UINT32_MAX_F (or UINT32_MAX_RECIP) and update all references to UINT32_MAX_F
to the new name (e.g., any uses in functions or files referencing UINT32_MAX_F).
Keep it constexpr float = 1.f / std::numeric_limits<uint32_t>::max(); and
optionally add a short comment like "reciprocal of UINT32_MAX" to prevent future
confusion; if backward compatibility is needed, add a deprecated alias constexpr
float UINT32_MAX_F = INV_UINT32_MAX_F and mark it with a comment.

15-15: 💤 Low value

Use float literals for consistency with float constants.

Lines 15 and 21 use double literals (1., 2.) which get implicitly narrowed to float. For clarity and consistency with the float type, use 1.f and 2.f.

Suggested fix
-    constexpr float SQRT12 = 1. / SQRT2;
+    constexpr float SQRT12 = 1.f / SQRT2;
-    constexpr float TAU = 2. * PI;
+    constexpr float TAU = 2.f * PI;

Also applies to: 21-21

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/native/core/math/constants.cppm` at line 15, The float constants use
double literals that get narrowed — update the numeric literals to float by
changing occurrences like in the SQRT12 definition (and the other constant on
line 21) from 1. and 2. to 1.f and 2.f respectively so the expressions (e.g.,
SQRT12 = 1.f / SQRT2) use float literals consistently with the float-typed
constants.
engine/native/core/math/math.test.cpp (1)

36-36: 💤 Low value

Use float literals for consistency.

The pow test uses double literals (2., .5) while the rest of the test suite consistently uses float literals (e.g., 1.0f). For consistency with the float-based math migration described in the PR summary, consider using 2.0f and 0.5f.

Suggested fix
-        float result = draco::math::pow(2., .5);
+        float result = draco::math::pow(2.0f, 0.5f);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/native/core/math/math.test.cpp` at line 36, The test uses double
literals for draco::math::pow which conflicts with the float-based math
migration; update the call in math.test.cpp so the literals are float types
(e.g., change 2. and .5 to 2.0f and 0.5f) so the computed value assigned to
result (float result) uses float operands consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CMakeLists.txt`:
- Around line 20-21: The CMake file incorrectly sets CMAKE_CXX_MODULE_STD to
"libc++" (an invalid boolean); remove that line and either enable standard
library modules by setting CMAKE_CXX_MODULE_STD ON (to allow import std; C++23
modules) or, if the goal is to use libc++ as the standard library
implementation, append the flag -stdlib=libc++ to CMAKE_CXX_FLAGS (e.g., update
CMAKE_CXX_FLAGS to include -stdlib=libc++). Ensure you reference and modify the
CMake variable CMAKE_CXX_MODULE_STD or CMAKE_CXX_FLAGS accordingly.

In `@engine/native/core/math/types_common.cppm`:
- Around line 37-38: The unary operator declarations for Vector2, Vector3, and
Vector4 (operator+() and operator-()) must be made const so they can be used on
const instances; update the signatures in types_common.cppm to add the trailing
const to each declaration for Vector2::operator+(), Vector2::operator-(),
Vector3::operator+(), Vector3::operator-(), Vector4::operator+(), and
Vector4::operator-(), and then update the corresponding out-of-line
definitions/implementations to match the new const-qualified signatures.

In `@engine/native/core/math/vector2.cppm`:
- Around line 358-366: Guard against zero-length vectors before dividing by
sqrt(len_sq) in both max_length and clamp_length: compute len_sq via
length_sq(a), and if len_sq == 0.0f and the target magnitude b (or min/max
threshold) is > 0, return a canonical unit-direction times b (e.g. Vector2{b,0})
instead of performing the division; otherwise proceed with the existing scaling
logic. Update the implementations of max_length and clamp_length to check len_sq
== 0.0f early and handle that case explicitly to avoid NaNs.

In `@engine/native/core/math/vector3.cppm`:
- Around line 390-398: The functions max_length and clamp_length can divide by
zero for a zero vector; add a guard after computing len_sq in both Vector3
max_length(const Vector3& a, float b) noexcept and clamp_length(...) to handle
len_sq == 0: if len_sq == 0.0f return a (do not attempt to scale) so you avoid
b/√len_sq producing NaN; otherwise keep the existing logic that computes scale =
b/√len_sq (or clamps appropriately) and returns a * scale.

In `@engine/native/core/math/vector4.cppm`:
- Around line 437-444: max_length and clamp_length can divide by zero when 'a'
is a zero vector; fix by checking for zero (or near-zero) length before
dividing: compute len_sq = length_sq(a), if len_sq <= tiny_eps (e.g.
std::numeric_limits<float>::epsilon() or a small constant) then return 'a'
unchanged (or for clamp_length, return 'a' when no meaningful direction exists)
else perform the existing sqrt/divide scaling; apply the same guard in both
Vector4 max_length and Vector4 clamp_length to avoid sqrt(0)/division-by-zero.
- Around line 300-302: Remove the compile-time hard error for ARCH_ARM64 inside
the dot implementation: replace the `#error "ARM64 NEON support not yet
implemented."` branch with the same scalar fallback used for other architectures
so ARM64 builds succeed until NEON is implemented; update the conditional in the
dot function (the ARCH_ARM64/#elif block) to call the existing scalar dot path
(or inline the scalar arithmetic) instead of aborting on ARM64, keeping
NEON-specific stubs commented or noted for future implementation.

---

Nitpick comments:
In `@engine/native/core/math/constants.cppm`:
- Line 27: The constant UINT32_MAX_F is misnamed because it stores the
reciprocal (1.f / std::numeric_limits<uint32_t>::max()); rename it to a clearer
identifier such as INV_UINT32_MAX_F (or UINT32_MAX_RECIP) and update all
references to UINT32_MAX_F to the new name (e.g., any uses in functions or files
referencing UINT32_MAX_F). Keep it constexpr float = 1.f /
std::numeric_limits<uint32_t>::max(); and optionally add a short comment like
"reciprocal of UINT32_MAX" to prevent future confusion; if backward
compatibility is needed, add a deprecated alias constexpr float UINT32_MAX_F =
INV_UINT32_MAX_F and mark it with a comment.
- Line 15: The float constants use double literals that get narrowed — update
the numeric literals to float by changing occurrences like in the SQRT12
definition (and the other constant on line 21) from 1. and 2. to 1.f and 2.f
respectively so the expressions (e.g., SQRT12 = 1.f / SQRT2) use float literals
consistently with the float-typed constants.

In `@engine/native/core/math/functions.cppm`:
- Around line 104-112: deg_to_rad and rad_to_deg currently use the float PI
constant causing precision loss for double specializations; replace T{PI} with
std::numbers::pi_v<T> so the pi value is generated at the correct floating-point
type for both template functions (deg_to_rad and rad_to_deg) and ensure you
include <numbers> where these templates are defined.

In `@engine/native/core/math/math.test.cpp`:
- Line 36: The test uses double literals for draco::math::pow which conflicts
with the float-based math migration; update the call in math.test.cpp so the
literals are float types (e.g., change 2. and .5 to 2.0f and 0.5f) so the
computed value assigned to result (float result) uses float operands
consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcbf5e3f-813b-42d9-96f2-a52a2f75f8dc

📥 Commits

Reviewing files that changed from the base of the PR and between b826b17 and cdf0326.

📒 Files selected for processing (10)
  • CMakeLists.txt
  • engine/native/core/math/constants.cppm
  • engine/native/core/math/functions.cppm
  • engine/native/core/math/math.cppm
  • engine/native/core/math/math.test.cpp
  • engine/native/core/math/types.cppm
  • engine/native/core/math/types_common.cppm
  • engine/native/core/math/vector2.cppm
  • engine/native/core/math/vector3.cppm
  • engine/native/core/math/vector4.cppm

Comment thread CMakeLists.txt Outdated
Comment thread engine/native/core/math/types_common.cppm Outdated
Comment thread engine/native/core/math/vector2.cppm
Comment thread engine/native/core/math/vector3.cppm
Comment on lines +300 to +302
#elif ARCH_ARM64
#error "ARM64 NEON support not yet implemented."
#endif
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove the ARM64 compile-time hard error in dot.

The #error path makes ARM64 builds fail unconditionally. Keep a scalar fallback until NEON is implemented.

Suggested fix
             `#elif` ARCH_ARM64
-                `#error` "ARM64 NEON support not yet implemented."
+                // Keep scalar fallback below until a NEON path is added.
             `#endif`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#elif ARCH_ARM64
#error "ARM64 NEON support not yet implemented."
#endif
`#elif` ARCH_ARM64
// Keep scalar fallback below until a NEON path is added.
`#endif`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/native/core/math/vector4.cppm` around lines 300 - 302, Remove the
compile-time hard error for ARCH_ARM64 inside the dot implementation: replace
the `#error "ARM64 NEON support not yet implemented."` branch with the same
scalar fallback used for other architectures so ARM64 builds succeed until NEON
is implemented; update the conditional in the dot function (the ARCH_ARM64/#elif
block) to call the existing scalar dot path (or inline the scalar arithmetic)
instead of aborting on ARM64, keeping NEON-specific stubs commented or noted for
future implementation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine to me, we don't have logging yet & will replace it later

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AR-DEV-1, understood — I'll defer to your judgment here. Would you like me to open a follow-up GitHub issue to track the ARM64 NEON implementation for dot (and any other functions that may need it) so it doesn't get lost?

(ˆ•ω•ˆ)


🧠 Learnings used
Learnt from: mcdubhghlas
Repo: Redot-Engine/DraconicEngine PR: 1
File: core/math/vector4.cppm:136-140
Timestamp: 2026-03-17T14:51:36.674Z
Learning: For all C++ module files (.cppm), ensure the build flags cover AVX2/-mfma unconditionally (as in the project: -mavx2 -mfma). If you use BMI1/BMI2 intrinsics in a module, also enable them explicitly with -mbmi and -mbmi2, since BMI features are not guaranteed by AVX2 alone. Note that SSE3 and older SSEs are guaranteed via ISA backwards compatibility on AVX2 targets, so no SSE2/SSE3 fallbacks are needed.

Comment thread engine/native/core/math/vector4.cppm
Copy link
Copy Markdown
Contributor

@OldDev78 OldDev78 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things to iron out, it's almost ready.

}

constexpr float ceil(float value) noexcept {
if consteval {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need a compile time vs runtime split for these cases. We need to look at reducing dependency on STL in any case.

// element access
[[nodiscard]] constexpr float& Vector2::operator[](const int i) noexcept {
if consteval {
return i ? y : x;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use select in the compile time case?

}

// Returns magnitude
[[nodiscard]] float length(const Vector2& v) noexcept {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll make these constexpr as well soon, but that's coming next. :)


// constructors
[[nodiscard]] constexpr Vector2() noexcept = default;
[[nodiscard]] constexpr Vector2(const float n) noexcept;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const is redundant when declaring value arguments. In the implementation, it says that the value cannot be modified, but for the caller this is redundant.

More importantly, it is especially relevant for the vector types to declare the single-argument constructors explicit.

};
}

template<typename T> consteval T select(const int i, const T v1, const T v2) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These compile time utilities are very useful for the vector types compile time branch index operators, and are underutilized in there.

Comment on lines +452 to +461
export namespace std {
template<> struct formatter<draco::math::Vector2> : formatter<float> {
auto format(const draco::math::Vector2& v, format_context& ctx) const {
ctx.advance_to(format_to(ctx.out(), "{{"));
ctx.advance_to(formatter<float>::format(v.x, ctx));
ctx.advance_to(format_to(ctx.out(), ", "));
ctx.advance_to(formatter<float>::format(v.y, ctx));
return format_to(ctx.out(), "}}");
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to reconsider the use of std::format at one point. Just to keep in mind. But for now, it's okay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants