Skip to content

Commit 3cbc376

Browse files
Merge pull request #685 from themuffinator/codex/add-runtime-validation-for-division-operators
Guard Vector3 division against zero divisors
2 parents 2135966 + 29484f1 commit 3cbc376

2 files changed

Lines changed: 91 additions & 4 deletions

File tree

src/shared/q_vec3.hpp

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#pragma once
2020
#include <array>
21+
#include <cassert>
2122
#include <cmath>
2223
#include <stdexcept>
2324
#include <type_traits>
@@ -26,11 +27,33 @@
2627
using nullptr_t = std::nullptr_t;
2728

2829
struct Vector3 {
30+
static constexpr float kDivisionEpsilon = 1.0e-6f;
31+
2932
std::array<float, 3> components{ 0.0f, 0.0f, 0.0f };
3033
float& x;
3134
float& y;
3235
float& z;
3336

37+
/*
38+
=============
39+
SafeDivisor
40+
41+
Clamps divisors away from zero while asserting in debug builds.
42+
=============
43+
*/
44+
[[nodiscard]] static inline float SafeDivisor(const float divisor) {
45+
const bool near_zero = divisor > -kDivisionEpsilon && divisor < kDivisionEpsilon;
46+
47+
#ifndef NDEBUG
48+
assert(!near_zero && "Vector3 division by zero or near-zero divisor");
49+
#endif
50+
51+
if (near_zero)
52+
return divisor >= 0.0f ? kDivisionEpsilon : -kDivisionEpsilon;
53+
54+
return divisor;
55+
}
56+
3457
/*
3558
=============
3659
Vector3
@@ -159,12 +182,27 @@ struct Vector3 {
159182
[[nodiscard]] constexpr Vector3 operator+(const Vector3& v) const {
160183
return { x + v.x, y + v.y, z + v.z };
161184
}
162-
[[nodiscard]] constexpr Vector3 operator/(const Vector3& v) const {
163-
return { x / v.x, y / v.y, z / v.z };
185+
/*
186+
=============
187+
operator/
188+
189+
Divides component-wise by another vector using guarded divisors.
190+
=============
191+
*/
192+
[[nodiscard]] inline Vector3 operator/(const Vector3& v) const {
193+
return { x / SafeDivisor(v.x), y / SafeDivisor(v.y), z / SafeDivisor(v.z) };
164194
}
165195
template<typename T, typename = std::enable_if_t<std::is_floating_point_v<T> || std::is_integral_v<T>>>
166-
[[nodiscard]] constexpr Vector3 operator/(const T& v) const {
167-
return { static_cast<float>(x / v), static_cast<float>(y / v), static_cast<float>(z / v) };
196+
/*
197+
=============
198+
operator/
199+
200+
Divides each component by a scalar using a guarded divisor.
201+
=============
202+
*/
203+
[[nodiscard]] inline Vector3 operator/(const T& v) const {
204+
const float divisor = SafeDivisor(static_cast<float>(v));
205+
return { static_cast<float>(x / divisor), static_cast<float>(y / divisor), static_cast<float>(z / divisor) };
168206
}
169207
template<typename T, typename = std::enable_if_t<std::is_floating_point_v<T> || std::is_integral_v<T>>>
170208
[[nodiscard]] constexpr Vector3 operator*(const T& v) const {

tests/test_vector3_division.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*Copyright (c) 2024 The DarkMatter Project
2+
Licensed under the GNU General Public License 2.0.
3+
4+
test_vector3_division.cpp implementation.*/
5+
6+
#include <cassert>
7+
#include <cmath>
8+
9+
#include "shared/q_vec3.hpp"
10+
11+
/*
12+
=============
13+
main
14+
15+
Validates Vector3 division behavior when divisors are zero or near-zero.
16+
=============
17+
*/
18+
int main() {
19+
Vector3 numerator{ 1.0f, -2.0f, 3.0f };
20+
Vector3 nearZeroDivisor{ 0.0f, 1.0e-8f, 4.0f };
21+
22+
Vector3 vectorResult = numerator / nearZeroDivisor;
23+
assert(std::isfinite(vectorResult.x));
24+
assert(std::isfinite(vectorResult.y));
25+
assert(vectorResult.z == 0.75f);
26+
const float expectedVectorX = 1.0f / Vector3::kDivisionEpsilon;
27+
const float expectedVectorY = -2.0f / Vector3::kDivisionEpsilon;
28+
assert(std::fabs(vectorResult.x - expectedVectorX) < 1.0f);
29+
assert(std::fabs(vectorResult.y - expectedVectorY) < 1.0f);
30+
31+
Vector3 scalarResult = numerator / 0.0f;
32+
assert(std::isfinite(scalarResult.x));
33+
assert(std::isfinite(scalarResult.y));
34+
assert(scalarResult.z == 3.0f / Vector3::kDivisionEpsilon);
35+
const float expectedScalarX = 1.0f / Vector3::kDivisionEpsilon;
36+
const float expectedScalarY = -2.0f / Vector3::kDivisionEpsilon;
37+
assert(std::fabs(scalarResult.x - expectedScalarX) < 1.0f);
38+
assert(std::fabs(scalarResult.y - expectedScalarY) < 1.0f);
39+
40+
Vector3 compoundVector = numerator;
41+
compoundVector /= nearZeroDivisor;
42+
assert(compoundVector.equals(vectorResult, 1.0f));
43+
44+
Vector3 compoundScalar = numerator;
45+
compoundScalar /= 0.0f;
46+
assert(compoundScalar.equals(scalarResult, 1.0f));
47+
48+
return 0;
49+
}

0 commit comments

Comments
 (0)