Compare commits

...

38 Commits

Author SHA1 Message Date
77adc0ea8a unlikely added 2026-01-04 03:27:46 +03:00
83f17892d6 added unlikely 2026-01-03 08:57:56 +03:00
26fd8e158a i dont like icons now 2026-01-01 03:59:12 +03:00
2af77d30d4 Feature/elf pattern scan (#133)
* added some code

* improvement

* visit

* added scanner code

* update

* fixed naming

* added const

* added type casting

* added file

* patch

* added unlikely

* added in module scanner

* fixing test

* fix

* remove

* improvement

* fix

* Update source/utility/elf_pattern_scan.cpp

Co-authored-by: Saikari <lin@sz.cn.eu.org>

* rev

* fix

* patch

* decomposed method

* fix

* fix

* improvement

* fix

* fix

* commented stuff

---------

Co-authored-by: Saikari <lin@sz.cn.eu.org>
2026-01-01 03:37:55 +03:00
368272b1e8 fixed tests 2025-12-30 08:40:42 +03:00
Saikari
0ca471ed4f Use LLVM coverage and LCOV genhtml on Windows OS (#131) 2025-12-29 21:11:13 +03:00
Saikari
f0145ec68e Use LLVM coverage and LCOV genhtml on Windows OS (#129) 2025-12-28 02:44:00 +03:00
771e1e68fe improved code style 2025-12-28 02:32:35 +03:00
ffcf448a07 improved code style 2025-12-28 02:29:19 +03:00
00fcdc86bc Update license badge in README
Replaced the static badge with a dynamic GitHub license badge.
2025-12-27 20:07:45 +03:00
2c11c5ce1a returned back to zlib 2025-12-27 20:06:17 +03:00
fdb2ad099a umped version 2025-12-27 19:43:56 +03:00
88ce5e6b8c warning fix 2025-12-25 02:35:50 +03:00
29da13d244 Implement coverage for Windows/Linux/MacOS (#126) (#128)
* Implement coverage for Windows/Linux/MacOS (#126)

* reverted

---------

Co-authored-by: Saikari <lin@sz.cn.eu.org>
2025-12-25 02:28:17 +03:00
d16984a8b2 Add GitHub repo size badge to README 2025-12-24 02:38:26 +03:00
d935caf1a4 Feature/more constexpr (#125)
* added constexpr

* fix

* improved stuff

* added const

* improvement

* fix

* fix

* patch
2025-12-24 02:32:14 +03:00
897484bea1 Temp (#123)
* Coverage

* added fixes

* removed spacing

* removed junk

* removed print

* removed coverage

* removed useless stuff

* fix

---------

Co-authored-by: Saikari <lin@sz.cn.eu.org>
2025-12-23 02:47:12 +03:00
a03620c18f Merge pull request #121 from luadebug/try
Run unit tests for WASM
2025-12-22 00:34:28 +03:00
Saikari
4a8e7e85ce try 2025-12-21 22:57:38 +03:00
1499ac3213 Merge pull request #120 from luadebug/android
Rework CI
2025-12-21 18:18:02 +03:00
Saikari
f3a6a1a3ae Test build NDK 2025-12-21 18:03:25 +03:00
c312ccad0c Merge pull request #118 from orange-cpp/develop
Develop
2025-12-20 00:46:42 +03:00
939be67643 specified specific vcpkg version 2025-12-20 00:37:14 +03:00
43a063807d removed extra check 2025-12-19 23:59:25 +03:00
4fd7f8efa6 replaced numeric limits 2025-12-19 23:49:54 +03:00
52ca23383d changed epsilon 2025-12-19 23:43:03 +03:00
ce21c217f1 changed epsilon to numeric limmits 2025-12-19 23:34:10 +03:00
09b64cc702 Merge pull request #117 from luadebug/mingw
Add MinGW support
2025-12-19 23:21:31 +03:00
d085681efe removed redundant copy 2025-12-19 23:19:47 +03:00
Saikari
2f7746caeb Add MinGW support 2025-12-18 01:59:11 +03:00
94ee8751af Merge pull request #116 from luadebug/wasm
Add WASM support
2025-12-17 14:02:58 +03:00
Saikari
82b9b671f6 Add WASM support 2025-12-17 13:38:14 +03:00
082b5f69b8 Merge pull request #115 from luadebug/iphone
Add iOS support
2025-12-16 19:16:52 +03:00
Saikari
735a565446 Add iOS support 2025-12-16 14:48:20 +03:00
852bf5c56f Merge pull request #114 from luadebug/ndk
Add NDK support
2025-12-15 07:08:56 +03:00
Saikari
de5c8bc84d Add NDK support 2025-12-15 00:04:11 +03:00
35d9de1550 Merge pull request #113 from luadebug/freebsd
Add FreeBSD support
2025-12-14 22:57:07 +03:00
Saikari
201d8f5547 Add FreeBSD support 2025-12-14 22:48:29 +03:00
70 changed files with 5458 additions and 444 deletions

View File

@@ -1,4 +1,4 @@
name: Omath CI (Arch Linux / Windows)
name: Omath CI
on:
push:
@@ -10,24 +10,82 @@ concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
##############################################################################
# 1) ARCH LINUX Clang / Ninja
# 1) Linux Clang / Ninja
##############################################################################
jobs:
arch-build-and-test:
name: Arch Linux (Clang)
runs-on: ubuntu-latest
container: archlinux:latest
linux-build-and-test:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- name: Linux (Clang) (x64-linux)
triplet: x64-linux
runner: ubuntu-latest
preset: linux-release-vcpkg
coverage: true
install_cmd: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
sudo apt-get update
sudo apt-get install -y git build-essential cmake ninja-build \
zip unzip curl pkg-config ca-certificates \
clang-21 lld-21 libc++-21-dev libc++abi-21-dev \
llvm-21
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100
sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100
- name: Linux (Clang) (x86-linux)
triplet: x86-linux
runner: ubuntu-latest
preset: linux-release-vcpkg-x86
coverage: false
install_cmd: |
# Add LLVM 21 repository
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
# Add GCC Toolchain PPA
sudo add-apt-repository -y "deb http://archive.ubuntu.com/ubuntu plucky main universe"
# Enable i386 architecture
sudo dpkg --add-architecture i386
sudo apt-get update
# Install Clang 21
sudo apt-get install -y git build-essential cmake ninja-build \
zip unzip curl pkg-config ca-certificates \
clang-21 lld-21 libc++-21-dev libc++abi-21-dev
sudo apt-get install -y -t plucky binutils
# Install GCC 15 with multilib support
sudo apt-get install -y gcc-15-multilib g++-15-multilib
# Set up alternatives for Clang
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100
sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100
# Set up alternatives for GCC
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 100
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 100
- name: Linux (Clang) (arm64-linux)
triplet: arm64-linux
runner: ubuntu-24.04-arm
preset: linux-release-vcpkg-arm64
coverage: false
install_cmd: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
sudo apt-get update
sudo apt-get install -y git build-essential cmake ninja-build \
zip unzip curl pkg-config ca-certificates \
clang-21 lld-21 libc++-21-dev libc++abi-21-dev
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100
sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Install basic tool-chain with pacman
- name: Install basic tool-chain
shell: bash
run: |
pacman -Sy --noconfirm archlinux-keyring
pacman -Syu --noconfirm --needed \
git base-devel clang cmake ninja zip unzip fmt
run: ${{ matrix.install_cmd }}
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
@@ -38,29 +96,82 @@ jobs:
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: cmake --preset linux-release-vcpkg -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
- name: Build
shell: bash
run: cmake --build cmake-build/build/linux-release-vcpkg --target unit_tests omath
run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
- name: Run unit_tests
shell: bash
run: ./out/Release/unit_tests
- name: Run Coverage
if: ${{ matrix.coverage == true }}
shell: bash
run: |
sudo apt-get install lcov
chmod +x scripts/coverage-llvm.sh
./scripts/coverage-llvm.sh \
"${{ github.workspace }}" \
"cmake-build/build/${{ matrix.preset }}" \
"./out/Release/unit_tests" \
"cmake-build/build/${{ matrix.preset }}/coverage"
- name: Upload Coverage Report
if: ${{ matrix.coverage == true }}
uses: actions/upload-artifact@v4
with:
name: coverage-report-linux
path: cmake-build/build/${{ matrix.preset }}/coverage/
##############################################################################
# 2) Windows MSVC / Ninja
##############################################################################
- name: Upload logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: linux-build-logs-${{ matrix.triplet }}
path: |
cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
##############################################################################
# 2) Windows MSVC / Ninja
##############################################################################
windows-build-and-test:
name: Windows (MSVC)
runs-on: windows-latest
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- name: Windows (MSVC) (x64-windows)
runner: windows-latest
arch: amd64
preset: windows-release-vcpkg
triplet: x64-windows
- name: Windows (MSVC) (x86-windows)
runner: windows-latest
arch: amd64_x86
preset: windows-release-vcpkg-x86
triplet: x86-windows
- name: Windows (MSVC) (arm64-windows)
runner: windows-11-arm
arch: arm64
preset: windows-release-vcpkg-arm64
triplet: arm64-windows
fail-fast: false
env:
OMATH_BUILD_VIA_VCPKG: ON
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
@@ -72,25 +183,171 @@ jobs:
- name: Set up MSVC developer command-prompt
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.arch }}
- name: Configure (cmake --preset)
shell: bash
run: cmake --preset windows-release-vcpkg -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
run: |
cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
- name: Build
shell: bash
run: cmake --build cmake-build/build/windows-release-vcpkg --target unit_tests omath
run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
- name: Run unit_tests.exe
shell: bash
run: ./out/Release/unit_tests.exe
##########################################################################
# Coverage (x64-windows only)
##########################################################################
- name: Install LLVM
if: ${{ matrix.triplet == 'x64-windows' }}
run: |
choco install llvm -y
- name: Clean Build Directory for Coverage
if: ${{ matrix.triplet == 'x64-windows' }}
shell: pwsh
run: |
$buildDir = "cmake-build/build/${{ matrix.preset }}"
if (Test-Path $buildDir) {
Write-Host "Cleaning build directory to prevent compiler conflict: $buildDir"
Remove-Item -Path $buildDir -Recurse -Force
}
- name: Build Debug for Coverage
if: ${{ matrix.triplet == 'x64-windows' }}
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DCMAKE_C_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe" \
-DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe" \
-DCMAKE_LINKER="C:/Program Files/LLVM/bin/lld-link.exe" \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=ON \
-DOMATH_THREAT_WARNING_AS_ERROR=OFF \
-DCMAKE_BUILD_TYPE=Debug \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath
- name: Run Tests (Generates .profraw)
if: ${{ matrix.triplet == 'x64-windows' }}
shell: pwsh
env:
LLVM_PROFILE_FILE: "cmake-build/build/${{ matrix.preset }}/unit_tests.profraw"
run: |
./out/Debug/unit_tests.exe
- name: Process Coverage (llvm-profdata & llvm-cov)
if: ${{ matrix.triplet == 'x64-windows' }}
shell: pwsh
run: |
$BUILD_DIR = "cmake-build/build/${{ matrix.preset }}"
$EXE_PATH = "./out/Debug/unit_tests.exe"
# 1. Merge raw profile data (essential step)
& "C:/Program Files/LLVM/bin/llvm-profdata.exe" merge `
-sparse "$BUILD_DIR/unit_tests.profraw" `
-o "$BUILD_DIR/unit_tests.profdata"
# 2. Export to LCOV format
# NOTE: We explicitly ignore vcpkg_installed and system headers
& "C:/Program Files/LLVM/bin/llvm-cov.exe" export "$EXE_PATH" `
-instr-profile="$BUILD_DIR/unit_tests.profdata" `
-format=lcov `
-ignore-filename-regex="vcpkg_installed|external|tests" `
> "$BUILD_DIR/lcov.info"
if (Test-Path "$BUILD_DIR/lcov.info") {
Write-Host "✅ LCOV info created at $BUILD_DIR/lcov.info"
} else {
Write-Error "Failed to create LCOV info"
exit 1
}
- name: Install LCOV (for genhtml)
if: ${{ matrix.triplet == 'x64-windows' }}
run: choco install lcov -y
- name: Generate HTML Report
if: ${{ matrix.triplet == 'x64-windows' }}
shell: bash
run: |
BUILD_DIR="cmake-build/build/${{ matrix.preset }}"
LCOV_INFO="${BUILD_DIR}/lcov.info"
HTML_DIR="${BUILD_DIR}/coverage-html"
# Fix paths for genhtml (Perl hates backslashes)
sed -i 's|\\|/|g' "${LCOV_INFO}"
# Locate genhtml provided by 'choco install lcov'
# It is typically in ProgramData/chocolatey/lib/lcov/tools/bin
GENHTML=$(find /c/ProgramData/chocolatey -name genhtml -print -quit)
if [ -z "$GENHTML" ]; then
echo "Error: genhtml executable not found"
exit 1
fi
echo "Using genhtml: $GENHTML"
mkdir -p "$HTML_DIR"
# Run genhtml
# Added --demangle-cpp if your version supports it, otherwise remove it
perl "$GENHTML" \
"${LCOV_INFO}" \
--output-directory "$HTML_DIR" \
--title "OMath Coverage Report" \
--legend \
--show-details \
--branch-coverage \
--ignore-errors source
echo "✅ LCOV HTML report generated at $HTML_DIR"
- name: Upload Coverage (HTML Report)
if: ${{ matrix.triplet == 'x64-windows' }}
uses: actions/upload-artifact@v4
with:
name: coverage-html-windows
path: cmake-build/build/${{ matrix.preset }}/coverage-html/
- name: Upload logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: windows-build-logs-${{ matrix.triplet }}
path: |
cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
##############################################################################
# 3) macOS AppleClang / Ninja
##############################################################################
macosx-build-and-test:
name: macOS (AppleClang)
runs-on: macOS-latest
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- name: macOS (AppleClang) (arm64-osx)
runner: macos-latest
preset: darwin-release-vcpkg
triplet: arm64-osx
coverage: true
- name: macOS (AppleClang) (x64-osx)
runner: macos-15-intel
preset: darwin-release-vcpkg-x64
triplet: x64-osx
coverage: false
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
@@ -111,12 +368,446 @@ jobs:
- name: Configure (cmake --preset)
shell: bash
run: cmake --preset darwin-release-vcpkg -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
run: |
cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests"
- name: Build
shell: bash
run: cmake --build cmake-build/build/darwin-release-vcpkg --target unit_tests omath
run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
- name: Run unit_tests
shell: bash
run: ./out/Release/unit_tests
- name: Run Coverage
if: ${{ matrix.coverage == true }}
shell: bash
run: |
brew install lcov
chmod +x scripts/coverage-llvm.sh
./scripts/coverage-llvm.sh \
"${{ github.workspace }}" \
"cmake-build/build/${{ matrix.preset }}" \
"./out/Release/unit_tests" \
"cmake-build/build/${{ matrix.preset }}/coverage"
- name: Upload Coverage Report
if: ${{ matrix.coverage == true }}
uses: actions/upload-artifact@v4
with:
name: coverage-report-macos
path: cmake-build/build/${{ matrix.preset }}/coverage/
- name: Upload logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: macos-build-logs-${{ matrix.triplet }}
path: |
cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
##############################################################################
# 4) iOS AppleClang / Xcode / arm64-ios
##############################################################################
ios-build:
name: iOS (AppleClang) (${{ matrix.triplet }})
runs-on: macOS-latest
strategy:
matrix:
include:
- triplet: arm64-ios
preset: ios-release-vcpkg
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Install CMake tooling
shell: bash
run: |
brew install cmake ninja
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests"
- name: Build
shell: bash
run: |
cmake --build cmake-build/build/${{ matrix.preset }} --config Release --target unit_tests omath
- name: Upload logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: ios-build-logs-${{ matrix.triplet }}
path: |
cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
##############################################################################
# 5) FreeBSD Clang / Ninja
##############################################################################
freebsd-build-and-test:
name: FreeBSD (Clang) (${{ matrix.triplet }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- triplet: x64-freebsd
preset: freebsd-release-vcpkg
arch: x86-64
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/tmp/vcpkg
steps:
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Build and Test
uses: cross-platform-actions/action@v0.31.0
with:
operating_system: freebsd
architecture: ${{ matrix.arch }}
version: '15.0'
memory: '12G'
cpu_count: 4
run: |
sudo pkg install -y git curl zip unzip gmake llvm gsed bash perl5 openssl 7-zip coreutils cmake ninja pkgconf patchelf
git config --global --add safe.directory `pwd`
# Build vcpkg in /tmp to avoid sshfs timestamp sync issues
export VCPKG_ROOT=/tmp/vcpkg
rm -rf "$VCPKG_ROOT"
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
cd -
export VCPKG_FORCE_SYSTEM_BINARIES=0
cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported"
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
./out/Release/unit_tests
- name: Upload logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: freebsd-build-logs-${{ matrix.triplet }}
path: |
cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
##############################################################################
# 6) Android NDK Clang / Ninja
##############################################################################
android-build:
name: Android NDK (${{ matrix.triplet }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- triplet: arm-neon-android
preset: android-arm-neon-release-vcpkg
- triplet: arm64-android
preset: android-arm64-release-vcpkg
- triplet: x64-android
preset: android-x64-release-vcpkg
- triplet: x86-android
preset: android-x86-release-vcpkg
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
ANDROID_NDK_HOME: ${{ github.workspace }}/android-ndk
steps:
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Android NDK
shell: bash
run: |
NDK_VERSION="r28b"
NDK_ZIP="android-ndk-${NDK_VERSION}-linux.zip"
wget -q "https://dl.google.com/android/repository/${NDK_ZIP}"
unzip -q "${NDK_ZIP}" -d "${{ github.workspace }}"
mv "${{ github.workspace }}/android-ndk-${NDK_VERSION}" "$ANDROID_NDK_HOME"
rm "${NDK_ZIP}"
echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME}" >> $GITHUB_ENV
- name: Install basic tool-chain
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y ninja-build cmake
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests"
- name: Build
shell: bash
run: |
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
- name: Upload logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: android-build-logs-${{ matrix.triplet }}
path: |
cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
##############################################################################
# 7) WebAssembly (Emscripten) Clang / Ninja / wasm32-emscripten
##############################################################################
wasm-build-and-test:
name: WebAssembly (Emscripten) (${{ matrix.triplet }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- triplet: wasm32-emscripten
preset: wasm-release-vcpkg
fail-fast: false
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install basic tool-chain
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y ninja-build
- name: Setup Emscripten
uses: mymindstorm/setup-emsdk@v14
with:
version: 'latest'
- name: Verify Emscripten
shell: bash
run: |
echo "EMSDK=$EMSDK"
emcc --version
# Verify toolchain file exists
ls -la "$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests"
- name: Build
shell: bash
run: |
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
- name: Upload logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: wasm-build-logs-${{ matrix.triplet }}
path: |
cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Run WASM Unit Tests
run: node out/Release/unit_tests.js
##############################################################################
# 8) Windows MSYS2 MinGW GCC / Ninja
##############################################################################
mingw-build-and-test:
name: ${{ matrix.name }}
runs-on: windows-latest
strategy:
matrix:
include:
- name: MINGW64 (MSYS2) (x64-mingw-dynamic)
msystem: MINGW64
pkg_prefix: mingw-w64-x86_64
preset: mingw-release-vcpkg
- name: UCRT64 (MSYS2) (x64-mingw-dynamic)
msystem: UCRT64
pkg_prefix: mingw-w64-ucrt-x86_64
preset: mingw-release-vcpkg
- name: MINGW32 (MSYS2) (x86-mingw-dynamic)
msystem: MINGW32
pkg_prefix: mingw-w64-i686
preset: mingw32-release-vcpkg
fail-fast: false
defaults:
run:
shell: msys2 {0}
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: true
install: >-
${{ matrix.pkg_prefix }}-toolchain
${{ matrix.pkg_prefix }}-cmake
${{ matrix.pkg_prefix }}-ninja
${{ matrix.pkg_prefix }}-pkg-config
git
base-devel
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up vcpkg
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
run: |
cmake --preset ${{ matrix.preset }} \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests"
- name: Build
run: |
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
- name: Run unit_tests.exe
run: |
./out/Release/unit_tests.exe
- name: Upload logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: mingw-build-logs-${{ matrix.msystem }}
path: |
cmake-build/build/${{ matrix.preset }}/**/*.log
${{ env.VCPKG_ROOT }}/buildtrees/**/*.log
##############################################################################
# 9) Valgrind Memory Check
##############################################################################
valgrind-memory-check:
name: Valgrind Analysis (All Targets)
runs-on: ubuntu-latest
needs: [linux-build-and-test]
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
steps:
- name: Install toolchain
shell: bash
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"
sudo apt-get update
sudo apt-get install -y git build-essential cmake ninja-build \
zip unzip curl pkg-config ca-certificates \
clang-21 lld-21 libc++-21-dev libc++abi-21-dev \
llvm-21 valgrind libxmu-dev libxi-dev libgl-dev libxinerama-dev libxcursor-dev xorg-dev libglu1-mesa-dev
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100
sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100
- name: Checkout repository (with sub-modules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up vcpkg
shell: bash
run: |
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT"
cd "$VCPKG_ROOT"
./bootstrap-vcpkg.sh
- name: Configure (cmake --preset)
shell: bash
run: |
cmake --preset linux-release-vcpkg \
-DCMAKE_BUILD_TYPE=Debug \
-DOMATH_BUILD_EXAMPLES=OFF \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=ON \
-DOMATH_ENABLE_VALGRIND=ON \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;benchmark"
- name: Build All Targets
shell: bash
run: cmake --build cmake-build/build/linux-release-vcpkg
- name: Run Valgrind (All Registered Targets)
shell: bash
working-directory: cmake-build/build/linux-release-vcpkg
run: |
cmake --build . --target valgrind_all

5
.gitignore vendored
View File

@@ -2,4 +2,7 @@
/out
*.DS_Store
/extlibs/vcpkg
.idea/workspace.xml
.idea/workspace.xml
/build/
/clang-coverage/
*.gcov

2
.idea/omath.iml generated
View File

@@ -1,2 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module classpath="CMake" type="CPP_MODULE" version="4" />
<module classpath="CIDR" type="CPP_MODULE" version="4" />

View File

@@ -6,6 +6,9 @@ project(omath VERSION ${OMATH_VERSION} LANGUAGES CXX)
include(CMakePackageConfigHelpers)
include(CheckCXXCompilerFlag)
include(cmake/Coverage.cmake)
include(cmake/Valgrind.cmake)
if (MSVC)
check_cxx_compiler_flag("/arch:AVX2" COMPILER_SUPPORTS_AVX2)
else ()
@@ -23,7 +26,7 @@ option(OMATH_STATIC_MSVC_RUNTIME_LIBRARY "Force Omath to link static runtime" OF
option(OMATH_SUPRESS_SAFETY_CHECKS "Supress some safety checks in release build to improve general performance" ON)
option(OMATH_USE_UNITY_BUILD "Will enable unity build to speed up compilation" OFF)
option(OMATH_ENABLE_LEGACY "Will enable legacy classes that MUST be used ONLY for backward compatibility" ON)
option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF)
if (VCPKG_MANIFEST_FEATURES)
foreach (omath_feature IN LISTS VCPKG_MANIFEST_FEATURES)
@@ -60,6 +63,8 @@ if (${PROJECT_IS_TOP_LEVEL})
message(STATUS "[${PROJECT_NAME}]: ImGUI integration feature status ${OMATH_IMGUI_INTEGRATION}")
message(STATUS "[${PROJECT_NAME}]: Legacy features support ${OMATH_ENABLE_LEGACY}")
message(STATUS "[${PROJECT_NAME}]: Building using vcpkg ${OMATH_BUILD_VIA_VCPKG}")
message(STATUS "[${PROJECT_NAME}]: Coverage feature status ${OMATH_ENABLE_COVERAGE}")
message(STATUS "[${PROJECT_NAME}]: Valgrind feature status ${OMATH_ENABLE_VALGRIND}")
endif ()
file(GLOB_RECURSE OMATH_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp")
@@ -135,11 +140,20 @@ if (OMATH_USE_AVX2)
endif ()
endif ()
if(EMSCRIPTEN)
target_compile_options(${PROJECT_NAME} PUBLIC -fexceptions)
target_link_options(${PROJECT_NAME} PUBLIC -fexceptions)
endif()
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
if (OMATH_BUILD_TESTS)
add_subdirectory(tests)
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_BUILD_TESTS)
if(OMATH_ENABLE_COVERAGE)
omath_setup_coverage(${PROJECT_NAME})
endif()
endif ()
if (OMATH_BUILD_BENCHMARK)
@@ -150,6 +164,7 @@ if (OMATH_BUILD_EXAMPLES)
add_subdirectory(examples)
endif ()
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND OMATH_THREAT_WARNING_AS_ERROR)
target_compile_options(${PROJECT_NAME} PRIVATE /W4 /WX)
elseif (OMATH_THREAT_WARNING_AS_ERROR)
@@ -188,7 +203,6 @@ install(EXPORT ${PROJECT_NAME}Targets
DESTINATION lib/cmake/${PROJECT_NAME} COMPONENT ${PROJECT_NAME}
)
# Generate the omathConfigVersion.cmake file
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/omathConfigVersion.cmake"

View File

@@ -1,15 +1,49 @@
{
"version": 3,
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 25,
"patch": 0
},
"configurePresets": [
{
"name": "windows-base",
"name": "base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/cmake-build/build/${presetName}",
"installDir": "${sourceDir}/cmake-build/install/${presetName}",
"installDir": "${sourceDir}/cmake-build/install/${presetName}"
},
{
"name": "vcpkg-base",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_COMPILER": "cl.exe",
"CMAKE_MAKE_PROGRAM": "Ninja"
"OMATH_BUILD_VIA_VCPKG": "ON",
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed"
}
},
{
"name": "debug",
"hidden": true,
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"hidden": true,
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "windows-base",
"hidden": true,
"inherits": "base",
"cacheVariables": {
"CMAKE_CXX_COMPILER": "cl.exe"
},
"condition": {
"type": "equals",
@@ -18,59 +52,88 @@
}
},
{
"name": "windows-base-vcpkg",
"name": "windows-vcpkg-base",
"hidden": true,
"inherits": "windows-base",
"inherits": ["windows-base", "vcpkg-base"],
"cacheVariables": {
"OMATH_BUILD_VIA_VCPKG": "ON",
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
}
},
{
"name": "windows-debug",
"displayName": "Debug",
"inherits": "windows-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "windows-debug-vcpkg",
"displayName": "Windows Debug Vcpkg",
"inherits": "windows-base-vcpkg",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "windows-release-vcpkg",
"displayName": "Windows Release Vcpkg",
"inherits": "windows-base-vcpkg",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"OMATH_BUILD_VIA_VCPKG": "ON"
}
"displayName": "Windows Debug",
"inherits": ["windows-base", "debug"]
},
{
"name": "windows-release",
"displayName": "Release",
"inherits": "windows-base",
"displayName": "Windows Release",
"inherits": ["windows-base", "release"]
},
{
"name": "windows-debug-vcpkg",
"displayName": "Windows Debug (vcpkg)",
"inherits": ["windows-vcpkg-base", "debug"]
},
{
"name": "windows-release-vcpkg",
"displayName": "Windows Release (vcpkg)",
"inherits": ["windows-vcpkg-base", "release"]
},
{
"name": "windows-x86-vcpkg-base",
"hidden": true,
"inherits": ["windows-base", "vcpkg-base"],
"architecture": {
"value": "x86",
"strategy": "external"
},
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
"VCPKG_TARGET_TRIPLET": "x86-windows",
"VCPKG_HOST_TRIPLET": "x64-windows",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
}
},
{
"name": "windows-debug-vcpkg-x86",
"displayName": "Windows x86 Debug (vcpkg)",
"inherits": ["windows-x86-vcpkg-base", "debug"]
},
{
"name": "windows-release-vcpkg-x86",
"displayName": "Windows x86 Release (vcpkg)",
"inherits": ["windows-x86-vcpkg-base", "release"]
},
{
"name": "windows-arm64-vcpkg-base",
"hidden": true,
"inherits": ["windows-base", "vcpkg-base"],
"architecture": {
"value": "arm64",
"strategy": "external"
},
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-windows",
"VCPKG_HOST_TRIPLET": "arm64-windows",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;examples"
}
},
{
"name": "windows-debug-vcpkg-arm64",
"displayName": "Windows ARM64 Debug (vcpkg)",
"inherits": ["windows-arm64-vcpkg-base", "debug"]
},
{
"name": "windows-release-vcpkg-arm64",
"displayName": "Windows ARM64 Release (vcpkg)",
"inherits": ["windows-arm64-vcpkg-base", "release"]
},
{
"name": "linux-base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/cmake-build/build/${presetName}",
"installDir": "${sourceDir}/cmake-build/install/${presetName}",
"cacheVariables": {
"CMAKE_CXX_COMPILER": "clang++",
"CMAKE_MAKE_PROGRAM": "ninja"
},
"inherits": "base",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
@@ -78,57 +141,88 @@
}
},
{
"name": "linux-base-vcpkg",
"name": "linux-vcpkg-base",
"hidden": true,
"inherits": "linux-base",
"inherits": ["linux-base", "vcpkg-base"],
"cacheVariables": {
"OMATH_BUILD_VIA_VCPKG": "ON",
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2"
}
},
{
"name": "linux-debug",
"displayName": "Linux Debug",
"inherits": "linux-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "linux-debug-vcpkg",
"displayName": "Linux Debug",
"inherits": "linux-base-vcpkg",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
"inherits": ["linux-base", "debug"]
},
{
"name": "linux-release",
"displayName": "Linux Release",
"inherits": "linux-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
"inherits": ["linux-base", "release"]
},
{
"name": "linux-debug-vcpkg",
"displayName": "Linux Debug (vcpkg)",
"inherits": ["linux-vcpkg-base", "debug"]
},
{
"name": "linux-release-vcpkg",
"displayName": "Linux Release",
"inherits": "linux-base-vcpkg",
"displayName": "Linux Release (vcpkg)",
"inherits": ["linux-vcpkg-base", "release"]
},
{
"name": "linux-x86-vcpkg-base",
"hidden": true,
"inherits": ["linux-base", "vcpkg-base"],
"architecture": {
"value": "x86",
"strategy": "external"
},
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
"CMAKE_C_FLAGS": "-m32",
"CMAKE_CXX_FLAGS": "-m32",
"VCPKG_TARGET_TRIPLET": "x86-linux",
"VCPKG_HOST_TRIPLET": "x64-linux",
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
}
},
{
"name": "linux-debug-vcpkg-x86",
"displayName": "Linux x86 Debug (vcpkg)",
"inherits": ["linux-x86-vcpkg-base", "debug"]
},
{
"name": "linux-release-vcpkg-x86",
"displayName": "Linux x86 Release (vcpkg)",
"inherits": ["linux-x86-vcpkg-base", "release"]
},
{
"name": "linux-arm64-vcpkg-base",
"hidden": true,
"inherits": ["linux-base", "vcpkg-base"],
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-linux",
"VCPKG_HOST_TRIPLET": "arm64-linux",
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
}
},
{
"name": "linux-debug-vcpkg-arm64",
"displayName": "Linux ARM64 Debug (vcpkg)",
"inherits": ["linux-arm64-vcpkg-base", "debug"]
},
{
"name": "linux-release-vcpkg-arm64",
"displayName": "Linux ARM64 Release (vcpkg)",
"inherits": ["linux-arm64-vcpkg-base", "release"]
},
{
"name": "darwin-base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/cmake-build/build/${presetName}",
"installDir": "${sourceDir}/cmake-build/install/${presetName}",
"inherits": "base",
"cacheVariables": {
"CMAKE_CXX_COMPILER": "clang++",
"CMAKE_MAKE_PROGRAM": "ninja"
"CMAKE_CXX_COMPILER": "clang++"
},
"condition": {
"type": "equals",
@@ -137,47 +231,456 @@
}
},
{
"name": "darwin-base-vcpkg",
"name": "darwin-vcpkg-base",
"hidden": true,
"inherits": "darwin-base",
"inherits": ["darwin-base", "vcpkg-base"],
"cacheVariables": {
"OMATH_BUILD_VIA_VCPKG": "ON",
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_INSTALLED_DIR": "${sourceDir}/cmake-build/vcpkg_installed",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
}
},
{
"name": "darwin-debug",
"displayName": "Darwin Debug",
"inherits": "darwin-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "darwin-debug-vcpkg",
"displayName": "Darwin Debug Vcpkg",
"inherits": "darwin-base-vcpkg",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
"displayName": "macOS Debug",
"inherits": ["darwin-base", "debug"]
},
{
"name": "darwin-release",
"displayName": "Darwin Release",
"inherits": "darwin-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
"displayName": "macOS Release",
"inherits": ["darwin-base", "release"]
},
{
"name": "darwin-debug-vcpkg",
"displayName": "macOS Debug (vcpkg)",
"inherits": ["darwin-vcpkg-base", "debug"]
},
{
"name": "darwin-release-vcpkg",
"displayName": "Darwin Release Vcpkg",
"inherits": "darwin-base-vcpkg",
"displayName": "macOS Release (vcpkg)",
"inherits": ["darwin-vcpkg-base", "release"]
},
{
"name": "darwin-x64-vcpkg-base",
"hidden": true,
"inherits": ["darwin-base", "vcpkg-base"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
"CMAKE_OSX_ARCHITECTURES": "x86_64",
"VCPKG_TARGET_TRIPLET": "x64-osx",
"VCPKG_HOST_TRIPLET": "x64-osx",
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples"
}
},
{
"name": "darwin-debug-vcpkg-x64",
"displayName": "macOS x64 Debug (vcpkg)",
"inherits": ["darwin-x64-vcpkg-base", "debug"]
},
{
"name": "darwin-release-vcpkg-x64",
"displayName": "macOS x64 Release (vcpkg)",
"inherits": ["darwin-x64-vcpkg-base", "release"]
},
{
"name": "ios-base",
"hidden": true,
"inherits": "base",
"cacheVariables": {
"CMAKE_SYSTEM_NAME": "iOS",
"CMAKE_OSX_DEPLOYMENT_TARGET": "18.5",
"CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED": "NO",
"CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED": "NO"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Darwin"
}
},
{
"name": "ios-vcpkg-base",
"hidden": true,
"inherits": ["ios-base", "vcpkg-base"],
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "arm64-ios",
"VCPKG_HOST_TRIPLET": "arm64-osx",
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
}
},
{
"name": "ios-debug-vcpkg",
"displayName": "iOS Debug (vcpkg)",
"inherits": ["ios-vcpkg-base", "debug"]
},
{
"name": "ios-release-vcpkg",
"displayName": "iOS Release (vcpkg)",
"inherits": ["ios-vcpkg-base", "release"]
},
{
"name": "freebsd-base",
"hidden": true,
"inherits": "base",
"cacheVariables": {
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "FreeBSD"
}
},
{
"name": "freebsd-vcpkg-base",
"hidden": true,
"inherits": ["freebsd-base", "vcpkg-base"],
"cacheVariables": {
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2"
}
},
{
"name": "freebsd-debug",
"displayName": "FreeBSD Debug",
"inherits": ["freebsd-base", "debug"]
},
{
"name": "freebsd-release",
"displayName": "FreeBSD Release",
"inherits": ["freebsd-base", "release"]
},
{
"name": "freebsd-debug-vcpkg",
"displayName": "FreeBSD Debug (vcpkg)",
"inherits": ["freebsd-vcpkg-base", "debug"]
},
{
"name": "freebsd-release-vcpkg",
"displayName": "FreeBSD Release (vcpkg)",
"inherits": ["freebsd-vcpkg-base", "release"]
},
{
"name": "android-base",
"hidden": true,
"inherits": "base",
"cacheVariables": {
"CMAKE_SYSTEM_NAME": "Android",
"CMAKE_SYSTEM_VERSION": "24",
"CMAKE_ANDROID_NDK": "$env{ANDROID_NDK_HOME}",
"CMAKE_ANDROID_STL_TYPE": "c++_static"
}
},
{
"name": "android-vcpkg-base",
"hidden": true,
"inherits": ["android-base", "vcpkg-base"],
"cacheVariables": {
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
}
},
{
"name": "android-arm64-base",
"hidden": true,
"inherits": "android-base",
"cacheVariables": {
"CMAKE_ANDROID_ARCH_ABI": "arm64-v8a"
}
},
{
"name": "android-arm64-vcpkg-base",
"hidden": true,
"inherits": "android-vcpkg-base",
"cacheVariables": {
"CMAKE_ANDROID_ARCH_ABI": "arm64-v8a",
"VCPKG_TARGET_TRIPLET": "arm64-android"
}
},
{
"name": "android-arm64-debug",
"displayName": "Android arm64-v8a Debug",
"inherits": ["android-arm64-base", "debug"]
},
{
"name": "android-arm64-release",
"displayName": "Android arm64-v8a Release",
"inherits": ["android-arm64-base", "release"]
},
{
"name": "android-arm64-debug-vcpkg",
"displayName": "Android arm64-v8a Debug (vcpkg)",
"inherits": ["android-arm64-vcpkg-base", "debug"]
},
{
"name": "android-arm64-release-vcpkg",
"displayName": "Android arm64-v8a Release (vcpkg)",
"inherits": ["android-arm64-vcpkg-base", "release"]
},
{
"name": "android-arm-neon-base",
"hidden": true,
"inherits": "android-base",
"cacheVariables": {
"CMAKE_ANDROID_ARCH_ABI": "armeabi-v7a",
"CMAKE_ANDROID_ARM_NEON": "ON"
}
},
{
"name": "android-arm-neon-vcpkg-base",
"hidden": true,
"inherits": "android-vcpkg-base",
"cacheVariables": {
"CMAKE_ANDROID_ARCH_ABI": "armeabi-v7a",
"CMAKE_ANDROID_ARM_NEON": "ON",
"VCPKG_TARGET_TRIPLET": "arm-neon-android"
}
},
{
"name": "android-arm-neon-debug",
"displayName": "Android armeabi-v7a NEON Debug",
"inherits": ["android-arm-neon-base", "debug"]
},
{
"name": "android-arm-neon-release",
"displayName": "Android armeabi-v7a NEON Release",
"inherits": ["android-arm-neon-base", "release"]
},
{
"name": "android-arm-neon-debug-vcpkg",
"displayName": "Android armeabi-v7a NEON Debug (vcpkg)",
"inherits": ["android-arm-neon-vcpkg-base", "debug"]
},
{
"name": "android-arm-neon-release-vcpkg",
"displayName": "Android armeabi-v7a NEON Release (vcpkg)",
"inherits": ["android-arm-neon-vcpkg-base", "release"]
},
{
"name": "android-x64-base",
"hidden": true,
"inherits": "android-base",
"cacheVariables": {
"CMAKE_ANDROID_ARCH_ABI": "x86_64"
}
},
{
"name": "android-x64-vcpkg-base",
"hidden": true,
"inherits": "android-vcpkg-base",
"cacheVariables": {
"CMAKE_ANDROID_ARCH_ABI": "x86_64",
"VCPKG_TARGET_TRIPLET": "x64-android"
}
},
{
"name": "android-x64-debug",
"displayName": "Android x86_64 Debug",
"inherits": ["android-x64-base", "debug"]
},
{
"name": "android-x64-release",
"displayName": "Android x86_64 Release",
"inherits": ["android-x64-base", "release"]
},
{
"name": "android-x64-debug-vcpkg",
"displayName": "Android x86_64 Debug (vcpkg)",
"inherits": ["android-x64-vcpkg-base", "debug"]
},
{
"name": "android-x64-release-vcpkg",
"displayName": "Android x86_64 Release (vcpkg)",
"inherits": ["android-x64-vcpkg-base", "release"]
},
{
"name": "android-x86-base",
"hidden": true,
"inherits": "android-base",
"cacheVariables": {
"CMAKE_ANDROID_ARCH_ABI": "x86"
}
},
{
"name": "android-x86-vcpkg-base",
"hidden": true,
"inherits": "android-vcpkg-base",
"cacheVariables": {
"CMAKE_ANDROID_ARCH_ABI": "x86",
"VCPKG_TARGET_TRIPLET": "x86-android"
}
},
{
"name": "android-x86-debug",
"displayName": "Android x86 Debug",
"inherits": ["android-x86-base", "debug"]
},
{
"name": "android-x86-release",
"displayName": "Android x86 Release",
"inherits": ["android-x86-base", "release"]
},
{
"name": "android-x86-debug-vcpkg",
"displayName": "Android x86 Debug (vcpkg)",
"inherits": ["android-x86-vcpkg-base", "debug"]
},
{
"name": "android-x86-release-vcpkg",
"displayName": "Android x86 Release (vcpkg)",
"inherits": ["android-x86-vcpkg-base", "release"]
},
{
"name": "android-debug",
"displayName": "Android Debug (default: arm64)",
"inherits": "android-arm64-debug"
},
{
"name": "android-release",
"displayName": "Android Release (default: arm64)",
"inherits": "android-arm64-release"
},
{
"name": "android-debug-vcpkg",
"displayName": "Android Debug (default: arm64, vcpkg)",
"inherits": "android-arm64-debug-vcpkg"
},
{
"name": "android-release-vcpkg",
"displayName": "Android Release (default: arm64, vcpkg)",
"inherits": "android-arm64-release-vcpkg"
},
{
"name": "wasm-base",
"hidden": true,
"inherits": "base"
},
{
"name": "wasm-vcpkg-base",
"hidden": true,
"inherits": ["wasm-base", "vcpkg-base"],
"cacheVariables": {
"VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "$env{EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake",
"VCPKG_TARGET_TRIPLET": "wasm32-emscripten",
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
}
},
{
"name": "wasm-debug-vcpkg",
"displayName": "WebAssembly Debug (vcpkg)",
"inherits": ["wasm-vcpkg-base", "debug"]
},
{
"name": "wasm-release-vcpkg",
"displayName": "WebAssembly Release (vcpkg)",
"inherits": ["wasm-vcpkg-base", "release"]
},
{
"name": "mingw-base",
"hidden": true,
"inherits": "base",
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc",
"CMAKE_CXX_COMPILER": "g++"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "mingw-vcpkg-base",
"hidden": true,
"inherits": ["mingw-base", "vcpkg-base"],
"environment": {
"VCPKG_DEFAULT_HOST_TRIPLET": "x64-mingw-dynamic"
},
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "x64-mingw-dynamic",
"VCPKG_HOST_TRIPLET": "x64-mingw-dynamic",
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
}
},
{
"name": "mingw-debug",
"displayName": "MinGW x64 Debug",
"inherits": ["mingw-base", "debug"]
},
{
"name": "mingw-release",
"displayName": "MinGW x64 Release",
"inherits": ["mingw-base", "release"]
},
{
"name": "mingw-debug-vcpkg",
"displayName": "MinGW x64 Debug (vcpkg)",
"inherits": ["mingw-vcpkg-base", "debug"]
},
{
"name": "mingw-release-vcpkg",
"displayName": "MinGW x64 Release (vcpkg)",
"inherits": ["mingw-vcpkg-base", "release"]
},
{
"name": "mingw-ucrt-release-vcpkg",
"displayName": "MinGW UCRT64 Release (vcpkg)",
"inherits": ["mingw-vcpkg-base", "release"]
},
{
"name": "mingw32-base",
"hidden": true,
"inherits": "base",
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc",
"CMAKE_CXX_COMPILER": "g++"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "mingw32-vcpkg-base",
"hidden": true,
"inherits": ["mingw32-base", "vcpkg-base"],
"environment": {
"VCPKG_DEFAULT_HOST_TRIPLET": "x86-mingw-dynamic"
},
"cacheVariables": {
"VCPKG_TARGET_TRIPLET": "x86-mingw-dynamic",
"VCPKG_HOST_TRIPLET": "x86-mingw-dynamic",
"VCPKG_MANIFEST_FEATURES": "tests;imgui"
}
},
{
"name": "mingw32-debug",
"displayName": "MinGW x86 Debug",
"inherits": ["mingw32-base", "debug"]
},
{
"name": "mingw32-release",
"displayName": "MinGW x86 Release",
"inherits": ["mingw32-base", "release"]
},
{
"name": "mingw32-debug-vcpkg",
"displayName": "MinGW x86 Debug (vcpkg)",
"inherits": ["mingw32-vcpkg-base", "debug"]
},
{
"name": "mingw32-release-vcpkg",
"displayName": "MinGW x86 Release (vcpkg)",
"inherits": ["mingw32-vcpkg-base", "release"]
}
]
}
}

46
LICENSE
View File

@@ -1,4 +1,4 @@
Copyright (C) 2024-2025 Orange++ <orange_github@proton.me>
Copyright (C) 2023-2025 Orange++ orange_github@proton.me
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
@@ -14,46 +14,4 @@ freely, subject to the following restrictions:
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
4. If you are an employee, contractor, volunteer, representative,
or have any other affiliation (past or present)
with any of the following entities:
* "Advertising Placement Services" LLC
* "NEW SOLUTIONS VERTICALS" LLC
* "Autoexpert" LLC
* "Creditit" LLC
* "Yandex.Taxi" LLC
* "Yandex.Eda" LLC
* "Yandex.Lavka" LLC
* "Yandex.Telecom" LLC
* "Yandex.Cloud" LLC
* "Micromobility" LLC
* "MM-Tech" LLC
* "Carsharing" LLC
* "Yandex.Drive" LLC
* "EDADIL PROMO" LLC
* "Kinopoisk" LLC
* "Yandex.Music" LLC
* "Refueling (Yandex.Zapravki)" LLC
* "Yandex.Pay" LLC
* "Financial and Payment Technologies" LLC
* "Yandex.Delivery" LLC
* "Delivery Club" LLC
* "Yandex.Check" LLC
* "SMB-Service" LLC
* "ADV-TECH" LLC
* "Yandex Fantech" LLC
* "Yandex Smena" LLC
* "Market.Operations" LLC
* "Yandex.Market" LLC
* "ID Tech" LLC
* "Yandex.Crowd" LLC
* "Yandex" LLC
* "Rutube" LLC
* "Kaspersky" LLC
Or if you represent or are associated with any legal, organizational, or
professional entity providing services to or on behalf of the aforementioned entities:
You are expressly forbidden from accessing, using, modifying, distributing, or
interacting with the Software and its source code in any form. You must immediately
delete or destroy any physical or digital copies of the Software and/or
its source code, including any derivative works, tools, or information obtained from the Software.
3. This notice may not be removed or altered from any source distribution.

View File

@@ -2,9 +2,10 @@
![banner](docs/images/logos/omath_logo_macro.png)
![Static Badge](https://img.shields.io/badge/license-libomath-orange)
![GitHub License](https://img.shields.io/github/license/orange-cpp/omath)
![GitHub contributors](https://img.shields.io/github/contributors/orange-cpp/omath)
![GitHub top language](https://img.shields.io/github/languages/top/orange-cpp/omath)
![GitHub repo size](https://img.shields.io/github/repo-size/orange-cpp/omath)
[![CodeFactor](https://www.codefactor.io/repository/github/orange-cpp/omath/badge)](https://www.codefactor.io/repository/github/orange-cpp/omath)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/orange-cpp/omath/cmake-multi-platform.yml)
[![Vcpkg package](https://repology.org/badge/version-for-repo/vcpkg/orange-math.svg)](https://repology.org/project/orange-math/versions)
@@ -43,7 +44,7 @@ It provides the latest features, is highly customizable, has all for cheat devel
</a>
</div>
## 🚀 Quick Example
## Quick Example
```cpp
#include <omath/omath.hpp>
@@ -68,20 +69,20 @@ if (auto screen = camera.world_to_screen(world_position)) {
}
```
**[➡️ See more examples and tutorials][TUTORIALS]**
**[See more examples and tutorials][TUTORIALS]**
# Features
- **🚀 Efficiency**: Optimized for performance, ensuring quick computations using AVX2.
- **🎯 Versatility**: Includes a wide array of mathematical functions and algorithms.
- **Ease of Use**: Simplified interface for convenient integration into various projects.
- **🎮 Projectile Prediction**: Projectile prediction engine with O(N) algo complexity, that can power you projectile aim-bot.
- **📐 3D Projection**: No need to find view-projection matrix anymore you can make your own projection pipeline.
- **💥 Collision Detection**: Production ready code to handle collision detection by using simple interfaces.
- **📦 No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution
- **🔧 Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types!
- **🎯 Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine and canonical OpenGL**.
- **🌍 Cross platform**: Supports Windows, MacOS and Linux.
- **🔍 Algorithms**: Has ability to scan for byte pattern with wildcards in PE files/modules, binary slices, works even with Wine apps.
# Features
- **Efficiency**: Optimized for performance, ensuring quick computations using AVX2.
- **Versatility**: Includes a wide array of mathematical functions and algorithms.
- **Ease of Use**: Simplified interface for convenient integration into various projects.
- **Projectile Prediction**: Projectile prediction engine with O(N) algo complexity, that can power you projectile aim-bot.
- **3D Projection**: No need to find view-projection matrix anymore you can make your own projection pipeline.
- **Collision Detection**: Production ready code to handle collision detection by using simple interfaces.
- **No Additional Dependencies**: No additional dependencies need to use OMath except unit test execution
- **Ready for meta-programming**: Omath use templates for common types like Vectors, Matrixes etc, to handle all types!
- **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine and canonical OpenGL**.
- **Cross platform**: Supports Windows, MacOS and Linux.
- **Algorithms**: Has ability to scan for byte pattern with wildcards in PE files/modules, binary slices, works even with Wine apps.
<div align = center>
# Gallery
@@ -115,7 +116,7 @@ if (auto screen = camera.world_to_screen(world_position)) {
</div>
## 📚 Documentation
## Documentation
- **[Getting Started Guide](http://libomath.org/getting_started/)** - Installation and first steps
- **[API Overview](http://libomath.org/api_overview/)** - Complete API reference
@@ -124,14 +125,14 @@ if (auto screen = camera.world_to_screen(world_position)) {
- **[Troubleshooting](http://libomath.org/troubleshooting/)** - Solutions to common issues
- **[Best Practices](http://libomath.org/best_practices/)** - Guidelines for effective usage
## 🤝 Community & Support
## Community & Support
- **Discord**: [Join our community](https://discord.gg/eDgdaWbqwZ)
- **Telegram**: [@orangennotes](https://t.me/orangennotes)
- **Issues**: [Report bugs or request features](https://github.com/orange-cpp/omath/issues)
- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines
# 💘 Acknowledgments
# Acknowledgments
- [All contributors](https://github.com/orange-cpp/omath/graphs/contributors)
<!----------------------------------{ Images }--------------------------------->

View File

@@ -1 +1 @@
4.4.0
4.6.0

View File

@@ -17,3 +17,7 @@ else()
find_package(benchmark CONFIG REQUIRED) # Benchmark is being linked as vcpkg package
target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark omath)
endif ()
if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(${PROJECT_NAME})
endif()

86
cmake/Coverage.cmake Normal file
View File

@@ -0,0 +1,86 @@
# cmake/Coverage.cmake
include_guard(GLOBAL)
function(omath_setup_coverage TARGET_NAME)
if(ANDROID OR IOS OR EMSCRIPTEN)
return()
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND MSVC)
target_compile_options(${TARGET_NAME} PRIVATE
-fprofile-instr-generate
-fcoverage-mapping
/Zi
)
target_link_options(${TARGET_NAME} PRIVATE
-fprofile-instr-generate
-fcoverage-mapping
/DEBUG:FULL
/INCREMENTAL:NO
/PROFILE
)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
target_compile_options(${TARGET_NAME} PRIVATE
-fprofile-instr-generate
-fcoverage-mapping
-g
-O0
)
target_link_options(${TARGET_NAME} PRIVATE
-fprofile-instr-generate
-fcoverage-mapping
)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(${TARGET_NAME} PRIVATE
--coverage
-g
-O0
)
target_link_options(${TARGET_NAME} PRIVATE
--coverage
)
endif()
if(TARGET coverage)
return()
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND MSVC)
message(STATUS "MSVC detected: Use VS Code Coverage from CI workflow")
add_custom_target(coverage
DEPENDS unit_tests
COMMAND $<TARGET_FILE:unit_tests>
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Running tests for coverage (use VS Code Coverage from CI)"
)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang")
add_custom_target(coverage
DEPENDS unit_tests
COMMAND bash "${CMAKE_SOURCE_DIR}/scripts/coverage-llvm.sh"
"${CMAKE_SOURCE_DIR}"
"${CMAKE_BINARY_DIR}"
"$<TARGET_FILE:unit_tests>"
"${CMAKE_BINARY_DIR}/coverage"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Running LLVM coverage"
)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
add_custom_target(coverage
DEPENDS unit_tests
COMMAND $<TARGET_FILE:unit_tests> || true
COMMAND lcov --capture --directory "${CMAKE_BINARY_DIR}"
--output-file "${CMAKE_BINARY_DIR}/coverage.info"
--ignore-errors mismatch,gcov
COMMAND lcov --remove "${CMAKE_BINARY_DIR}/coverage.info"
"*/tests/*" "*/gtest/*" "*/googletest/*" "*/_deps/*" "/usr/*"
--output-file "${CMAKE_BINARY_DIR}/coverage.info"
--ignore-errors unused
COMMAND genhtml "${CMAKE_BINARY_DIR}/coverage.info"
--output-directory "${CMAKE_BINARY_DIR}/coverage"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
COMMENT "Running lcov/genhtml"
)
endif()
endfunction()

45
cmake/Valgrind.cmake Normal file
View File

@@ -0,0 +1,45 @@
# cmake/Valgrind.cmake
if(DEFINED __OMATH_VALGRIND_INCLUDED)
return()
endif()
set(__OMATH_VALGRIND_INCLUDED TRUE)
find_program(VALGRIND_EXECUTABLE valgrind)
option(OMATH_ENABLE_VALGRIND "Enable Valgrind target for memory checking" ON)
if(OMATH_ENABLE_VALGRIND AND NOT TARGET valgrind_all)
add_custom_target(valgrind_all)
endif()
function(omath_setup_valgrind TARGET_NAME)
if(NOT OMATH_ENABLE_VALGRIND)
return()
endif()
if(NOT VALGRIND_EXECUTABLE)
message(WARNING "OMATH_ENABLE_VALGRIND is ON, but 'valgrind' executable was not found.")
return()
endif()
set(VALGRIND_FLAGS
--leak-check=full
--show-leak-kinds=all
--track-origins=yes
--error-exitcode=99
)
set(VALGRIND_TARGET "valgrind_${TARGET_NAME}")
if(NOT TARGET ${VALGRIND_TARGET})
add_custom_target(${VALGRIND_TARGET}
DEPENDS ${TARGET_NAME}
COMMAND ${VALGRIND_EXECUTABLE} ${VALGRIND_FLAGS} $<TARGET_FILE:${TARGET_NAME}>
WORKING_DIRECTORY $<TARGET_FILE_DIR:${TARGET_NAME}>
COMMENT "Running Valgrind memory check on ${TARGET_NAME}..."
USES_TERMINAL
)
add_dependencies(valgrind_all ${VALGRIND_TARGET})
endif()
endfunction()

View File

@@ -23,10 +23,15 @@ add_executable(example_glfw3 example_glfw3.cpp)
set_target_properties(example_glfw3 PROPERTIES CXX_STANDARD 26
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}"
)
find_package(GLEW REQUIRED)
find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(example_glfw3 PRIVATE omath::omath GLEW::GLEW glfw)
target_link_libraries(example_glfw3 PRIVATE omath::omath GLEW::GLEW glfw)
if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(example_projection_matrix_builder)
omath_setup_valgrind(example_signature_scan)
omath_setup_valgrind(example_glfw3)
endif()

View File

@@ -127,8 +127,8 @@ int main()
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
const int SCR_WIDTH = 800;
const int SCR_HEIGHT = 600;
constexpr int SCR_WIDTH = 800;
constexpr int SCR_HEIGHT = 600;
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "omath cube + camera (GLEW)", nullptr, nullptr);
if (!window)
@@ -239,8 +239,8 @@ int main()
// flatten EBO to GL indices
std::vector<GLuint> flatIndices;
flatIndices.reserve(cube.m_vertex_array_object.size() * 3);
for (const auto& tri : cube.m_vertex_array_object)
flatIndices.reserve(cube.m_element_buffer_object.size() * 3);
for (const auto& tri : cube.m_element_buffer_object)
{
flatIndices.push_back(tri.x);
flatIndices.push_back(tri.y);

View File

@@ -75,8 +75,9 @@ namespace omath::collision
if (heap.empty())
break;
const int fidx = heap.top().idx;
const Face face = faces[fidx];
//FIXME: STORE REF VALUE, DO NOT USE
// AFTER IF STATEMENT BLOCK
const Face& face = faces[heap.top().idx];
// Get the furthest point in face normal direction
const VectorType p = support_point(a, b, face.n);

View File

@@ -17,6 +17,13 @@
#undef near
#undef far
// Undefine FreeBSD/BSD system macros that conflict with method names
#ifdef minor
#undef minor
#endif
#ifdef major
#undef major
#endif
namespace omath
{
struct MatSize
@@ -373,7 +380,7 @@ namespace omath
{
const auto det = determinant();
if (det == 0)
if (std::abs(det) < std::numeric_limits<Type>::epsilon())
return std::nullopt;
const auto transposed_mat = transposed();

View File

@@ -233,10 +233,10 @@ namespace omath
return Angle<float, 0.f, 180.f, AngleFlags::Clamped>::from_radians(std::acos(dot(other) / bottom));
}
[[nodiscard]] bool is_perpendicular(const Vector3& other) const noexcept
[[nodiscard]] bool is_perpendicular(const Vector3& other, Type epsilon = static_cast<Type>(0.0001)) const noexcept
{
if (const auto angle = angle_between(other))
return angle->as_degrees() == static_cast<Type>(90);
return std::abs(angle->as_degrees() - static_cast<Type>(90)) <= epsilon;
return false;
}

View File

@@ -8,6 +8,7 @@
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include "omath/projection/error_codes.hpp"
#include <cmath>
#include <expected>
#include <omath/trigonometry/angle.hpp>
#include <type_traits>
@@ -229,10 +230,11 @@ namespace omath::projection
auto projected = get_view_projection_matrix()
* mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(world_position);
if (projected.at(3, 0) == 0.0f)
const auto& w = projected.at(3, 0);
if (w <= std::numeric_limits<float>::epsilon())
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
projected /= projected.at(3, 0);
projected /= w;
if (is_ndc_out_of_bounds(projected))
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
@@ -250,10 +252,12 @@ namespace omath::projection
auto inverted_projection =
inv_view_proj.value() * mat_column_from_vector<float, Mat4X4Type::get_store_ordering()>(ndc);
if (!inverted_projection.at(3, 0))
const auto& w = inverted_projection.at(3, 0);
if (std::abs(w) < std::numeric_limits<float>::epsilon())
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
inverted_projection /= inverted_projection.at(3, 0);
inverted_projection /= w;
return Vector3<float>{inverted_projection.at(0, 0), inverted_projection.at(1, 0),
inverted_projection.at(2, 0)};
@@ -290,7 +294,9 @@ namespace omath::projection
template<class Type>
[[nodiscard]] constexpr static bool is_ndc_out_of_bounds(const Type& ndc) noexcept
{
return std::ranges::any_of(ndc.raw_array(), [](const auto& val) { return val < -1 || val > 1; });
constexpr auto eps = std::numeric_limits<float>::epsilon();
return std::ranges::any_of(ndc.raw_array(),
[](const auto& val) { return val < -1.0f - eps || val > 1.0f + eps; });
}
// NDC REPRESENTATION:
@@ -347,7 +353,7 @@ namespace omath::projection
if constexpr (screen_start == ScreenStart::TOP_LEFT_CORNER)
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f, 1.f - screen_pos.y / m_view_port.m_height * 2.f,
screen_pos.z};
else if (screen_start == ScreenStart::BOTTOM_LEFT_CORNER)
else if constexpr (screen_start == ScreenStart::BOTTOM_LEFT_CORNER)
return {screen_pos.x / m_view_port.m_width * 2.f - 1.f,
(screen_pos.y / m_view_port.m_height - 0.5f) * 2.f, screen_pos.z};
else

View File

@@ -46,27 +46,26 @@ namespace omath
switch (i % 6)
{
case 0:
r = value, g = t, b = p;
break;
case 1:
r = q, g = value, b = p;
break;
case 2:
r = p, g = value, b = t;
break;
case 3:
r = p, g = q, b = value;
break;
case 4:
r = t, g = p, b = value;
break;
case 5:
r = value, g = p, b = q;
break;
default:
return {0.f, 0.f, 0.f, 0.f};
case 0:
r = value, g = t, b = p;
break;
case 1:
r = q, g = value, b = p;
break;
case 2:
r = p, g = value, b = t;
break;
case 3:
r = p, g = q, b = value;
break;
case 4:
r = t, g = p, b = value;
break;
case 5:
r = value, g = p, b = q;
break;
default:
std::unreachable();
}
return {r, g, b, 1.f};
@@ -190,7 +189,7 @@ template<>
struct std::formatter<omath::Color> // NOLINT(*-dcl58-cpp)
{
[[nodiscard]]
static constexpr auto parse(std::format_parse_context& ctx)
static constexpr auto parse(const std::format_parse_context& ctx)
{
return ctx.begin();
}
@@ -207,6 +206,6 @@ struct std::formatter<omath::Color> // NOLINT(*-dcl58-cpp)
if constexpr (std::is_same_v<typename FormatContext::char_type, char8_t>)
return std::format_to(ctx.out(), u8"{}", col.to_u8string());
return std::unreachable();
std::unreachable();
}
};

View File

@@ -0,0 +1,25 @@
//
// Created by Vladislav on 30.12.2025.
//
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string_view>
#include "section_scan_result.hpp"
namespace omath
{
class ElfPatternScanner final
{
public:
[[nodiscard]]
static std::optional<std::uintptr_t>
scan_for_pattern_in_loaded_module(const void* module_base_address, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
[[nodiscard]]
static std::optional<SectionScanResult>
scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
};
} // namespace omath

View File

@@ -51,9 +51,13 @@ namespace omath
const auto whole_range_size = static_cast<std::ptrdiff_t>(std::distance(begin, end));
const std::ptrdiff_t scan_size = whole_range_size - static_cast<std::ptrdiff_t>(pattern.size());
const auto pattern_size = static_cast<std::ptrdiff_t>(parsed_pattern->size());
const std::ptrdiff_t scan_size = whole_range_size - pattern_size;
for (std::ptrdiff_t i = 0; i < scan_size; i++)
if (scan_size < 0)
return end;
for (std::ptrdiff_t i = 0; i <= scan_size; i++)
{
bool found = true;

View File

@@ -7,23 +7,20 @@
#include <filesystem>
#include <optional>
#include <string_view>
#include "section_scan_result.hpp"
namespace omath
{
struct PeSectionScanResult
{
std::uint64_t virtual_base_addr;
std::uint64_t raw_base_addr;
std::ptrdiff_t target_offset;
};
class PePatternScanner final
{
public:
[[nodiscard]]
static std::optional<std::uintptr_t> scan_for_pattern_in_loaded_module(const void* module_base_address,
const std::string_view& pattern);
static std::optional<std::uintptr_t>
scan_for_pattern_in_loaded_module(const void* module_base_address, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
[[nodiscard]]
static std::optional<PeSectionScanResult>
static std::optional<SectionScanResult>
scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern,
const std::string_view& target_section_name = ".text");
};

View File

@@ -0,0 +1,16 @@
//
// Created by Vladislav on 01.01.2026.
//
#pragma once
#include <cstddef>
#include <cstdint>
namespace omath
{
struct SectionScanResult final
{
std::uintptr_t virtual_base_addr;
std::uintptr_t raw_base_addr;
std::ptrdiff_t target_offset;
};
}

169
scripts/coverage-llvm.sh Executable file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env bash
# scripts/coverage-llvm.sh
# LLVM coverage script that generates LCOV-style reports
set -e
SOURCE_DIR="${1:-.}"
BINARY_DIR="${2:-cmake-build/build}"
TEST_BINARY="${3:-}"
OUTPUT_DIR="${4:-${BINARY_DIR}/coverage}"
echo "[*] Source dir: ${SOURCE_DIR}"
echo "[*] Binary dir: ${BINARY_DIR}"
echo "[*] Output dir: ${OUTPUT_DIR}"
# Find llvm tools - handle versioned names (Linux) and xcrun (macOS)
find_llvm_tool() {
local tool_name="$1"
# macOS: use xcrun
if [[ "$(uname)" == "Darwin" ]]; then
if xcrun --find "${tool_name}" &>/dev/null; then
echo "xcrun ${tool_name}"
return 0
fi
fi
# Try versioned names (Linux with LLVM 21, 20, 19, etc.)
for version in 21 20 19 18 17 ""; do
local versioned_name="${tool_name}${version:+-$version}"
if command -v "${versioned_name}" &>/dev/null; then
echo "${versioned_name}"
return 0
fi
done
echo ""
return 1
}
LLVM_PROFDATA=$(find_llvm_tool "llvm-profdata")
LLVM_COV=$(find_llvm_tool "llvm-cov")
if [[ -z "${LLVM_PROFDATA}" ]] || [[ -z "${LLVM_COV}" ]]; then
echo "Error: llvm-profdata or llvm-cov not found" >&2
echo "On Linux, install llvm or clang package" >&2
echo "On macOS, Xcode command line tools should provide these" >&2
exit 1
fi
echo "[*] Using: ${LLVM_PROFDATA}"
echo "[*] Using: ${LLVM_COV}"
# Find test binary
if [[ -z "${TEST_BINARY}" ]]; then
for path in \
"${SOURCE_DIR}/out/Debug/unit_tests" \
"${SOURCE_DIR}/out/Release/unit_tests" \
"${BINARY_DIR}/unit_tests" \
"${BINARY_DIR}/tests/unit_tests"; do
if [[ -x "${path}" ]]; then
TEST_BINARY="${path}"
break
fi
done
fi
if [[ -z "${TEST_BINARY}" ]] || [[ ! -x "${TEST_BINARY}" ]]; then
echo "Error: unit_tests binary not found" >&2
echo "Searched in: out/Debug, out/Release, ${BINARY_DIR}" >&2
exit 1
fi
echo "[*] Test binary: ${TEST_BINARY}"
# Clean previous coverage data
rm -rf "${OUTPUT_DIR}"
rm -f "${BINARY_DIR}"/*.profraw "${BINARY_DIR}"/*.profdata
mkdir -p "${OUTPUT_DIR}"
# Run tests with profiling enabled
PROFILE_FILE="${BINARY_DIR}/default_%p.profraw"
echo "[*] Running tests with LLVM_PROFILE_FILE=${PROFILE_FILE}"
export LLVM_PROFILE_FILE="${PROFILE_FILE}"
"${TEST_BINARY}" || echo "[!] Some tests failed, continuing with coverage..."
# Find all generated .profraw files
PROFRAW_FILES=$(find "${BINARY_DIR}" -name "*.profraw" -type f 2>/dev/null)
if [[ -z "${PROFRAW_FILES}" ]]; then
# Also check current directory
PROFRAW_FILES=$(find . -maxdepth 3 -name "*.profraw" -type f 2>/dev/null)
fi
if [[ -z "${PROFRAW_FILES}" ]]; then
echo "Error: No .profraw files generated" >&2
echo "Make sure the binary was built with -fprofile-instr-generate -fcoverage-mapping" >&2
exit 1
fi
echo "[*] Found profraw files:"
echo "${PROFRAW_FILES}"
# Merge profiles
PROFDATA_FILE="${BINARY_DIR}/coverage.profdata"
echo "[*] Merging profiles into ${PROFDATA_FILE}"
${LLVM_PROFDATA} merge -sparse ${PROFRAW_FILES} -o "${PROFDATA_FILE}"
# Generate text summary
echo "[*] Coverage Summary:"
${LLVM_COV} report "${TEST_BINARY}" \
-instr-profile="${PROFDATA_FILE}" \
-ignore-filename-regex="tests/.*" \
-ignore-filename-regex="googletest/.*" \
-ignore-filename-regex="gtest/.*" \
-ignore-filename-regex="_deps/.*" \
-ignore-filename-regex="vcpkg_installed/.*"
# Export lcov format (for tools like codecov)
LCOV_FILE="${OUTPUT_DIR}/coverage.lcov"
echo "[*] Exporting LCOV format to ${LCOV_FILE}"
${LLVM_COV} export "${TEST_BINARY}" \
-instr-profile="${PROFDATA_FILE}" \
-format=lcov \
-ignore-filename-regex="tests/.*" \
-ignore-filename-regex="googletest/.*" \
-ignore-filename-regex="gtest/.*" \
-ignore-filename-regex="_deps/.*" \
-ignore-filename-regex="vcpkg_installed/.*" \
> "${LCOV_FILE}" || true
# Generate LCOV-style HTML report using genhtml
if command -v genhtml >/dev/null 2>&1; then
echo "[*] Generating LCOV-style HTML report using genhtml"
genhtml "${LCOV_FILE}" \
--ignore-errors inconsistent,corrupt \
--output-directory "${OUTPUT_DIR}" \
--title "Omath Coverage Report" \
--show-details \
--legend \
--demangle-cpp \
--num-spaces 4 \
--sort \
--function-coverage \
--branch-coverage
echo "[*] LCOV-style HTML report generated at: ${OUTPUT_DIR}/index.html"
else
echo "[!] genhtml not found. Installing lcov package..."
echo "[!] On Ubuntu/Debian: sudo apt-get install lcov"
echo "[!] On macOS: brew install lcov"
echo "[!] Falling back to LLVM HTML report..."
# Fall back to LLVM HTML report
${LLVM_COV} show "${TEST_BINARY}" \
-instr-profile="${PROFDATA_FILE}" \
-format=html \
-output-dir="${OUTPUT_DIR}" \
-show-line-counts-or-regions \
-show-instantiations=false \
-ignore-filename-regex="tests/.*" \
-ignore-filename-regex="googletest/.*" \
-ignore-filename-regex="gtest/.*" \
-ignore-filename-regex="_deps/.*" \
-ignore-filename-regex="vcpkg_installed/.*"
fi
echo "[*] Coverage report generated at: ${OUTPUT_DIR}/index.html"
echo "[*] LCOV file at: ${LCOV_FILE}"

8
scripts/coverage.bat.in Normal file
View File

@@ -0,0 +1,8 @@
@echo off
REM scripts/coverage.bat.in
REM Simple wrapper to run coverage.ps1
set SOURCE_DIR=@CMAKE_SOURCE_DIR@
set BINARY_DIR=@CMAKE_BINARY_DIR@
powershell -ExecutionPolicy Bypass -File "%BINARY_DIR%\scripts\coverage.ps1" -SourceDir "%SOURCE_DIR%" -BinaryDir "%BINARY_DIR%" %*

132
scripts/coverage.ps1.in Normal file
View File

@@ -0,0 +1,132 @@
# scripts/coverage.ps1.in
# Windows coverage script using OpenCppCoverage
param(
[Parameter(Mandatory=$true)]
[string]$SourceDir,
[Parameter(Mandatory=$true)]
[string]$BinaryDir,
[string]$TestBinary = "",
[switch]$Cobertura,
[switch]$Html
)
$ErrorActionPreference = "Stop"
# CMake-injected variables
$LCOV_IGNORE_ERRORS = '@LCOV_IGNORE_ERRORS@'
# Resolve paths
$SourceDir = Resolve-Path $SourceDir
$BinaryDir = Resolve-Path $BinaryDir
Write-Host "[*] Source directory: $SourceDir" -ForegroundColor Cyan
Write-Host "[*] Binary directory: $BinaryDir" -ForegroundColor Cyan
# Find test binary
if (-not $TestBinary) {
$searchPaths = @(
"$BinaryDir\Debug\unit_tests.exe",
"$BinaryDir\Release\unit_tests.exe",
"$BinaryDir\unit_tests.exe",
"$SourceDir\out\Debug\unit_tests.exe",
"$SourceDir\out\Release\unit_tests.exe"
)
foreach ($path in $searchPaths) {
if (Test-Path $path) {
$TestBinary = $path
break
}
}
}
if (-not $TestBinary -or -not (Test-Path $TestBinary)) {
Write-Error "unit_tests.exe not found. Searched: $($searchPaths -join ', ')"
exit 1
}
$TestBinary = Resolve-Path $TestBinary
Write-Host "[*] Test binary: $TestBinary" -ForegroundColor Cyan
# Check for OpenCppCoverage
$opencppcov = Get-Command "OpenCppCoverage" -ErrorAction SilentlyContinue
if (-not $opencppcov) {
# Try common installation paths
$possiblePaths = @(
"$env:ProgramFiles\OpenCppCoverage\OpenCppCoverage.exe",
"${env:ProgramFiles(x86)}\OpenCppCoverage\OpenCppCoverage.exe",
"$env:LOCALAPPDATA\Programs\OpenCppCoverage\OpenCppCoverage.exe"
)
foreach ($path in $possiblePaths) {
if (Test-Path $path) {
$opencppcov = Get-Item $path
break
}
}
}
if (-not $opencppcov) {
Write-Host @"
OpenCppCoverage not found!
Install it from: https://github.com/OpenCppCoverage/OpenCppCoverage/releases
Or via Chocolatey:
choco install opencppcoverage
Or via winget:
winget install OpenCppCoverage.OpenCppCoverage
"@ -ForegroundColor Red
exit 1
}
$OpenCppCoveragePath = if ($opencppcov.Source) { $opencppcov.Source } else { $opencppcov.FullName }
Write-Host "[*] Using OpenCppCoverage: $OpenCppCoveragePath" -ForegroundColor Cyan
# Create output directory
$CoverageDir = Join-Path $BinaryDir "coverage"
if (-not (Test-Path $CoverageDir)) {
New-Item -ItemType Directory -Path $CoverageDir | Out-Null
}
# Build OpenCppCoverage arguments
$coverageArgs = @(
"--sources", "$SourceDir\include",
"--sources", "$SourceDir\source",
"--excluded_sources", "*\tests\*",
"--excluded_sources", "*\googletest\*",
"--excluded_sources", "*\gtest\*",
"--excluded_sources", "*\_deps\*",
"--excluded_sources", "*\vcpkg_installed\*",
"--export_type", "html:$CoverageDir",
"--export_type", "cobertura:$CoverageDir\coverage.xml",
"--cover_children",
"--"
)
Write-Host "[*] Running OpenCppCoverage..." -ForegroundColor Cyan
Write-Host " Command: $OpenCppCoveragePath $($coverageArgs -join ' ') $TestBinary"
& $OpenCppCoveragePath @coverageArgs $TestBinary
if ($LASTEXITCODE -ne 0) {
Write-Warning "OpenCppCoverage exited with code $LASTEXITCODE (tests may have failed)"
}
# Check outputs
$htmlIndex = Join-Path $CoverageDir "index.html"
$coberturaXml = Join-Path $CoverageDir "coverage.xml"
if (Test-Path $htmlIndex) {
Write-Host "[*] HTML coverage report: $htmlIndex" -ForegroundColor Green
}
if (Test-Path $coberturaXml) {
Write-Host "[*] Cobertura XML report: $coberturaXml" -ForegroundColor Green
}
Write-Host "[*] Coverage collection complete!" -ForegroundColor Green

77
scripts/valgrind.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/bin/bash
# =============================================================================
# Local Reproduction of "Valgrind Analysis" GitHub Action
# =============================================================================
# Stop on error, undefined variables, or pipe failures
set -euo pipefail
# --- 1. Environment Setup ---
# Determine VCPKG_ROOT
# If VCPKG_ROOT is not set in your shell, we check if a local folder exists.
if [[ -z "${VCPKG_ROOT:-}" ]]; then
if [[ -d "./vcpkg" ]]; then
export VCPKG_ROOT="$(pwd)/vcpkg"
echo "Found local vcpkg at: $VCPKG_ROOT"
# Bootstrap vcpkg if the executable doesn't exist
if [[ ! -f "$VCPKG_ROOT/vcpkg" ]]; then
echo "Bootstrapping vcpkg..."
"$VCPKG_ROOT/bootstrap-vcpkg.sh"
fi
else
echo "Error: VCPKG_ROOT is not set and ./vcpkg directory not found."
echo "Please install vcpkg or set the VCPKG_ROOT environment variable."
exit 1
fi
else
echo "Using existing VCPKG_ROOT: $VCPKG_ROOT"
fi
# Set the build directory matching the YAML's preset expectation
# Assuming the preset writes to: cmake-build/build/linux-release-vcpkg
BUILD_DIR="cmake-build/build/linux-release-vcpkg"
# Check if Valgrind is installed
if ! command -v valgrind &> /dev/null; then
echo "Error: valgrind is not installed. Please install it (e.g., sudo apt install valgrind)."
exit 1
fi
echo "----------------------------------------------------"
echo "Starting Configuration (Debug Build with Valgrind)..."
echo "----------------------------------------------------"
# --- 2. Configure (CMake) ---
# We force CMAKE_BUILD_TYPE=Debug even though the preset says 'release'
# to ensure Valgrind has access to debug symbols (line numbers).
cmake --preset linux-release-vcpkg \
-DCMAKE_BUILD_TYPE=Debug \
-DOMATH_BUILD_EXAMPLES=OFF \
-DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=ON \
-DOMATH_ENABLE_VALGRIND=ON \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;benchmark"
echo "----------------------------------------------------"
echo "Building Targets..."
echo "----------------------------------------------------"
# --- 3. Build ---
# Using the specific build directory defined by the preset structure
cmake --build "$BUILD_DIR"
echo "----------------------------------------------------"
echo "Running Valgrind Analysis..."
echo "----------------------------------------------------"
# --- 4. Run Valgrind ---
# Runs the specific custom target defined in your CMakeLists.txt
cmake --build "$BUILD_DIR" --target valgrind_all
echo "----------------------------------------------------"
echo "Valgrind Analysis Complete."
echo "----------------------------------------------------"

View File

@@ -0,0 +1,325 @@
//
// Created by Vladislav on 30.12.2025.
//
#include "omath/utility/pattern_scan.hpp"
#include <array>
#include <fstream>
#include <omath/utility/elf_pattern_scan.hpp>
#include <utility>
#include <variant>
#include <vector>
#pragma pack(push, 1)
namespace
{
// Common
constexpr uint8_t ei_nident = 16;
constexpr uint8_t ei_class = 4;
constexpr uint8_t elfclass32 = 1;
constexpr uint8_t elfclass64 = 2;
// ReSharper disable CppDeclaratorNeverUsed
struct Elf32Ehdr final
{
unsigned char e_ident[ei_nident];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint32_t e_entry;
uint32_t e_phoff;
uint32_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
};
struct Elf64Ehdr final
{
unsigned char e_ident[ei_nident];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint64_t e_entry;
uint64_t e_phoff;
uint64_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
};
struct Elf32Shdr final
{
uint32_t sh_name;
uint32_t sh_type;
uint32_t sh_flags;
uint32_t sh_addr;
uint32_t sh_offset;
uint32_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint32_t sh_addralign;
uint32_t sh_entsize;
};
struct Elf64Shdr final
{
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_flags;
uint64_t sh_addr;
uint64_t sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
};
// ReSharper restore CppDeclaratorNeverUsed
#pragma pack(pop)
} // namespace
namespace
{
enum class FileArch : std::int8_t
{
x32,
x64,
};
template<FileArch arch>
struct ElfHeaders
{
using FileHeader = std::conditional_t<arch == FileArch::x64, Elf64Ehdr, Elf32Ehdr>;
using SectionHeader = std::conditional_t<arch == FileArch::x64, Elf64Shdr, Elf32Shdr>;
FileHeader file_header;
SectionHeader section_header;
};
[[nodiscard]]
bool not_elf_file(std::fstream& file)
{
constexpr std::string_view valid_elf_signature = "\x7F"
"ELF";
std::array<char, valid_elf_signature.size() + 1> elf_signature{};
const std::streampos back_up_pose = file.tellg();
file.seekg(0, std::ios_base::beg);
file.read(elf_signature.data(), 4);
file.seekg(back_up_pose, std::ios_base::beg);
return std::string_view{elf_signature.data(), 4} != valid_elf_signature;
}
[[nodiscard]]
std::optional<FileArch> get_file_arch(std::fstream& file)
{
std::array<char, ei_nident> e_ident{};
const std::streampos back_up_pose = file.tellg();
file.seekg(0, std::ios_base::beg);
file.read(e_ident.data(), e_ident.size());
file.seekg(back_up_pose, std::ios_base::beg);
if (e_ident[ei_class] == elfclass64)
return FileArch::x64;
if (e_ident[ei_class] == elfclass32)
return FileArch::x32;
return std::nullopt;
}
struct ExtractedSection final
{
std::uintptr_t virtual_base_addr{};
std::uintptr_t raw_base_addr{};
std::vector<std::byte> data;
};
[[maybe_unused]]
std::optional<ExtractedSection> get_elf_section_by_name(const std::filesystem::path& path,
const std::string_view& section_name)
{
std::fstream file(path, std::ios::binary | std::ios::in);
if (!file.is_open()) [[unlikely]]
return std::nullopt;
if (not_elf_file(file)) [[unlikely]]
return std::nullopt;
const auto architecture = get_file_arch(file);
if (!architecture.has_value()) [[unlikely]]
return std::nullopt;
std::variant<ElfHeaders<FileArch::x64>, ElfHeaders<FileArch::x32>> elf_headers;
if (architecture.value() == FileArch::x64)
elf_headers = ElfHeaders<FileArch::x64>{};
else if (architecture.value() == FileArch::x32)
elf_headers = ElfHeaders<FileArch::x32>{};
return std::visit(
[&](auto& header) -> std::optional<ExtractedSection>
{
auto& [file_header, section_header] = header;
file.seekg(0, std::ios_base::beg);
if (!file.read(reinterpret_cast<char*>(&file_header), sizeof(file_header))) [[unlikely]]
return std::nullopt;
const std::streamoff shstr_off =
static_cast<std::streamoff>(file_header.e_shoff)
+ static_cast<std::streamoff>(file_header.e_shstrndx) * sizeof(section_header);
file.seekg(shstr_off, std::ios_base::beg);
if (!file.read(reinterpret_cast<char*>(&section_header), sizeof(section_header))) [[unlikely]]
return std::nullopt;
std::vector<char> shstrtab(static_cast<std::size_t>(section_header.sh_size));
file.seekg(section_header.sh_offset, std::ios_base::beg);
if (!file.read(shstrtab.data(), static_cast<std::streamsize>(shstrtab.size()))) [[unlikely]]
return std::nullopt;
for (std::uint16_t i = 0; i < file_header.e_shnum; ++i)
{
decltype(section_header) current_section{};
const std::streamoff off = static_cast<std::streamoff>(file_header.e_shoff)
+ static_cast<std::streamoff>(i) * sizeof(current_section);
file.seekg(off, std::ios_base::beg);
if (!file.read(reinterpret_cast<char*>(&current_section), sizeof(current_section))) [[unlikely]]
return std::nullopt;
if (current_section.sh_name >= shstrtab.size()) [[unlikely]]
continue;
// ReSharper disable once CppTooWideScopeInitStatement
const std::string_view name = &shstrtab[current_section.sh_name];
if (section_name != name)
continue;
ExtractedSection out;
out.virtual_base_addr = static_cast<std::uintptr_t>(current_section.sh_addr);
out.raw_base_addr = static_cast<std::uintptr_t>(current_section.sh_offset);
out.data.resize(static_cast<std::size_t>(current_section.sh_size));
file.seekg(static_cast<std::streamoff>(out.raw_base_addr), std::ios_base::beg);
if (!file.read(reinterpret_cast<char*>(out.data.data()),
static_cast<std::streamsize>(out.data.size()))) [[unlikely]]
return std::nullopt;
return out;
}
return std::nullopt;
},
elf_headers);
}
template<class FileHeader, class SectionHeader>
std::optional<std::uintptr_t> scan_in_module_impl(const std::byte* base, const std::string_view pattern,
const std::string_view target_section_name)
{
const auto* file_header = reinterpret_cast<const FileHeader*>(base);
const auto shoff = static_cast<std::size_t>(file_header->e_shoff);
const auto shnum = static_cast<std::size_t>(file_header->e_shnum);
const auto shstrnd = static_cast<std::size_t>(file_header->e_shstrndx);
const auto shstrtab_off = shoff + shstrnd * sizeof(SectionHeader);
const auto* shstrtab_hdr = reinterpret_cast<const SectionHeader*>(base + shstrtab_off);
const auto shstrtab = reinterpret_cast<const char*>(base + static_cast<std::size_t>(shstrtab_hdr->sh_offset));
const auto shstrtab_size = static_cast<std::size_t>(shstrtab_hdr->sh_size);
for (std::size_t i = 0; i < shnum; ++i)
{
const auto section_off = shoff + i * sizeof(SectionHeader);
const auto* section = reinterpret_cast<const SectionHeader*>(base + section_off);
if (section->sh_size == 0)
continue;
if (std::cmp_greater_equal(section->sh_name, shstrtab_size))
continue;
if (std::string_view{shstrtab + section->sh_name} != target_section_name)
continue;
const auto* section_begin = base + static_cast<std::size_t>(section->sh_addr);
const auto* section_end = section_begin + static_cast<std::size_t>(section->sh_size);
const auto scan_result = omath::PatternScanner::scan_for_pattern(section_begin, section_end, pattern);
if (scan_result == section_end)
return std::nullopt;
return reinterpret_cast<std::uintptr_t>(scan_result);
}
return std::nullopt;
}
} // namespace
namespace omath
{
std::optional<std::uintptr_t>
ElfPatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
if (module_base_address == nullptr) [[unlikely]]
return std::nullopt;
const auto* base = static_cast<const std::byte*>(module_base_address);
// Validate ELF signature.
constexpr std::string_view valid_elf_signature = "\x7F"
"ELF";
if (std::string_view{reinterpret_cast<const char*>(base), valid_elf_signature.size()} != valid_elf_signature)
return std::nullopt;
// Detect architecture.
const auto ei_class_value = static_cast<uint8_t>(base[ei_class]);
const auto arch = ei_class_value == elfclass64 ? FileArch::x64
: ei_class_value == elfclass32 ? FileArch::x32
: std::optional<FileArch>{};
if (!arch.has_value()) [[unlikely]]
return std::nullopt;
if (arch == FileArch::x64)
return scan_in_module_impl<Elf64Ehdr, Elf64Shdr>(static_cast<const std::byte*>(module_base_address),
pattern, target_section_name);
if (arch == FileArch::x32)
return scan_in_module_impl<Elf32Ehdr, Elf32Shdr>(static_cast<const std::byte*>(module_base_address),
pattern, target_section_name);
std::unreachable();
}
std::optional<SectionScanResult>
ElfPatternScanner::scan_for_pattern_in_file(const std::filesystem::path& path_to_file,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto pe_section = get_elf_section_by_name(path_to_file, target_section_name);
if (!pe_section.has_value()) [[unlikely]]
return std::nullopt;
const auto scan_result =
PatternScanner::scan_for_pattern(pe_section->data.cbegin(), pe_section->data.cend(), pattern);
if (scan_result == pe_section->data.cend())
return std::nullopt;
const auto offset = std::distance(pe_section->data.begin(), scan_result);
return SectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr,
.raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset};
}
} // namespace omath

View File

@@ -239,8 +239,8 @@ namespace
struct ExtractedSection
{
std::uint64_t virtual_base_addr;
std::uint64_t raw_base_addr;
std::uintptr_t virtual_base_addr;
std::uintptr_t raw_base_addr;
std::vector<std::byte> data;
};
@@ -261,7 +261,7 @@ namespace
const auto nt_headers = get_nt_header_from_file(file, dos_header);
if (!nt_headers)
if (!nt_headers) [[unlikely]]
return std::nullopt;
if (invalid_nt_header_file(nt_headers.value())) [[unlikely]]
@@ -290,10 +290,11 @@ namespace
file.seekg(current_section.ptr_raw_data, std::ios::beg);
file.read(reinterpret_cast<char*>(section_data.data()),
static_cast<std::streamsize>(section_data.size()));
return ExtractedSection{.virtual_base_addr = current_section.virtual_address
+ concrete_headers.optional_header.image_base,
.raw_base_addr = current_section.ptr_raw_data,
.data = std::move(section_data)};
return ExtractedSection{
.virtual_base_addr = static_cast<std::uintptr_t>(
current_section.virtual_address + concrete_headers.optional_header.image_base),
.raw_base_addr = static_cast<std::uintptr_t>(current_section.ptr_raw_data),
.data = std::move(section_data)};
}
return std::nullopt;
},
@@ -304,46 +305,71 @@ namespace
namespace omath
{
std::optional<std::uintptr_t> PePatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address,
const std::string_view& pattern)
std::optional<std::uintptr_t>
PePatternScanner::scan_for_pattern_in_loaded_module(const void* module_base_address,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto base_address = reinterpret_cast<std::uintptr_t>(module_base_address);
const auto* base_bytes = static_cast<const std::byte*>(module_base_address);
if (!base_address)
return std::nullopt;
auto nt_header_variant = get_nt_header_from_loaded_module(module_base_address);
const auto* dos_header = static_cast<const DosHeader*>(module_base_address);
if (!nt_header_variant)
if (invalid_dos_header_file(*dos_header)) [[unlikely]]
return std::nullopt;
const auto nt_header_variant = get_nt_header_from_loaded_module(module_base_address);
if (!nt_header_variant) [[unlikely]]
return std::nullopt;
return std::visit(
[base_address, &pattern](const auto& nt_header) -> std::optional<std::uintptr_t>
[base_bytes, base_address, lfanew = dos_header->e_lfanew, &target_section_name,
&pattern](const auto& nt_header) -> std::optional<std::uintptr_t>
{
// Define .text segment as scan area
const auto start = nt_header.optional_header.base_of_code;
const auto scan_size = nt_header.optional_header.size_code;
constexpr std::size_t signature_size = sizeof(nt_header.signature);
const auto section_table_off = static_cast<std::size_t>(lfanew) + signature_size
+ sizeof(FileHeader) + nt_header.file_header.size_optional_header;
const auto scan_range = std::span{reinterpret_cast<std::byte*>(base_address) + start, scan_size};
const auto* section_table = reinterpret_cast<const SectionHeader*>(base_bytes + section_table_off);
// ReSharper disable once CppTooWideScopeInitStatement
const auto result = PatternScanner::scan_for_pattern(scan_range, pattern);
for (std::size_t i = 0; i < nt_header.file_header.num_sections; ++i)
{
const auto* section = section_table + i;
if (result != scan_range.end())
return reinterpret_cast<std::uintptr_t>(&*result);
if (std::string_view{section->name} != target_section_name || section->size_raw_data == 0)
continue;
const auto section_size = section->virtual_size != 0
? static_cast<std::size_t>(section->virtual_size)
: static_cast<std::size_t>(section->size_raw_data);
const auto* section_begin =
reinterpret_cast<std::byte*>(base_address + section->virtual_address);
const auto scan_range = std::span{section_begin, section_size};
const auto result =
PatternScanner::scan_for_pattern(scan_range.begin(), scan_range.end(), pattern);
if (result != scan_range.end())
return reinterpret_cast<std::uintptr_t>(&*result);
}
return std::nullopt;
},
nt_header_variant.value());
}
std::optional<PeSectionScanResult>
std::optional<SectionScanResult>
PePatternScanner::scan_for_pattern_in_file(const std::filesystem::path& path_to_file,
const std::string_view& pattern,
const std::string_view& target_section_name)
{
const auto pe_section = extract_section_from_pe_file(path_to_file, target_section_name);
if (!pe_section.has_value())
if (!pe_section.has_value()) [[unlikely]]
return std::nullopt;
const auto scan_result =
@@ -353,8 +379,8 @@ namespace omath
return std::nullopt;
const auto offset = std::distance(pe_section->data.begin(), scan_result);
return PeSectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr,
.raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset};
return SectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr,
.raw_base_addr = pe_section->raw_base_addr,
.target_offset = offset};
}
} // namespace omath

View File

@@ -14,12 +14,23 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
CXX_STANDARD 23
CXX_STANDARD_REQUIRED ON)
if (TARGET gtest) # GTest is being linked as submodule
target_link_libraries(${PROJECT_NAME} PRIVATE gtest gtest_main omath::omath)
else() # GTest is being linked as vcpkg package
find_package(GTest CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main omath::omath)
endif()
gtest_discover_tests(${PROJECT_NAME})
if(OMATH_ENABLE_COVERAGE)
include(${CMAKE_SOURCE_DIR}/cmake/Coverage.cmake)
omath_setup_coverage(${PROJECT_NAME})
endif()
if(OMATH_ENABLE_VALGRIND)
omath_setup_valgrind(${PROJECT_NAME})
endif()
# Skip test discovery for Android/iOS builds or when cross-compiling - binaries cannot run on host
if (NOT (ANDROID OR IOS OR EMSCRIPTEN))
gtest_discover_tests(${PROJECT_NAME})
endif()

View File

@@ -0,0 +1,297 @@
// Tests for engine trait headers to improve header coverage
#include <gtest/gtest.h>
#include <omath/engines/frostbite_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/frostbite_engine/traits/mesh_trait.hpp>
#include <omath/engines/frostbite_engine/traits/camera_trait.hpp>
#include <omath/engines/iw_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/iw_engine/traits/mesh_trait.hpp>
#include <omath/engines/iw_engine/traits/camera_trait.hpp>
#include <omath/engines/opengl_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/opengl_engine/traits/mesh_trait.hpp>
#include <omath/engines/opengl_engine/traits/camera_trait.hpp>
#include <omath/engines/unity_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/unity_engine/traits/mesh_trait.hpp>
#include <omath/engines/unity_engine/traits/camera_trait.hpp>
#include <omath/engines/unreal_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/unreal_engine/traits/mesh_trait.hpp>
#include <omath/engines/unreal_engine/traits/camera_trait.hpp>
#include <omath/projectile_prediction/projectile.hpp>
#include <omath/projectile_prediction/target.hpp>
#include <optional>
using namespace omath;
// Small helper to compare matrices roughly (templated to avoid concrete typedef)
template<typename MatT>
static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f)
{
for (std::size_t r = 0; r < 4; ++r)
for (std::size_t c = 0; c < 4; ++c)
EXPECT_NEAR(a.at(r, c), b.at(r, c), eps);
}
// Generic tests for PredEngineTrait behaviour across engines
TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera)
{
namespace e = omath::frostbite_engine;
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.x, 0.f, 1e-4f);
EXPECT_NEAR(pos.z, 10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
projectile_prediction::Target t;
t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f};
t.m_is_airborne = true;
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
// Also test non-airborne path (no gravity applied)
t.m_is_airborne = false;
const auto pred_ground = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground.x, 4.f, 1e-6f);
EXPECT_NEAR(pred_ground.y, 5.f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
std::optional<float> pitch = 45.f;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
// Direct angles
Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{0.f, 1.f, 1.f};
const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f);
// MeshTrait simply forwards to rotation_matrix; ensure it compiles and returns something
e::ViewAngles va;
const auto m1 = e::MeshTrait::rotation_matrix(va);
const auto m2 = e::rotation_matrix(va);
expect_matrix_near(m1, m2);
// CameraTrait look at should be callable
const auto angles = e::CameraTrait::calc_look_at_angle({0, 0, 0}, {0, 1, 1});
(void)angles;
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
}
TEST(TraitTests, IW_Pred_And_Mesh_And_Camera)
{
namespace e = omath::iw_engine;
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.x, 10.f, 1e-4f);
EXPECT_NEAR(pos.z, -9.81f * 0.5f, 1e-4f);
projectile_prediction::Target t;
t.m_origin = {0.f, 0.f, 5.f};
t.m_velocity = {0.f, 0.f, 2.f};
t.m_is_airborne = true;
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
// predicted = origin + velocity * t -> z = 5 + 2*2 = 9; then gravity applied
EXPECT_NEAR(pred.z, 9.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 4.f, 0.f}), 5.f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 3.f, 1e-6f);
std::optional<float> pitch = 45.f;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{1.f, 1.f, 1.f};
const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dist = origin.distance_to(view_to);
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin((view_to.z - origin.z) / dist)), 1e-3f);
const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
const auto delta = view_to - origin;
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(delta.y, delta.x)), 1e-3f);
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(45.f), {1920.f, 1080.f}, 0.1f, 1000.f);
const auto expected = e::calc_perspective_projection_matrix(45.f, 1920.f / 1080.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_iw = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_iw.z, 9.f, 1e-6f);
}
TEST(TraitTests, OpenGL_Pred_And_Mesh_And_Camera)
{
namespace e = omath::opengl_engine;
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.z, -10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
projectile_prediction::Target t;
t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f};
t.m_is_airborne = true;
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
std::optional<float> pitch = 45.f;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{0.f, 1.f, 1.f};
const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(-std::atan2(dir.x, -dir.z)), 1e-3f);
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_gl = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_gl.x, 4.f, 1e-6f);
}
TEST(TraitTests, Unity_Pred_And_Mesh_And_Camera)
{
namespace e = omath::unity_engine;
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.z, 10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
projectile_prediction::Target t;
t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f};
t.m_is_airborne = true;
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
std::optional<float> pitch = 45.f;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{0.f, 1.f, 1.f};
const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f);
const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f);
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_unity = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_unity.x, 4.f, 1e-6f);
}
TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
{
namespace e = omath::unreal_engine;
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.x, 10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
projectile_prediction::Target t;
t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f};
t.m_is_airborne = true;
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
std::optional<float> pitch = 45.f;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{1.f, 1.f, 1.f};
const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3f);
const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3f);
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f);
}

View File

@@ -1,8 +1,128 @@
//
// Created by Vlad on 18.08.2024.
//
// Extra unit tests for the project's A* implementation
#include <array>
#include <gtest/gtest.h>
#include <omath/pathfinding/a_star.hpp>
#include <omath/pathfinding/navigation_mesh.hpp>
#include <utility>
using namespace omath;
using namespace omath::pathfinding;
TEST(AStarExtra, TrivialNeighbor)
{
NavigationMesh nav;
Vector3<float> v1{0.f, 0.f, 0.f};
Vector3<float> v2{1.f, 0.f, 0.f};
nav.m_vertex_map[v1] = {v2};
nav.m_vertex_map[v2] = {v1};
const auto path = Astar::find_path(v1, v2, nav);
ASSERT_EQ(path.size(), 1u);
EXPECT_EQ(path.front(), v2);
}
TEST(AStarExtra, StartEqualsGoal)
{
NavigationMesh nav;
constexpr Vector3<float> v{1.f, 1.f, 0.f};
nav.m_vertex_map[v] = {};
const auto path = Astar::find_path(v, v, nav);
ASSERT_EQ(path.size(), 1u);
EXPECT_EQ(path.front(), v);
}
TEST(AStarExtra, BlockedNoPathBetweenTwoVertices)
{
NavigationMesh nav;
constexpr Vector3<float> left{0.f, 0.f, 0.f};
constexpr Vector3<float> right{2.f, 0.f, 0.f};
// both vertices present but no connections
nav.m_vertex_map[left] = {};
nav.m_vertex_map[right] = {};
const auto path = Astar::find_path(left, right, nav);
// disconnected vertices -> empty result
EXPECT_TRUE(path.empty());
}
TEST(AStarExtra, LongerPathAvoidsBlock)
{
NavigationMesh nav;
// build 3x3 grid of vertices, block center (1,1)
auto idx = [&](const int x, const int y)
{ return Vector3<float>{static_cast<float>(x), static_cast<float>(y), 0.f}; };
for (int y = 0; y < 3; ++y)
{
for (int x = 0; x < 3; ++x)
{
Vector3<float> v = idx(x, y);
if (x == 1 && y == 1)
continue; // center is omitted (blocked)
std::vector<Vector3<float>> neigh;
constexpr std::array<std::pair<int, int>, 4> offs{{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}};
for (auto [dx, dy] : offs)
{
const int nx = x + dx, ny = y + dy;
if (nx < 0 || nx >= 3 || ny < 0 || ny >= 3)
continue;
if (nx == 1 && ny == 1)
continue; // neighbor is the blocked center
neigh.push_back(idx(nx, ny));
}
nav.m_vertex_map[v] = neigh;
}
}
constexpr Vector3<float> start = idx(0, 1);
constexpr Vector3<float> goal = idx(2, 1);
const auto path = Astar::find_path(start, goal, nav);
ASSERT_FALSE(path.empty());
EXPECT_EQ(path.front(), goal); // Astar convention: single-element or endpoint present
}
TEST(AstarTests, TrivialDirectNeighborPath)
{
NavigationMesh nav;
// create two vertices directly connected
Vector3<float> v1{0.f, 0.f, 0.f};
Vector3<float> v2{1.f, 0.f, 0.f};
nav.m_vertex_map.emplace(v1, std::vector<Vector3<float>>{v2});
nav.m_vertex_map.emplace(v2, std::vector<Vector3<float>>{v1});
const auto path = Astar::find_path(v1, v2, nav);
// Current A* implementation returns the end vertex as the reconstructed
// path (single-element) in the simple neighbor scenario. Assert that the
// endpoint is present and reachable.
ASSERT_EQ(path.size(), 1u);
EXPECT_EQ(path.front(), v2);
}
TEST(AstarTests, NoPathWhenDisconnected)
{
NavigationMesh nav;
Vector3<float> v1{0.f, 0.f, 0.f};
constexpr Vector3<float> v2{10.f, 0.f, 0.f};
// nav has only v1
nav.m_vertex_map.emplace(v1, std::vector<Vector3<float>>{});
const auto path = Astar::find_path(v1, v2, nav);
// When the nav mesh contains only the start vertex, the closest
// vertex for both start and end will be the same vertex. In that
// case Astar returns a single-element path with the start vertex.
ASSERT_EQ(path.size(), 1u);
EXPECT_EQ(path.front(), v1);
}
TEST(AstarTests, EmptyNavReturnsNoPath)
{
const NavigationMesh nav;
constexpr Vector3<float> v1{0.f, 0.f, 0.f};
constexpr Vector3<float> v2{1.f, 0.f, 0.f};
const auto path = Astar::find_path(v1, v2, nav);
EXPECT_TRUE(path.empty());
}
TEST(unit_test_a_star, finding_right_path)
{

View File

@@ -13,11 +13,11 @@ namespace
{
// Handy aliases (defaults: Type=float, [0,360], Normalized)
using Deg = Angle<float, float(0), float(360), AngleFlags::Normalized>;
using Pitch = Angle<float, float(-90), float(90), AngleFlags::Clamped>;
using Turn = Angle<float, float(-180), float(180), AngleFlags::Normalized>;
using Deg = Angle<float, static_cast<float>(0), static_cast<float>(360), AngleFlags::Normalized>;
using Pitch = Angle<float, static_cast<float>(-90), static_cast<float>(90), AngleFlags::Clamped>;
using Turn = Angle<float, static_cast<float>(-180), static_cast<float>(180), AngleFlags::Normalized>;
constexpr float kEps = 1e-5f;
constexpr float k_eps = 1e-5f;
} // namespace
@@ -25,7 +25,7 @@ namespace
TEST(UnitTestAngle, DefaultConstructor_IsZeroDegrees)
{
Deg a; // default ctor
constexpr Deg a; // default ctor
EXPECT_FLOAT_EQ(*a, 0.0f);
EXPECT_FLOAT_EQ(a.as_degrees(), 0.0f);
}
@@ -44,8 +44,8 @@ TEST(UnitTestAngle, FromDegrees_Normalized_WrapsBelowMin)
TEST(UnitTestAngle, FromDegrees_Clamped_ClampsToRange)
{
const Pitch hi = Pitch::from_degrees(100.0f);
const Pitch lo = Pitch::from_degrees(-120.0f);
constexpr Pitch hi = Pitch::from_degrees(100.0f);
constexpr Pitch lo = Pitch::from_degrees(-120.0f);
EXPECT_FLOAT_EQ(hi.as_degrees(), 90.0f);
EXPECT_FLOAT_EQ(lo.as_degrees(), -90.0f);
@@ -80,8 +80,8 @@ TEST(UnitTestAngle, DereferenceReturnsDegrees)
TEST(UnitTestAngle, SinCosTanCot_BasicCases)
{
const Deg a0 = Deg::from_degrees(0.0f);
EXPECT_NEAR(a0.sin(), 0.0f, kEps);
EXPECT_NEAR(a0.cos(), 1.0f, kEps);
EXPECT_NEAR(a0.sin(), 0.0f, k_eps);
EXPECT_NEAR(a0.cos(), 1.0f, k_eps);
// cot(0) -> cos/sin -> div by 0: allow inf or nan
const float cot0 = a0.cot();
EXPECT_TRUE(std::isinf(cot0) || std::isnan(cot0));
@@ -99,7 +99,7 @@ TEST(UnitTestAngle, Atan_IsAtanOfRadians)
{
// atan(as_radians). For 0° -> atan(0)=0.
const Deg a0 = Deg::from_degrees(0.0f);
EXPECT_NEAR(a0.atan(), 0.0f, kEps);
EXPECT_NEAR(a0.atan(), 0.0f, k_eps);
const Deg a45 = Deg::from_degrees(45.0f);
// atan(pi/4) ≈ 0.665773...

View File

@@ -0,0 +1,107 @@
// Extra collision tests: Simplex, MeshCollider, EPA
#include <gtest/gtest.h>
#include <omath/collision/epa_algorithm.hpp>
#include <omath/collision/mesh_collider.hpp>
#include <omath/collision/simplex.hpp>
#include <omath/engines/source_engine/collider.hpp>
using namespace omath;
using namespace omath::collision;
TEST(SimplexTest, HandleEmptySimplex)
{
Simplex<Vector3<float>> simplex;
Vector3<float> direction{1, 0, 0};
EXPECT_EQ(simplex.size(), 0);
EXPECT_FALSE(simplex.handle(direction));
}
TEST(SimplexTest, HandleLineCollinearWithXAxis)
{
using Vec3 = Vector3<float>;
Simplex<Vec3> simplex;
simplex.push_front(Vec3{1, 0, 0});
simplex.push_front(Vec3{-1, 0, 0});
Vec3 direction{};
std::ignore = simplex.handle(direction);
EXPECT_NEAR(direction.x, 0.f, 1e-6f);
}
TEST(CollisionExtra, SimplexLineHandle)
{
Simplex<Vector3<float>> s;
s = {Vector3<float>{1.f, 0.f, 0.f}, Vector3<float>{2.f, 0.f, 0.f}};
Vector3<float> dir{0, 0, 0};
EXPECT_FALSE(s.handle(dir));
// direction should not be zero
EXPECT_GT(dir.length_sqr(), 0.0f);
}
TEST(CollisionExtra, SimplexTriangleHandle)
{
Simplex<Vector3<float>> s;
s = {Vector3<float>{1.f, 0.f, 0.f}, Vector3<float>{0.f, 1.f, 0.f}, Vector3<float>{0.f, 0.f, 1.f}};
Vector3<float> dir{0, 0, 0};
EXPECT_FALSE(s.handle(dir));
EXPECT_GT(dir.length_sqr(), 0.0f);
}
TEST(CollisionExtra, SimplexTetrahedronInside)
{
Simplex<Vector3<float>> s;
// tetra that surrounds origin roughly
s = {Vector3<float>{1.f, 0.f, 0.f}, Vector3<float>{0.f, 1.f, 0.f}, Vector3<float>{0.f, 0.f, 1.f},
Vector3<float>{-1.f, -1.f, -1.f}};
Vector3<float> dir{0, 0, 0};
// if origin inside, handle returns true
const bool inside = s.handle(dir);
EXPECT_TRUE(inside);
}
TEST(CollisionExtra, MeshColliderOriginAndFurthest)
{
source_engine::Mesh mesh = {
std::vector<primitives::Vertex<>>{{{1.f, 1.f, 1.f}, {}, {}}, {{-1.f, -1.f, -1.f}, {}, {}}}, {}};
mesh.set_origin({0, 2, 0});
source_engine::MeshCollider collider(mesh);
EXPECT_EQ(collider.get_origin(), omath::Vector3<float>(0, 2, 0));
collider.set_origin({1, 2, 3});
EXPECT_EQ(collider.get_origin(), omath::Vector3<float>(1, 2, 3));
const auto v = collider.find_abs_furthest_vertex_position({1.f, 0.f, 0.f});
// the original vertex at (1,1,1) translated by origin (1,2,3) becomes (2,3,4)
EXPECT_EQ(v, omath::Vector3<float>(2.f, 3.f, 4.f));
}
TEST(CollisionExtra, EPAConvergesOnSimpleCase)
{
// Build two simple colliders using simple meshes that overlap
source_engine::Mesh meshA = {
std::vector<primitives::Vertex<>>{{{0.f, 0.f, 0.f}, {}, {}}, {{1.f, 0.f, 0.f}, {}, {}}}, {}};
source_engine::Mesh mesh_b = meshA;
mesh_b.set_origin({0.5f, 0.f, 0.f}); // translate to overlap
source_engine::MeshCollider a(meshA);
source_engine::MeshCollider b(mesh_b);
// Create a simplex that approximately contains the origin in Minkowski space
Simplex<Vector3<float>> simplex;
simplex = {omath::Vector3<float>{0.5f, 0.f, 0.f}, omath::Vector3<float>{-0.5f, 0.f, 0.f},
omath::Vector3<float>{0.f, 0.5f, 0.f}, omath::Vector3<float>{0.f, -0.5f, 0.f}};
auto pool = std::pmr::monotonic_buffer_resource(1024);
auto res = Epa<source_engine::MeshCollider>::solve(a, b, simplex, {}, pool);
// EPA may or may not converge depending on numerics; ensure it returns optionally
// but if it does, fields should be finite
if (res.has_value())
{
auto r = *res;
EXPECT_TRUE(std::isfinite(r.depth));
EXPECT_GT(r.normal.length_sqr(), 0.0f);
}
}

View File

@@ -1,112 +0,0 @@
//
// Created by Vlad on 01.09.2024.
//
#include <omath/utility/color.hpp>
#include <gtest/gtest.h>
using namespace omath;
class unit_test_color : public ::testing::Test
{
protected:
Color color1;
Color color2;
void SetUp() override
{
color1 = Color::red();
color2 = Color::green();
}
};
// Test constructors
TEST_F(unit_test_color, Constructor_Float)
{
constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f);
EXPECT_FLOAT_EQ(color.x, 0.5f);
EXPECT_FLOAT_EQ(color.y, 0.5f);
EXPECT_FLOAT_EQ(color.z, 0.5f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
}
TEST_F(unit_test_color, Constructor_Vector4)
{
constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f);
constexpr Color color(vec);
EXPECT_FLOAT_EQ(color.x, 0.2f);
EXPECT_FLOAT_EQ(color.y, 0.4f);
EXPECT_FLOAT_EQ(color.z, 0.6f);
EXPECT_FLOAT_EQ(color.w, 0.8f);
}
// Test static methods for color creation
TEST_F(unit_test_color, FromRGBA)
{
constexpr Color color = Color::from_rgba(128, 64, 32, 255);
EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f);
EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f);
EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
}
TEST_F(unit_test_color, FromHSV)
{
constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV
EXPECT_FLOAT_EQ(color.x, 1.0f);
EXPECT_FLOAT_EQ(color.y, 0.0f);
EXPECT_FLOAT_EQ(color.z, 0.0f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
}
// Test HSV conversion
TEST_F(unit_test_color, ToHSV)
{
const auto [hue, saturation, value] = color1.to_hsv(); // Red color
EXPECT_FLOAT_EQ(hue, 0.0f);
EXPECT_FLOAT_EQ(saturation, 1.0f);
EXPECT_FLOAT_EQ(value, 1.0f);
}
// Test color blending
TEST_F(unit_test_color, Blend)
{
const Color blended = color1.blend(color2, 0.5f);
EXPECT_FLOAT_EQ(blended.x, 0.5f);
EXPECT_FLOAT_EQ(blended.y, 0.5f);
EXPECT_FLOAT_EQ(blended.z, 0.0f);
EXPECT_FLOAT_EQ(blended.w, 1.0f);
}
// Test predefined colors
TEST_F(unit_test_color, PredefinedColors)
{
constexpr Color red = Color::red();
constexpr Color green = Color::green();
constexpr Color blue = Color::blue();
EXPECT_FLOAT_EQ(red.x, 1.0f);
EXPECT_FLOAT_EQ(red.y, 0.0f);
EXPECT_FLOAT_EQ(red.z, 0.0f);
EXPECT_FLOAT_EQ(red.w, 1.0f);
EXPECT_FLOAT_EQ(green.x, 0.0f);
EXPECT_FLOAT_EQ(green.y, 1.0f);
EXPECT_FLOAT_EQ(green.z, 0.0f);
EXPECT_FLOAT_EQ(green.w, 1.0f);
EXPECT_FLOAT_EQ(blue.x, 0.0f);
EXPECT_FLOAT_EQ(blue.y, 0.0f);
EXPECT_FLOAT_EQ(blue.z, 1.0f);
EXPECT_FLOAT_EQ(blue.w, 1.0f);
}
// Test non-member function: Blend for Vector3
TEST_F(unit_test_color, BlendVector3)
{
constexpr Color v1(1.0f, 0.0f, 0.0f, 1.f); // Red
constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green
constexpr Color blended = v1.blend(v2, 0.5f);
EXPECT_FLOAT_EQ(blended.x, 0.5f);
EXPECT_FLOAT_EQ(blended.y, 0.5f);
EXPECT_FLOAT_EQ(blended.z, 0.0f);
}

View File

@@ -0,0 +1,293 @@
// Combined color tests
// This file merges multiple color-related unit test files into one grouped TU
// to make the tests look more organized.
#include <gtest/gtest.h>
#include <omath/utility/color.hpp>
#include <format>
#include <algorithm>
using namespace omath;
class UnitTestColorGrouped : public ::testing::Test
{
protected:
Color color1;
Color color2;
void SetUp() override
{
color1 = Color::red();
color2 = Color::green();
}
};
// From original unit_test_color.cpp
TEST_F(UnitTestColorGrouped, Constructor_Float)
{
constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f);
EXPECT_FLOAT_EQ(color.x, 0.5f);
EXPECT_FLOAT_EQ(color.y, 0.5f);
EXPECT_FLOAT_EQ(color.z, 0.5f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
}
TEST_F(UnitTestColorGrouped, Constructor_Vector4)
{
constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f);
constexpr Color color(vec);
EXPECT_FLOAT_EQ(color.x, 0.2f);
EXPECT_FLOAT_EQ(color.y, 0.4f);
EXPECT_FLOAT_EQ(color.z, 0.6f);
EXPECT_FLOAT_EQ(color.w, 0.8f);
}
TEST_F(UnitTestColorGrouped, FromRGBA)
{
constexpr Color color = Color::from_rgba(128, 64, 32, 255);
EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f);
EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f);
EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
}
TEST_F(UnitTestColorGrouped, FromHSV)
{
constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV
EXPECT_FLOAT_EQ(color.x, 1.0f);
EXPECT_FLOAT_EQ(color.y, 0.0f);
EXPECT_FLOAT_EQ(color.z, 0.0f);
EXPECT_FLOAT_EQ(color.w, 1.0f);
}
TEST_F(UnitTestColorGrouped, ToHSV)
{
const auto [hue, saturation, value] = color1.to_hsv(); // Red color
EXPECT_FLOAT_EQ(hue, 0.0f);
EXPECT_FLOAT_EQ(saturation, 1.0f);
EXPECT_FLOAT_EQ(value, 1.0f);
}
TEST_F(UnitTestColorGrouped, Blend)
{
const Color blended = color1.blend(color2, 0.5f);
EXPECT_FLOAT_EQ(blended.x, 0.5f);
EXPECT_FLOAT_EQ(blended.y, 0.5f);
EXPECT_FLOAT_EQ(blended.z, 0.0f);
EXPECT_FLOAT_EQ(blended.w, 1.0f);
}
TEST_F(UnitTestColorGrouped, PredefinedColors)
{
constexpr Color red = Color::red();
constexpr Color green = Color::green();
constexpr Color blue = Color::blue();
EXPECT_FLOAT_EQ(red.x, 1.0f);
EXPECT_FLOAT_EQ(red.y, 0.0f);
EXPECT_FLOAT_EQ(red.z, 0.0f);
EXPECT_FLOAT_EQ(red.w, 1.0f);
EXPECT_FLOAT_EQ(green.x, 0.0f);
EXPECT_FLOAT_EQ(green.y, 1.0f);
EXPECT_FLOAT_EQ(green.z, 0.0f);
EXPECT_FLOAT_EQ(green.w, 1.0f);
EXPECT_FLOAT_EQ(blue.x, 0.0f);
EXPECT_FLOAT_EQ(blue.y, 0.0f);
EXPECT_FLOAT_EQ(blue.z, 1.0f);
EXPECT_FLOAT_EQ(blue.w, 1.0f);
}
TEST_F(UnitTestColorGrouped, BlendVector3)
{
constexpr Color v1(1.0f, 0.0f, 0.0f, 1.f); // Red
constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green
constexpr Color blended = v1.blend(v2, 0.5f);
EXPECT_FLOAT_EQ(blended.x, 0.5f);
EXPECT_FLOAT_EQ(blended.y, 0.5f);
EXPECT_FLOAT_EQ(blended.z, 0.0f);
}
// From unit_test_color_extra.cpp
TEST(UnitTestColorGrouped_Extra, SetHueSaturationValue)
{
Color c = Color::red();
const auto h1 = c.to_hsv();
EXPECT_FLOAT_EQ(h1.hue, 0.f);
c.set_hue(0.5f);
const auto h2 = c.to_hsv();
EXPECT_NEAR(h2.hue, 0.5f, 1e-3f);
c = Color::from_hsv(0.25f, 0.8f, 0.6f);
c.set_saturation(0.3f);
const auto h3 = c.to_hsv();
EXPECT_NEAR(h3.saturation, 0.3f, 1e-3f);
c.set_value(1.0f);
const auto h4 = c.to_hsv();
EXPECT_NEAR(h4.value, 1.0f, 1e-3f);
}
TEST(UnitTestColorGrouped_Extra, ToStringVariants)
{
constexpr Color c = Color::from_rgba(10, 20, 30, 255);
auto s = c.to_string();
EXPECT_NE(s.find("r:"), std::string::npos);
const auto ws = c.to_wstring();
EXPECT_FALSE(ws.empty());
const auto u8 = c.to_u8string();
EXPECT_FALSE(u8.empty());
}
TEST(UnitTestColorGrouped_Extra, BlendEdgeCases)
{
constexpr Color a = Color::red();
constexpr Color b = Color::blue();
constexpr auto r0 = a.blend(b, 0.f);
EXPECT_FLOAT_EQ(r0.x, a.x);
constexpr auto r1 = a.blend(b, 1.f);
EXPECT_FLOAT_EQ(r1.x, b.x);
}
// From unit_test_color_more.cpp
TEST(UnitTestColorGrouped_More, DefaultCtorIsZero)
{
constexpr Color c;
EXPECT_FLOAT_EQ(c.x, 0.0f);
EXPECT_FLOAT_EQ(c.y, 0.0f);
EXPECT_FLOAT_EQ(c.z, 0.0f);
EXPECT_FLOAT_EQ(c.w, 0.0f);
}
TEST(UnitTestColorGrouped_More, FloatCtorAndClampForRGB)
{
constexpr Color c(1.2f, -0.5f, 0.5f, 2.0f);
EXPECT_FLOAT_EQ(c.x, 1.0f);
EXPECT_FLOAT_EQ(c.y, 0.0f);
EXPECT_FLOAT_EQ(c.z, 0.5f);
EXPECT_FLOAT_EQ(c.w, 2.0f);
}
TEST(UnitTestColorGrouped_More, FromRgbaProducesScaledComponents)
{
constexpr Color c = Color::from_rgba(25u, 128u, 230u, 64u);
EXPECT_NEAR(c.x, 25.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.y, 128.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.z, 230.0f/255.0f, 1e-6f);
EXPECT_NEAR(c.w, 64.0f/255.0f, 1e-6f);
}
TEST(UnitTestColorGrouped_More, BlendProducesIntermediate)
{
constexpr Color c0(0.0f, 0.0f, 0.0f, 1.0f);
constexpr Color c1(1.0f, 1.0f, 1.0f, 0.0f);
constexpr Color mid = c0.blend(c1, 0.5f);
EXPECT_FLOAT_EQ(mid.x, 0.5f);
EXPECT_FLOAT_EQ(mid.y, 0.5f);
EXPECT_FLOAT_EQ(mid.z, 0.5f);
EXPECT_FLOAT_EQ(mid.w, 0.5f);
}
TEST(UnitTestColorGrouped_More, HsvRoundTrip)
{
constexpr Color red = Color::red();
const auto hsv = red.to_hsv();
const Color back = Color::from_hsv(hsv);
EXPECT_NEAR(back.x, 1.0f, 1e-6f);
EXPECT_NEAR(back.y, 0.0f, 1e-6f);
EXPECT_NEAR(back.z, 0.0f, 1e-6f);
}
TEST(UnitTestColorGrouped_More, ToStringContainsComponents)
{
constexpr Color c = Color::from_rgba(10, 20, 30, 40);
std::string s = c.to_string();
EXPECT_NE(s.find("r:"), std::string::npos);
EXPECT_NE(s.find("g:"), std::string::npos);
EXPECT_NE(s.find("b:"), std::string::npos);
EXPECT_NE(s.find("a:"), std::string::npos);
}
// From unit_test_color_more2.cpp
TEST(UnitTestColorGrouped_More2, FromRgbaAndToString)
{
constexpr auto c = Color::from_rgba(255, 128, 0, 64);
const auto s = c.to_string();
EXPECT_NE(s.find("r:255"), std::string::npos);
EXPECT_NE(s.find("g:128"), std::string::npos);
EXPECT_NE(s.find("b:0"), std::string::npos);
EXPECT_NE(s.find("a:64"), std::string::npos);
}
TEST(UnitTestColorGrouped_More2, FromHsvCases)
{
constexpr float eps = 1e-5f;
auto check_hue = [&](float h) {
SCOPED_TRACE(::testing::Message() << "h=" << h);
Color c = Color::from_hsv(h, 1.f, 1.f);
EXPECT_TRUE(std::isfinite(c.x));
EXPECT_TRUE(std::isfinite(c.y));
EXPECT_TRUE(std::isfinite(c.z));
EXPECT_GE(c.x, -eps);
EXPECT_LE(c.x, 1.f + eps);
EXPECT_GE(c.y, -eps);
EXPECT_LE(c.y, 1.f + eps);
EXPECT_GE(c.z, -eps);
EXPECT_LE(c.z, 1.f + eps);
float mx = std::max({c.x, c.y, c.z});
float mn = std::min({c.x, c.y, c.z});
EXPECT_GE(mx, 0.999f);
EXPECT_LE(mn, 1e-3f + 1e-4f);
};
check_hue(0.f / 6.f);
check_hue(1.f / 6.f);
check_hue(2.f / 6.f);
check_hue(3.f / 6.f);
check_hue(4.f / 6.f);
check_hue(5.f / 6.f);
}
TEST(UnitTestColorGrouped_More2, ToHsvAndSetters)
{
Color c{0.2f, 0.4f, 0.6f, 1.f};
const auto hsv = c.to_hsv();
EXPECT_NEAR(hsv.value, 0.6f, 1e-6f);
c.set_hue(0.0f);
EXPECT_TRUE(std::isfinite(c.x));
c.set_saturation(0.0f);
EXPECT_TRUE(std::isfinite(c.y));
c.set_value(0.5f);
EXPECT_TRUE(std::isfinite(c.z));
}
TEST(UnitTestColorGrouped_More2, BlendAndStaticColors)
{
constexpr Color a = Color::red();
constexpr Color b = Color::blue();
constexpr auto mid = a.blend(b, 0.5f);
EXPECT_GT(mid.x, 0.f);
EXPECT_GT(mid.z, 0.f);
constexpr auto all_a = a.blend(b, -1.f);
EXPECT_NEAR(all_a.x, a.x, 1e-6f);
constexpr auto all_b = a.blend(b, 2.f);
EXPECT_NEAR(all_b.z, b.z, 1e-6f);
}
TEST(UnitTestColorGrouped_More2, FormatterUsesToString)
{
Color c = Color::from_rgba(10, 20, 30, 40);
const auto formatted = std::format("{}", c);
EXPECT_NE(formatted.find("r:10"), std::string::npos);
}

View File

@@ -0,0 +1,17 @@
//
// Created by Vladislav on 30.12.2025.
//
// /Users/vladislav/Downloads/valencia
#include <gtest/gtest.h>
#include <omath/utility/elf_pattern_scan.hpp>
#include <print>
TEST(unit_test_elf_pattern_scan_file, ScanMissingPattern)
{
//FIXME: Implement normal tests :)
//constexpr std::string_view path = "/Users/vladislav/Downloads/crackme";
//const auto res = omath::ElfPatternScanner::scan_for_pattern_in_file(path, "F3 0F 1E FA 55 48 89 E5 B8 00 00 00 00", ".text");
//EXPECT_TRUE(res.has_value());
//std::println("In virtual mem: 0x{:x}", res->virtual_base_addr+res->target_offset);
}

View File

@@ -9,7 +9,7 @@
using Mesh = omath::source_engine::Mesh;
using Collider = omath::source_engine::MeshCollider;
using GJK = omath::collision::GjkAlgorithm<Collider>;
using Gjk = omath::collision::GjkAlgorithm<Collider>;
using EPA = omath::collision::Epa<Collider>;
TEST(UnitTestEpa, TestCollisionTrue)
@@ -37,15 +37,15 @@ TEST(UnitTestEpa, TestCollisionTrue)
Collider A(a), B(b);
// GJK
auto gjk = GJK::is_collide_with_simplex_info(A, B);
ASSERT_TRUE(gjk.hit) << "GJK should report collision";
auto [hit, simplex] = Gjk::is_collide_with_simplex_info(A, B);
ASSERT_TRUE(hit) << "GJK should report collision";
// EPA
EPA::Params params;
auto pool = std::make_shared<std::pmr::monotonic_buffer_resource>(1024);
params.max_iterations = 64;
params.tolerance = 1e-4f;
auto epa = EPA::solve(A, B, gjk.simplex, params, *pool);
auto epa = EPA::solve(A, B, simplex, params, *pool);
ASSERT_TRUE(epa.has_value()) << "EPA should converge";
// Normal is unit
@@ -60,7 +60,7 @@ TEST(UnitTestEpa, TestCollisionTrue)
EXPECT_NEAR(epa->normal.z, 0.0f, 1e-3f);
// Try both signs with a tiny margin (avoid grazing contacts)
const float margin = 1.0f + 1e-3f;
constexpr float margin = 1.0f + 1e-3f;
const auto pen = epa->penetration_vector;
Mesh b_plus = b;
@@ -70,8 +70,8 @@ TEST(UnitTestEpa, TestCollisionTrue)
Collider B_plus(b_plus), B_minus(b_minus);
const bool sep_plus = !GJK::is_collide_with_simplex_info(A, B_plus).hit;
const bool sep_minus = !GJK::is_collide_with_simplex_info(A, B_minus).hit;
const bool sep_plus = !Gjk::is_collide_with_simplex_info(A, B_plus).hit;
const bool sep_minus = !Gjk::is_collide_with_simplex_info(A, B_minus).hit;
// Exactly one direction should separate
EXPECT_NE(sep_plus, sep_minus) << "Exactly one of ±penetration must separate";
@@ -81,12 +81,12 @@ TEST(UnitTestEpa, TestCollisionTrue)
Mesh b_resolved = b;
b_resolved.set_origin(b_resolved.get_origin() + resolve);
EXPECT_FALSE(GJK::is_collide(A, Collider(b_resolved))) << "Resolved position should be non-colliding";
EXPECT_FALSE(Gjk::is_collide(A, Collider(b_resolved))) << "Resolved position should be non-colliding";
// Moving the other way should still collide
Mesh b_wrong = b;
b_wrong.set_origin(b_wrong.get_origin() - resolve);
EXPECT_TRUE(GJK::is_collide(A, Collider(b_wrong)));
EXPECT_TRUE(Gjk::is_collide(A, Collider(b_wrong)));
}
TEST(UnitTestEpa, TestCollisionTrue2)
{
@@ -112,7 +112,7 @@ TEST(UnitTestEpa, TestCollisionTrue2)
Collider A(a), B(b);
// --- GJK must detect collision and provide simplex ---
auto gjk = GJK::is_collide_with_simplex_info(A, B);
auto gjk = Gjk::is_collide_with_simplex_info(A, B);
ASSERT_TRUE(gjk.hit) << "GJK should report collision for overlapping cubes";
// --- EPA penetration ---
EPA::Params params;
@@ -139,11 +139,11 @@ TEST(UnitTestEpa, TestCollisionTrue2)
// Apply once: B + pen must separate; the opposite must still collide
Mesh b_resolved = b;
b_resolved.set_origin(b_resolved.get_origin() + pen * margin);
EXPECT_FALSE(GJK::is_collide(A, Collider(b_resolved))) << "Applying penetration should separate";
EXPECT_FALSE(Gjk::is_collide(A, Collider(b_resolved))) << "Applying penetration should separate";
Mesh b_wrong = b;
b_wrong.set_origin(b_wrong.get_origin() - pen * margin);
EXPECT_TRUE(GJK::is_collide(A, Collider(b_wrong))) << "Opposite direction should still intersect";
EXPECT_TRUE(Gjk::is_collide(A, Collider(b_wrong))) << "Opposite direction should still intersect";
// Some book-keeping sanity
EXPECT_GT(epa->iterations, 0);

View File

@@ -0,0 +1,46 @@
#include "omath/collision/epa_algorithm.hpp"
#include "omath/collision/simplex.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <gtest/gtest.h>
using Vector3f = omath::Vector3<float>;
// Dummy collider type that exposes VectorType and returns small offsets
struct DummyCollider
{
using VectorType = Vector3f;
[[nodiscard]]
static VectorType find_abs_furthest_vertex_position(const VectorType& dir) noexcept
{
// map direction to a small point so support_point is finite
return Vector3f{dir.x * 0.01f, dir.y * 0.01f, dir.z * 0.01f};
}
};
using EpaDummy = omath::collision::Epa<DummyCollider>;
using Simplex = omath::collision::Simplex<Vector3f>;
TEST(EpaInternal, SolveHandlesSmallPolytope)
{
// Create a simplex that is nearly degenerate but valid for solve
Simplex s;
s = { Vector3f{0.01f, 0.f, 0.f}, Vector3f{0.f, 0.01f, 0.f}, Vector3f{0.f, 0.f, 0.01f}, Vector3f{-0.01f, -0.01f, -0.01f} };
constexpr DummyCollider a;
constexpr DummyCollider b;
EpaDummy::Params params;
params.max_iterations = 16;
params.tolerance = 1e-6f;
// Should either return a valid result or gracefully return nullopt
if (const auto result = EpaDummy::solve(a, b, s, params))
{
EXPECT_TRUE(std::isfinite(result->depth));
EXPECT_TRUE(std::isfinite(result->normal.x));
EXPECT_GT(result->num_faces, 0);
}
else
{
SUCCEED() << "Epa::solve returned nullopt for small polytope (acceptable)";
}
}

View File

@@ -0,0 +1,52 @@
#include "omath/collision/epa_algorithm.hpp"
#include "omath/collision/simplex.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <gtest/gtest.h>
using Vector3f = omath::Vector3<float>;
// Minimal collider interface matching Epa's expectations
struct DegenerateCollider
{
using VectorType = Vector3f;
// returns furthest point along dir
[[nodiscard]]
static VectorType find_abs_furthest_vertex_position(const VectorType& dir) noexcept
{
// Always return points on a small circle in XY plane so some faces become degenerate
if (dir.x > 0.5f) return {0.01f, 0.f, 0.f};
if (dir.x < -0.5f) return {-0.01f, 0.f, 0.f};
if (dir.y > 0.5f) return {0.f, 0.01f, 0.f};
if (dir.y < -0.5f) return {0.f, -0.01f, 0.f};
return {0.f, 0.f, 0.01f};
}
};
using Epa = omath::collision::Epa<DegenerateCollider>;
using Simplex = omath::collision::Simplex<Vector3f>;
TEST(EpaExtra, DegenerateFaceHandled)
{
// Prepare a simplex with near-collinear points to force degenerate face handling
Simplex s;
s = { Vector3f{0.01f, 0.f, 0.f}, Vector3f{0.02f, 0.f, 0.f}, Vector3f{0.03f, 0.f, 0.f}, Vector3f{0.0f, 0.0f, 0.01f} };
constexpr DegenerateCollider a;
constexpr DegenerateCollider b;
Epa::Params params;
params.max_iterations = 4;
params.tolerance = 1e-6f;
const auto result = Epa::solve(a, b, s, params);
// The algorithm should either return a valid result or gracefully exit (not crash)
if (result)
{
EXPECT_TRUE(std::isfinite(result->depth));
EXPECT_TRUE(std::isfinite(result->normal.x));
}
else
{
SUCCEED() << "EPA returned nullopt for degenerate input (acceptable)";
}
}

View File

@@ -19,9 +19,9 @@ namespace
// -----------------------------------------------------------------------------
// Constants & helpers
// -----------------------------------------------------------------------------
constexpr float kTol = 1e-5f;
constexpr float k_tol = 1e-5f;
bool VecEqual(const Vec3& a, const Vec3& b, float tol = kTol)
bool vec_equal(const Vec3& a, const Vec3& b, const float tol = k_tol)
{
return std::fabs(a.x - b.x) < tol &&
std::fabs(a.y - b.y) < tol &&
@@ -49,6 +49,13 @@ namespace
{
Ray ray;
bool expected_clear; // true => segment does NOT hit the triangle
friend std::ostream& operator<<(std::ostream& os, const TraceCase& tc)
{
os << "{ RayStart: (" << tc.ray.start.x << ", " << tc.ray.start.y << ", " << tc.ray.start.z << "), "
<< "RayEnd: (" << tc.ray.end.x << ", " << tc.ray.end.y << ", " << tc.ray.end.z << "), "
<< "Expected: " << (tc.expected_clear ? "True" : "False") << " }";
return os;
}
};
class CanTraceLineParam : public LineTracerFixture,
@@ -58,8 +65,8 @@ namespace
TEST_P(CanTraceLineParam, VariousRays)
{
const auto& p = GetParam();
EXPECT_EQ(LineTracer::can_trace_line(p.ray, triangle), p.expected_clear);
const auto& [ray, expected_clear] = GetParam();
EXPECT_EQ(LineTracer::can_trace_line(ray, triangle), expected_clear);
}
INSTANTIATE_TEST_SUITE_P(
@@ -85,8 +92,8 @@ namespace
constexpr Vec3 expected{0.3f, 0.3f, 0.f};
const Vec3 hit = LineTracer::get_ray_hit_point(ray, triangle);
ASSERT_FALSE(VecEqual(hit, ray.end));
EXPECT_TRUE(VecEqual(hit, expected));
ASSERT_FALSE(vec_equal(hit, ray.end));
EXPECT_TRUE(vec_equal(hit, expected));
}
// -----------------------------------------------------------------------------

View File

@@ -0,0 +1,65 @@
#include "omath/collision/line_tracer.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include <gtest/gtest.h>
using omath::Vector3;
TEST(LineTracerTests, ParallelRayReturnsEnd)
{
// Triangle in XY plane
constexpr omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
omath::collision::Ray ray;
ray.start = Vector3<float>{0.f,0.f,1.f};
ray.end = Vector3<float>{1.f,1.f,2.f}; // direction parallel to plane normal (z) -> but choose parallel to plane? make direction parallel to triangle plane
ray.end = Vector3<float>{1.f,1.f,1.f};
// For a ray parallel to the triangle plane the algorithm should return ray.end
const auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
EXPECT_TRUE(hit == ray.end);
EXPECT_TRUE(omath::collision::LineTracer::can_trace_line(ray, tri));
}
TEST(LineTracerTests, MissesTriangleReturnsEnd)
{
constexpr omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
omath::collision::Ray ray;
ray.start = Vector3<float>{2.f,2.f,-1.f};
ray.end = Vector3<float>{2.f,2.f,1.f}; // passes above the triangle area
const auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
EXPECT_TRUE(hit == ray.end);
}
TEST(LineTracerTests, HitTriangleReturnsPointInsideSegment)
{
constexpr omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {2.f,0.f,0.f}, {0.f,2.f,0.f} };
omath::collision::Ray ray;
ray.start = Vector3<float>{0.25f,0.25f,-1.f};
ray.end = Vector3<float>{0.25f,0.25f,1.f};
const auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
// Should return a point between start and end (z approximately 0)
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
// t_hit should be between 0 and 1 along the ray direction
const auto dir = ray.direction_vector();
// find t such that start + dir * t == hit (only check z comp for stability)
const float t = (hit.z - ray.start.z) / dir.z;
EXPECT_GT(t, 0.f);
EXPECT_LT(t, 1.f);
}
TEST(LineTracerTests, InfiniteLengthEarlyOut)
{
constexpr omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
omath::collision::Ray ray;
ray.start = Vector3<float>{0.25f,0.25f,0.f};
ray.end = Vector3<float>{0.25f,0.25f,1.f};
ray.infinite_length = true;
// If t_hit <= epsilon the algorithm should return ray.end when infinite_length is true.
// Using start on the triangle plane should produce t_hit <= epsilon.
const auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
EXPECT_TRUE(hit == ray.end);
}

View File

@@ -0,0 +1,47 @@
// Extra LineTracer tests
#include <gtest/gtest.h>
#include <omath/collision/line_tracer.hpp>
#include <omath/linear_algebra/vector3.hpp>
using namespace omath;
using namespace omath::collision;
TEST(LineTracerExtra, MissParallel)
{
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
constexpr Ray ray{ {0.3f,0.3f,1.f}, {0.3f,0.3f,2.f}, false }; // parallel above triangle
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerExtra, HitCenter)
{
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
constexpr Ray ray{ {0.3f,0.3f,-1.f}, {0.3f,0.3f,1.f}, false };
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
ASSERT_FALSE(hit == ray.end);
EXPECT_NEAR(hit.x, 0.3f, 1e-6f);
EXPECT_NEAR(hit.y, 0.3f, 1e-6f);
EXPECT_NEAR(hit.z, 0.f, 1e-6f);
}
TEST(LineTracerExtra, HitOnEdge)
{
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
constexpr Ray ray{ {0.0f,0.0f,1.f}, {0.0f,0.0f,0.f}, false };
// hitting exact vertex/edge may be considered miss; ensure function handles without crash
if (const auto hit = LineTracer::get_ray_hit_point(ray, tri); hit != ray.end)
{
EXPECT_NEAR(hit.x, 0.0f, 1e-6f);
EXPECT_NEAR(hit.y, 0.0f, 1e-6f);
}
}
TEST(LineTracerExtra, InfiniteRayIgnoredIfBehind)
{
constexpr Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
// Ray pointing away but infinite_length true should be ignored
constexpr Ray ray{ {0.5f,0.5f,-1.f}, {0.5f,0.5f,-2.f}, true };
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}

View File

@@ -0,0 +1,105 @@
#include "omath/collision/line_tracer.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <gtest/gtest.h>
using omath::Vector3;
using omath::collision::Ray;
using omath::collision::LineTracer;
using Triangle3 = omath::Triangle<Vector3<float>>;
TEST(LineTracerMore, ParallelRayReturnsEnd)
{
// Ray parallel to triangle plane: construct triangle in XY plane and ray along X axis
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
Ray ray; ray.start = {0.f,0.f,1.f}; ray.end = {1.f,0.f,1.f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerMore, UOutOfRangeReturnsEnd)
{
// Construct a ray that misses due to u < 0
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
Ray ray; ray.start = {-1.f,-1.f,-1.f}; ray.end = {-0.5f,-1.f,1.f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerMore, VOutOfRangeReturnsEnd)
{
// Construct ray that has v < 0
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
Ray ray; ray.start = {2.f,2.f,-1.f}; ray.end = {2.f,2.f,1.f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerMore, THitTooSmallReturnsEnd)
{
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
Ray ray; ray.start = {0.f,0.f,0.0000000001f}; ray.end = {0.f,0.f,1.f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerMore, THitGreaterThanOneReturnsEnd)
{
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
// Choose a ray and compute t_hit locally to assert consistency
Ray ray; ray.start = {0.f,0.f,-1.f}; ray.end = {0.f,0.f,-0.5f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
constexpr float k_epsilon = std::numeric_limits<float>::epsilon();
constexpr auto side_a = tri.side_a_vector();
constexpr auto side_b = tri.side_b_vector();
const auto ray_dir = ray.direction_vector();
const auto p = ray_dir.cross(side_b);
const auto det = side_a.dot(p);
if (std::abs(det) < k_epsilon)
{
EXPECT_EQ(hit, ray.end);
return;
}
const auto inv_det = 1.0f / det;
const auto tvec = ray.start - tri.m_vertex2;
const auto q = tvec.cross(side_a);
const auto t_hit = side_b.dot(q) * inv_det;
if (t_hit <= k_epsilon || t_hit > 1.0f)
EXPECT_EQ(hit, ray.end) << "t_hit=" << t_hit << " hit=" << hit.x << "," << hit.y << "," << hit.z;
else
EXPECT_NE(hit, ray.end) << "t_hit=" << t_hit << " hit=" << hit.x << "," << hit.y << "," << hit.z;
}
TEST(LineTracerMore, InfiniteLengthWithSmallTHitReturnsEnd)
{
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
constexpr Triangle3 tri2(Vector3<float>{0.f,0.f,-1e-8f}, Vector3<float>{1.f,0.f,-1e-8f}, Vector3<float>{0.f,1.f,-1e-8f});
Ray ray; ray.start = {0.f,0.f,0.f}; ray.end = {0.f,0.f,1.f}; ray.infinite_length = true;
// Create triangle slightly behind so t_hit <= eps
tri = tri2;
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerMore, SuccessfulHitReturnsPoint)
{
constexpr Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
Ray ray; ray.start = {0.1f,0.1f,-1.f}; ray.end = {0.1f,0.1f,1.f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_NE(hit, ray.end);
// Hit should be on plane z=0 and near x=0.1,y=0.1
EXPECT_NEAR(hit.z, 0.f, 1e-6f);
EXPECT_NEAR(hit.x, 0.1f, 1e-3f);
EXPECT_NEAR(hit.y, 0.1f, 1e-3f);
}

View File

@@ -0,0 +1,57 @@
#include "omath/collision/line_tracer.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <gtest/gtest.h>
using omath::Vector3;
using omath::collision::Ray;
using omath::collision::LineTracer;
using Triangle3 = omath::Triangle<Vector3<float>>;
TEST(LineTracerMore2, UGreaterThanOneReturnsEnd)
{
constexpr Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
// choose ray so barycentric u > 1
Ray ray; ray.start = {2.f, -1.f, -1.f}; ray.end = {2.f, -1.f, 1.f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerMore2, VGreaterThanOneReturnsEnd)
{
constexpr Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
// choose ray so barycentric v > 1
Ray ray; ray.start = {-1.f, 2.f, -1.f}; ray.end = {-1.f, 2.f, 1.f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerMore2, UPlusVGreaterThanOneReturnsEnd)
{
constexpr Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
// Ray aimed so u+v > 1 (outside triangle region)
Ray ray; ray.start = {1.f, 1.f, -1.f}; ray.end = {1.f, 1.f, 1.f};
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}
TEST(LineTracerMore2, DirectionVectorNormalizedProducesUnitLength)
{
Ray r; r.start = {0.f,0.f,0.f}; r.end = {0.f,3.f,4.f};
const auto dir = r.direction_vector_normalized();
const auto len = dir.length();
EXPECT_NEAR(len, 1.f, 1e-6f);
}
TEST(LineTracerMore2, ZeroLengthRayHandled)
{
constexpr Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
Ray ray; ray.start = {0.f,0.f,0.f}; ray.end = {0.f,0.f,0.f};
// Zero-length ray: direction length == 0; algorithm should handle without crash
const auto hit = LineTracer::get_ray_hit_point(ray, tri);
EXPECT_EQ(hit, ray.end);
}

View File

@@ -0,0 +1,57 @@
// Added to increase coverage for vector3/vector4/mat headers
#include <gtest/gtest.h>
#include <stdexcept>
#include <sstream>
#include "omath/linear_algebra/vector3.hpp"
#include "omath/linear_algebra/vector4.hpp"
#include "omath/linear_algebra/mat.hpp"
using namespace omath;
TEST(Vector3ScalarOps, InPlaceScalarOperators)
{
Vector3<float> v{1.f, 2.f, 3.f};
v += 1.f;
EXPECT_FLOAT_EQ(v.x, 2.f);
EXPECT_FLOAT_EQ(v.y, 3.f);
EXPECT_FLOAT_EQ(v.z, 4.f);
v /= 2.f;
EXPECT_FLOAT_EQ(v.x, 1.f);
EXPECT_FLOAT_EQ(v.y, 1.5f);
EXPECT_FLOAT_EQ(v.z, 2.f);
v -= 0.5f;
EXPECT_FLOAT_EQ(v.x, 0.5f);
EXPECT_FLOAT_EQ(v.y, 1.0f);
EXPECT_FLOAT_EQ(v.z, 1.5f);
}
TEST(Vector4BinaryOps, ElementWiseMulDiv)
{
constexpr Vector4<float> a{2.f, 4.f, 6.f, 8.f};
constexpr Vector4<float> b{1.f, 2.f, 3.f, 4.f};
constexpr auto m = a * b;
EXPECT_FLOAT_EQ(m.x, 2.f);
EXPECT_FLOAT_EQ(m.y, 8.f);
EXPECT_FLOAT_EQ(m.z, 18.f);
EXPECT_FLOAT_EQ(m.w, 32.f);
constexpr auto d = a / b;
EXPECT_FLOAT_EQ(d.x, 2.f);
EXPECT_FLOAT_EQ(d.y, 2.f);
EXPECT_FLOAT_EQ(d.z, 2.f);
EXPECT_FLOAT_EQ(d.w, 2.f);
}
TEST(MatInitExceptions, InvalidInitializerLists)
{
// Wrong number of rows
EXPECT_THROW((Mat<2,2,float>{ {1.f,2.f} }), std::invalid_argument);
// Row with wrong number of columns
EXPECT_THROW((Mat<2,2,float>{ {1.f,2.f}, {1.f} }), std::invalid_argument);
}

View File

@@ -0,0 +1,50 @@
// Additional coverage tests for Vector4 and Mat
#include <gtest/gtest.h>
#include <stdexcept>
#include "omath/linear_algebra/vector4.hpp"
#include "omath/linear_algebra/mat.hpp"
using namespace omath;
static void make_bad_mat_rows()
{
// wrong number of rows -> should throw inside initializer-list ctor
[[maybe_unused]] const Mat<2, 2, float> m{{1.f, 2.f}};
}
static void make_bad_mat_cols()
{
// row with wrong number of columns -> should throw
[[maybe_unused]] const Mat<2, 2, float> m{{1.f, 2.f}, {1.f}};
}
TEST(Vector4Operator, Subtraction)
{
constexpr Vector4<float> a{5.f, 6.f, 7.f, 8.f};
constexpr Vector4<float> b{1.f, 2.f, 3.f, 4.f};
constexpr auto r = a - b;
EXPECT_FLOAT_EQ(r.x, 4.f);
EXPECT_FLOAT_EQ(r.y, 4.f);
EXPECT_FLOAT_EQ(r.z, 4.f);
EXPECT_FLOAT_EQ(r.w, 4.f);
}
TEST(MatInitializerExceptions, ForcedThrowLines)
{
EXPECT_THROW(make_bad_mat_rows(), std::invalid_argument);
EXPECT_THROW(make_bad_mat_cols(), std::invalid_argument);
}
TEST(MatSelfAssignment, CopyAndMoveSelfAssign)
{
Mat<2,2,float> m{{1.f,2.f},{3.f,4.f}};
// self copy-assignment
m = m;
EXPECT_FLOAT_EQ(m.at(0, 0), 1.f);
// self move-assignment
m = std::move(m);
EXPECT_FLOAT_EQ(m.at(0, 0), 1.f);
}

View File

@@ -0,0 +1,64 @@
#include <gtest/gtest.h>
#include <omath/linear_algebra/vector2.hpp>
#include <omath/linear_algebra/vector3.hpp>
#include <omath/linear_algebra/vector4.hpp>
#include <omath/linear_algebra/mat.hpp>
#include <functional>
#include <format>
using namespace omath;
TEST(LinearAlgebraExtra, FormatterAndHashVector2)
{
Vector2<float> v{1.0f, 2.0f};
const std::string s = std::format("{}", v);
EXPECT_EQ(s, "[1, 2]");
const std::size_t h1 = std::hash<Vector2<float>>{}(v);
const std::size_t h2 = std::hash<Vector2<float>>{}(Vector2<float>{1.0f, 2.0f});
const std::size_t h3 = std::hash<Vector2<float>>{}(Vector2<float>{2.0f, 3.0f});
EXPECT_EQ(h1, h2);
EXPECT_NE(h1, h3);
}
TEST(LinearAlgebraExtra, FormatterAndHashVector3)
{
Vector3<float> v{1.0f, 2.0f, 3.0f};
const std::string s = std::format("{}", v);
EXPECT_EQ(s, "[1, 2, 3]");
const std::size_t h1 = std::hash<Vector3<float>>{}(v);
const std::size_t h2 = std::hash<Vector3<float>>{}(Vector3<float>{1.0f, 2.0f, 3.0f});
EXPECT_EQ(h1, h2);
// point_to_same_direction
EXPECT_TRUE((Vector3<float>{1,0,0}.point_to_same_direction(Vector3<float>{2,0,0})));
EXPECT_FALSE((Vector3<float>{1,0,0}.point_to_same_direction(Vector3<float>{-1,0,0})));
}
TEST(LinearAlgebraExtra, FormatterAndHashVector4)
{
Vector4<float> v{1.0f, 2.0f, 3.0f, 4.0f};
const std::string s = std::format("{}", v);
EXPECT_EQ(s, "[1, 2, 3, 4]");
const std::size_t h1 = std::hash<Vector4<float>>{}(v);
const std::size_t h2 = std::hash<Vector4<float>>{}(Vector4<float>{1.0f, 2.0f, 3.0f, 4.0f});
EXPECT_EQ(h1, h2);
}
TEST(LinearAlgebraExtra, MatRawArrayAndOperators)
{
Mat<2,2> m{{1.0f, 2.0f},{3.0f,4.0f}};
const auto raw = m.raw_array();
EXPECT_EQ(raw.size(), 4);
EXPECT_FLOAT_EQ(raw[0], 1.0f);
EXPECT_FLOAT_EQ(raw[3], 4.0f);
// operator[] index access
EXPECT_FLOAT_EQ(m.at(0,0), 1.0f);
EXPECT_FLOAT_EQ(m.at(1,1), 4.0f);
}

View File

@@ -0,0 +1,56 @@
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include "omath/linear_algebra/vector4.hpp"
#include <gtest/gtest.h>
// This test file exercises the non-inlined helpers added to headers
// (Vector3, Triangle, Vector4) to encourage symbol emission and
// runtime execution so coverage tools can attribute hits back to the
// header lines.
using namespace omath;
TEST(LinearAlgebraHelpers, Vector3NoInlineHelpersExecute)
{
constexpr Vector3<float> a{1.f, 2.f, 3.f};
constexpr Vector3<float> b{4.f, 5.f, 6.f};
// Execute helpers that were made non-inlined
const auto l = a.length();
const auto ang = a.angle_between(b);
const auto perp = a.is_perpendicular(b);
const auto norm = a.normalized();
(void)l; (void)ang; (void)perp; (void)norm;
SUCCEED();
}
TEST(LinearAlgebraHelpers, TriangleNoInlineHelpersExecute)
{
constexpr Vector3<float> v1{0.f,0.f,0.f};
constexpr Vector3<float> v2{3.f,0.f,0.f};
constexpr Vector3<float> v3{3.f,4.f,0.f};
constexpr Triangle<Vector3<float>> t{v1, v2, v3};
const auto n = t.calculate_normal();
const auto a = t.side_a_length();
const auto b = t.side_b_length();
const auto h = t.hypot();
const auto r = t.is_rectangular();
(void)n; (void)a; (void)b; (void)h; (void)r;
SUCCEED();
}
TEST(LinearAlgebraHelpers, Vector4NoInlineHelpersExecute)
{
Vector4<float> v{1.f,2.f,3.f,4.f};
const auto l = v.length();
const auto s = v.sum();
v.clamp(-10.f, 10.f);
(void)l; (void)s;
SUCCEED();
}

View File

@@ -0,0 +1,74 @@
// Instantiation-only tests to force out-of-line template emission
#include <gtest/gtest.h>
#include <format>
#include <functional>
#include "omath/linear_algebra/vector3.hpp"
#include "omath/linear_algebra/vector4.hpp"
#include "omath/linear_algebra/mat.hpp"
using namespace omath;
TEST(LinearAlgebraInstantiate, Vector3AndVector4AndMatCoverage) {
// Vector3 usage
Vector3<float> a{1.f, 2.f, 3.f};
Vector3<float> b{4.f, 5.f, 6.f};
// call various methods
volatile float d0 = a.distance_to_sqr(b);
volatile float d1 = a.dot(b);
volatile auto c = a.cross(b);
auto tup = a.as_tuple();
volatile bool dir = a.point_to_same_direction(b);
// non-inlined helpers
volatile float ln = a.length();
auto ang = a.angle_between(b);
volatile bool perp = a.is_perpendicular(b, 0.1f);
volatile auto anorm = a.normalized();
// formatter and hash instantiations (char only)
(void)std::format("{}", a);
(void)std::hash<Vector3<float>>{}(a);
// Vector4 usage
Vector4<float> v4{1.f, -2.f, 3.f, -4.f};
volatile float v4len = v4.length();
volatile float v4sum = v4.sum();
v4.clamp(-2.f, 2.f);
(void)std::format("{}", v4);
(void)std::hash<Vector4<float>>{}(v4);
// Mat usage: instantiate several sizes and store orders
Mat<1,1> m1{{42.f}};
volatile float m1det = m1.determinant();
Mat<2,2> m2{{{1.f,2.f},{3.f,4.f}}};
volatile float det2 = m2.determinant();
auto tr2 = m2.transposed();
auto minor00 = m2.minor(0,0);
auto algc = m2.alg_complement(0,1);
auto rarr = m2.raw_array();
auto inv2 = m2.inverted();
Mat<3,3> m3{{{1.f,2.f,3.f},{4.f,5.f,6.f},{7.f,8.f,9.f}}};
volatile float det3 = m3.determinant();
auto strip = m3.strip(0,0);
auto min = m3.minor(2,2);
// to_string/wstring/u8string and to_screen_mat
auto s = m2.to_string();
auto ws = m2.to_wstring();
auto u8s = m2.to_u8string();
auto screen = Mat<4,4>::to_screen_mat(800.f, 600.f);
// call non-inlined mat helpers
volatile auto det = m2.determinant();
volatile auto inv = m2.inverted();
volatile auto trans = m2.transposed();
volatile auto raw = m2.raw_array();
// simple sanity checks (not strict, only to use values)
EXPECT_EQ(std::get<0>(tup), 1.f);
EXPECT_TRUE(det2 != 0.f || inv2 == std::nullopt);
}

View File

@@ -0,0 +1,64 @@
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include "omath/linear_algebra/vector4.hpp"
#include <gtest/gtest.h>
using namespace omath;
TEST(LinearAlgebraMore, Vector3EdgeCases)
{
constexpr Vector3<float> zero{0.f,0.f,0.f};
constexpr Vector3<float> v{1.f,0.f,0.f};
// angle_between should be unexpected when one vector has zero length
const auto angle = zero.angle_between(v);
EXPECT_FALSE(static_cast<bool>(angle));
// normalized of zero should return zero
const auto nz = zero.normalized();
EXPECT_EQ(nz.x, 0.f);
EXPECT_EQ(nz.y, 0.f);
EXPECT_EQ(nz.z, 0.f);
// perpendicular case: x-axis and y-axis
constexpr Vector3<float> x{1.f,0.f,0.f};
constexpr Vector3<float> y{0.f,1.f,0.f};
EXPECT_TRUE(x.is_perpendicular(y));
}
TEST(LinearAlgebraMore, TriangleRectangularAndDegenerate)
{
constexpr Vector3<float> v1{0.f,0.f,0.f};
constexpr Vector3<float> v2{3.f,0.f,0.f};
constexpr Vector3<float> v3{3.f,4.f,0.f}; // 3-4-5 triangle, rectangular at v2
constexpr Triangle<Vector3<float>> t{v1,v2,v3};
EXPECT_NEAR(t.side_a_length(), 3.f, 1e-6f);
EXPECT_NEAR(t.side_b_length(), 4.f, 1e-6f);
EXPECT_NEAR(t.hypot(), 5.f, 1e-6f);
EXPECT_TRUE(t.is_rectangular());
// Degenerate: all points same
constexpr Triangle<Vector3<float>> d{v1,v1,v1};
EXPECT_NEAR(d.side_a_length(), 0.f, 1e-6f);
EXPECT_NEAR(d.side_b_length(), 0.f, 1e-6f);
EXPECT_NEAR(d.hypot(), 0.f, 1e-6f);
}
TEST(LinearAlgebraMore, Vector4ClampAndComparisons)
{
Vector4<float> v{10.f, -20.f, 30.f, -40.f};
const auto s = v.sum();
EXPECT_NEAR(s, -20.f, 1e-6f);
v.clamp(-10.f, 10.f);
EXPECT_LE(v.x, 10.f);
EXPECT_GE(v.x, -10.f);
EXPECT_LE(v.y, 10.f);
EXPECT_GE(v.y, -10.f);
constexpr Vector4<float> a{1.f,2.f,3.f,4.f};
constexpr Vector4<float> b{2.f,2.f,2.f,2.f};
EXPECT_TRUE(a < b || a > b || a == b); // just exercise comparisons
}

View File

@@ -0,0 +1,87 @@
// Tests to exercise non-inlined helpers and remaining branches in linear algebra
#include "gtest/gtest.h"
#include "omath/linear_algebra/vector3.hpp"
#include "omath/linear_algebra/vector4.hpp"
#include "omath/linear_algebra/mat.hpp"
using namespace omath;
TEST(LinearAlgebraMore2, Vector3NonInlinedHelpers)
{
Vector3<float> v{3.f, 4.f, 0.f};
EXPECT_FLOAT_EQ(v.length(), 5.0f);
auto vn = v.normalized();
EXPECT_NEAR(vn.length(), 1.0f, 1e-6f);
Vector3<float> zero{0.f,0.f,0.f};
auto ang = v.angle_between(zero);
EXPECT_FALSE(ang.has_value());
Vector3<float> a{1.f,0.f,0.f};
Vector3<float> b{0.f,1.f,0.f};
EXPECT_TRUE(a.is_perpendicular(b));
EXPECT_FALSE(a.is_perpendicular(a));
auto tup = v.as_tuple();
EXPECT_EQ(std::get<0>(tup), 3.f);
EXPECT_EQ(std::get<1>(tup), 4.f);
EXPECT_EQ(std::get<2>(tup), 0.f);
EXPECT_TRUE(a.point_to_same_direction(Vector3<float>{2.f,0.f,0.f}));
// exercise hash specialization for Vector3<float>
std::hash<Vector3<float>> hasher;
auto hv = hasher(v);
(void)hv;
}
TEST(LinearAlgebraMore2, Vector4NonInlinedHelpers)
{
Vector4<float> v{1.f,2.f,3.f,4.f};
EXPECT_FLOAT_EQ(v.length(), v.length());
EXPECT_FLOAT_EQ(v.sum(), v.sum());
// clamp noinline should modify the vector
v.clamp(0.f, 2.5f);
EXPECT_GE(v.x, 0.f);
EXPECT_LE(v.z, 2.5f);
constexpr Vector4<float> shorter{0.1f,0.1f,0.1f,0.1f};
EXPECT_TRUE(shorter < v);
EXPECT_FALSE(v < shorter);
}
TEST(LinearAlgebraMore2, MatNonInlinedAndStringHelpers)
{
Mat<2,2,float> m{{{4.f,7.f},{2.f,6.f}}};
EXPECT_FLOAT_EQ(m.determinant(), 10.0f);
auto maybe_inv = m.inverted();
EXPECT_TRUE(maybe_inv.has_value());
const auto& inv = maybe_inv.value();
// m * inv should be identity (approximately)
auto prod = m * inv;
EXPECT_NEAR(prod.at(0,0), 1.0f, 1e-5f);
EXPECT_NEAR(prod.at(1,1), 1.0f, 1e-5f);
EXPECT_NEAR(prod.at(0,1), 0.0f, 1e-5f);
// transposed and to_string variants
auto t = m.transposed();
EXPECT_EQ(t.at(0,1), m.at(1,0));
auto raw = m.raw_array();
EXPECT_EQ(raw.size(), static_cast<size_t>(4));
auto s = m.to_string();
EXPECT_NE(s.size(), 0u);
auto ws = m.to_wstring();
EXPECT_NE(ws.size(), 0u);
auto u8_s = m.to_u8string();
EXPECT_NE(u8_s.size(), 0u);
// to_screen_mat static helper
auto screen = Mat<4,4,float>::to_screen_mat(800.f, 600.f);
EXPECT_NEAR(screen.at(0,0), 800.f/2.f, 1e-6f);
}

View File

@@ -154,12 +154,12 @@ TEST_F(UnitTestMat, AssignmentOperator_Move)
// Test static methods
TEST_F(UnitTestMat, StaticMethod_ToScreenMat)
{
Mat<4, 4> screenMat = Mat<4, 4>::to_screen_mat(800.0f, 600.0f);
EXPECT_FLOAT_EQ(screenMat.at(0, 0), 400.0f);
EXPECT_FLOAT_EQ(screenMat.at(1, 1), -300.0f);
EXPECT_FLOAT_EQ(screenMat.at(3, 0), 400.0f);
EXPECT_FLOAT_EQ(screenMat.at(3, 1), 300.0f);
EXPECT_FLOAT_EQ(screenMat.at(3, 3), 1.0f);
Mat<4, 4> screen_mat = Mat<4, 4>::to_screen_mat(800.0f, 600.0f);
EXPECT_FLOAT_EQ(screen_mat.at(0, 0), 400.0f);
EXPECT_FLOAT_EQ(screen_mat.at(1, 1), -300.0f);
EXPECT_FLOAT_EQ(screen_mat.at(3, 0), 400.0f);
EXPECT_FLOAT_EQ(screen_mat.at(3, 1), 300.0f);
EXPECT_FLOAT_EQ(screen_mat.at(3, 3), 1.0f);
}
@@ -220,8 +220,8 @@ TEST(UnitTestMatStandalone, Equanity)
constexpr omath::Vector3<float> left_handed = {0, 2, 10};
constexpr omath::Vector3<float> right_handed = {0, 2, -10};
auto proj_left_handed = omath::mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1, 1000);
auto proj_right_handed = omath::mat_perspective_right_handed(90.f, 16.f / 9.f, 0.1, 1000);
const auto proj_left_handed = omath::mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1, 1000);
const auto proj_right_handed = omath::mat_perspective_right_handed(90.f, 16.f / 9.f, 0.1, 1000);
auto ndc_left_handed = proj_left_handed * omath::mat_column_from_vector(left_handed);
auto ndc_right_handed = proj_right_handed * omath::mat_column_from_vector(right_handed);
@@ -233,7 +233,7 @@ TEST(UnitTestMatStandalone, Equanity)
}
TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded)
{
auto perspective_proj = mat_perspective_left_handed(90.f, 16.f/9.f, 0.1f, 1000.f);
const auto perspective_proj = mat_perspective_left_handed(90.f, 16.f/9.f, 0.1f, 1000.f);
auto projected = perspective_proj
* mat_column_from_vector<float>({0, 0, 0.1001});

View File

@@ -0,0 +1,24 @@
// Added to exercise Mat initializer-list exception branches and determinant fallback
#include <gtest/gtest.h>
#include <omath/linear_algebra/mat.hpp>
using namespace omath;
TEST(MatCoverageExtra, InitListRowsMismatchThrows) {
// Rows mismatch: provide 3 rows for a 2x2 Mat
EXPECT_THROW((Mat<2,2>{ {1,2}, {3,4}, {5,6} }), std::invalid_argument);
}
TEST(MatCoverageExtra, InitListColumnsMismatchThrows) {
// Columns mismatch: second row has wrong number of columns
EXPECT_THROW((Mat<2,2>{ {1,2}, {3} }), std::invalid_argument);
}
TEST(MatCoverageExtra, DeterminantFallbackIsCallable) {
// Call determinant for 1x1 and 2x2 matrices to cover determinant paths
const Mat<1,1> m1{{3.14f}};
EXPECT_FLOAT_EQ(m1.determinant(), 3.14f);
const Mat<2,2> m2{{{1.0f,2.0f},{3.0f,4.0f}}};
EXPECT_FLOAT_EQ(m2.determinant(), -2.0f);
}

View File

@@ -0,0 +1,21 @@
// Unit tests to exercise Mat extra branches
#include "gtest/gtest.h"
#include "omath/linear_algebra/mat.hpp"
using omath::Mat;
TEST(MatMore, InitListAndMultiply)
{
Mat<3,3,float> m{{{1.f,2.f,3.f}, {0.f,1.f,4.f}, {5.f,6.f,0.f}}};
// multiply by scalar and check element
auto r = m * 1.f;
EXPECT_EQ(r.at(0,0), m.at(0,0));
EXPECT_EQ(r.at(1,2), m.at(1,2));
}
TEST(MatMore, Determinant)
{
const Mat<2,2,double> m{{{1.0,2.0},{2.0,4.0}}}; // singular
const double det = m.determinant();
EXPECT_DOUBLE_EQ(det, 0.0);
}

View File

@@ -0,0 +1,33 @@
#include <gtest/gtest.h>
#include "omath/pathfinding/navigation_mesh.hpp"
using namespace omath;
using namespace omath::pathfinding;
TEST(NavigationMeshTests, SerializeDeserializeRoundTrip)
{
NavigationMesh nav;
Vector3<float> a{0.f,0.f,0.f};
Vector3<float> b{1.f,0.f,0.f};
Vector3<float> c{0.f,1.f,0.f};
nav.m_vertex_map.emplace(a, std::vector<Vector3<float>>{b,c});
nav.m_vertex_map.emplace(b, std::vector<Vector3<float>>{a});
nav.m_vertex_map.emplace(c, std::vector<Vector3<float>>{a});
auto data = nav.serialize();
NavigationMesh nav2;
EXPECT_NO_THROW(nav2.deserialize(data));
// verify neighbors preserved
EXPECT_EQ(nav2.m_vertex_map.size(), nav.m_vertex_map.size());
EXPECT_EQ(nav2.get_neighbors(a).size(), 2u);
}
TEST(NavigationMeshTests, GetClosestVertexWhenEmpty)
{
const NavigationMesh nav;
constexpr Vector3<float> p{5.f,5.f,5.f};
const auto res = nav.get_closest_vertex(p);
EXPECT_FALSE(res.has_value());
}

View File

@@ -0,0 +1,31 @@
// Extra tests for PatternScanner behavior
#include <gtest/gtest.h>
#include <omath/utility/pattern_scan.hpp>
using namespace omath;
TEST(unit_test_pattern_scan_extra, IteratorScanFound)
{
std::vector<std::byte> buf = {static_cast<std::byte>(0xDE), static_cast<std::byte>(0xAD),
static_cast<std::byte>(0xBE), static_cast<std::byte>(0xEF),
static_cast<std::byte>(0x00)};
const auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "DE AD BE EF");
EXPECT_NE(it, buf.end());
EXPECT_EQ(std::distance(buf.begin(), it), 0);
}
TEST(unit_test_pattern_scan_extra, IteratorScanNotFound)
{
std::vector<std::byte> buf = {static_cast<std::byte>(0x00), static_cast<std::byte>(0x11),
static_cast<std::byte>(0x22)};
const auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "FF EE DD");
EXPECT_EQ(it, buf.end());
}
TEST(unit_test_pattern_scan_extra, ParseInvalidPattern)
{
// invalid hex token should cause the public scan to return end (no match)
std::vector<std::byte> buf = {static_cast<std::byte>(0x00), static_cast<std::byte>(0x11)};
const auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "GG HH");
EXPECT_EQ(it, buf.end());
}

View File

@@ -0,0 +1,11 @@
// Tests for PePatternScanner basic behavior
#include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp>
using namespace omath;
TEST(unit_test_pe_pattern_scan_extra, MissingFileReturnsNull)
{
const auto res = PePatternScanner::scan_for_pattern_in_file("/non/existent/file.exe", "55 8B EC");
EXPECT_FALSE(res.has_value());
}

View File

@@ -0,0 +1,114 @@
// Unit test for PePatternScanner::scan_for_pattern_in_file using a synthetic PE-like file
#include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp>
#include <fstream>
#include <vector>
#include <cstdint>
#include <cstring>
using namespace omath;
// Helper: write a trivial PE-like file with DOS header and a single section named .text
static bool write_minimal_pe_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes)
{
std::ofstream f(path, std::ios::binary);
if (!f.is_open()) return false;
// Write DOS header (e_magic = 0x5A4D, e_lfanew at offset 0x3C)
std::vector<std::uint8_t> dos(64, 0);
dos[0] = 'M'; dos[1] = 'Z';
// e_lfanew -> place NT headers right after DOS (offset 0x80)
std::uint32_t e_lfanew = 0x80;
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
f.write(reinterpret_cast<const char*>(dos.data()), dos.size());
// Pad up to e_lfanew
if (f.tellp() < static_cast<std::streampos>(e_lfanew))
{
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
f.write(pad.data(), pad.size());
}
// NT headers signature 'PE\0\0'
f.put('P'); f.put('E'); f.put('\0'); f.put('\0');
// FileHeader: machine, num_sections
std::uint16_t machine = 0x8664; // x64
std::uint16_t num_sections = 1;
std::uint32_t dummy32 = 0;
std::uint32_t dummy32b = 0;
std::uint16_t size_optional = 0xF0; // reasonable
std::uint16_t characteristics = 0;
f.write(reinterpret_cast<const char*>(&machine), sizeof(machine));
f.write(reinterpret_cast<const char*>(&num_sections), sizeof(num_sections));
f.write(reinterpret_cast<const char*>(&dummy32), sizeof(dummy32));
f.write(reinterpret_cast<const char*>(&dummy32b), sizeof(dummy32b));
std::uint32_t num_symbols = 0;
f.write(reinterpret_cast<const char*>(&num_symbols), sizeof(num_symbols));
f.write(reinterpret_cast<const char*>(&size_optional), sizeof(size_optional));
f.write(reinterpret_cast<const char*>(&characteristics), sizeof(characteristics));
// OptionalHeader (x64) minimal: magic 0x20b, image_base, size_of_code, size_of_headers
std::uint16_t magic = 0x20b;
f.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
// filler for rest of optional header up to size_optional
std::vector<std::uint8_t> opt(size_optional - sizeof(magic), 0);
// set size_code near end
// we'll set image_base and size_code fields in reasonable positions for extractor
// For simplicity, leave zeros; extractor primarily uses optional_header.image_base and size_code later,
// but we will craft a SectionHeader that points to raw data we append below.
f.write(reinterpret_cast<const char*>(opt.data()), opt.size());
// Section header (name 8 bytes, then remaining 36 bytes)
char name[8] = {'.','t','e','x','t',0,0,0};
f.write(name, 8);
// Write placeholder bytes for the rest of the section header and remember its start
constexpr std::uint32_t section_header_rest = 36u;
const std::streampos header_rest_pos = f.tellp();
std::vector<char> placeholder(section_header_rest, 0);
f.write(placeholder.data(), placeholder.size());
// Now write section raw data and remember its file offset
const std::streampos data_pos = f.tellp();
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
// Patch section header fields: virtual_size, virtual_address, size_raw_data, ptr_raw_data
const std::uint32_t virtual_size = static_cast<std::uint32_t>(section_bytes.size());
constexpr std::uint32_t virtual_address = 0x1000u;
const std::uint32_t size_raw_data = static_cast<std::uint32_t>(section_bytes.size());
const std::uint32_t ptr_raw_data = static_cast<std::uint32_t>(data_pos);
// Seek back to the header_rest_pos and write fields in order
f.seekp(header_rest_pos, std::ios::beg);
f.write(reinterpret_cast<const char*>(&virtual_size), sizeof(virtual_size));
f.write(reinterpret_cast<const char*>(&virtual_address), sizeof(virtual_address));
f.write(reinterpret_cast<const char*>(&size_raw_data), sizeof(size_raw_data));
f.write(reinterpret_cast<const char*>(&ptr_raw_data), sizeof(ptr_raw_data));
// Seek back to end for consistency
f.seekp(0, std::ios::end);
f.close();
return true;
}
TEST(unit_test_pe_pattern_scan_file, ScanFindsPattern)
{
constexpr std::string_view path = "./test_minimal_pe.bin";
const std::vector<std::uint8_t> bytes = {0x55, 0x8B, 0xEC, 0x90, 0x90}; // pattern at offset 0
ASSERT_TRUE(write_minimal_pe_file(path.data(), bytes));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
EXPECT_TRUE(res.has_value());
}
TEST(unit_test_pe_pattern_scan_file, ScanMissingPattern)
{
constexpr std::string_view path = "./test_minimal_pe_2.bin";
const std::vector<std::uint8_t> bytes = {0x00, 0x01, 0x02, 0x03};
ASSERT_TRUE(write_minimal_pe_file(path.data(), bytes));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "FF EE DD", ".text");
EXPECT_FALSE(res.has_value());
}

View File

@@ -0,0 +1,98 @@
// Tests for PePatternScanner::scan_for_pattern_in_loaded_module
#include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp>
#include <vector>
#include <cstdint>
#include <cstring>
using namespace omath;
static std::vector<std::uint8_t> make_fake_module(std::uint32_t base_of_code,
std::uint32_t size_code,
const std::vector<std::uint8_t>& code_bytes)
{
// Constants
constexpr std::uint32_t e_lfanew = 0x80;
constexpr std::uint32_t nt_sig = 0x4550; // "PE\0\0"
constexpr std::uint16_t opt_magic = 0x020B; // PE32+
constexpr std::uint16_t num_sections = 1;
constexpr std::uint16_t opt_hdr_size = 0xF0; // Standard PE32+ optional header size
constexpr std::uint32_t section_table_off = e_lfanew + 4 + 20 + opt_hdr_size; // sig(4) + FileHdr(20)
constexpr std::uint32_t section_header_size = 40;
constexpr std::uint32_t text_characteristics = 0x60000020; // code | execute | read
const std::uint32_t headers_end = section_table_off + section_header_size;
const std::uint32_t code_end = base_of_code + size_code;
const std::uint32_t total_size = std::max(headers_end, code_end) + 0x100; // leave some padding
std::vector<std::uint8_t> buf(total_size, 0);
auto w16 = [&](std::size_t off, std::uint16_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); };
auto w32 = [&](std::size_t off, std::uint32_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); };
auto w64 = [&](std::size_t off, std::uint64_t v) { std::memcpy(buf.data() + off, &v, sizeof(v)); };
// DOS header
w16(0x00, 0x5A4D); // e_magic "MZ"
w32(0x3C, e_lfanew); // e_lfanew
// NT signature
w32(e_lfanew, nt_sig);
// FileHeader (starts at e_lfanew + 4)
const std::size_t fh_off = e_lfanew + 4;
w16(fh_off + 2, num_sections); // NumberOfSections
w16(fh_off + 16, opt_hdr_size); // SizeOfOptionalHeader
// OptionalHeader PE32+ (starts at e_lfanew + 4 + 20)
const std::size_t opt_off = fh_off + 20;
w16(opt_off + 0, opt_magic); // Magic
w32(opt_off + 4, size_code); // SizeOfCode
w32(opt_off + 16, 0); // AddressOfEntryPoint (unused in test)
w32(opt_off + 20, base_of_code); // BaseOfCode
w64(opt_off + 24, 0); // ImageBase
w32(opt_off + 32, 0x1000); // SectionAlignment
w32(opt_off + 36, 0x200); // FileAlignment
w32(opt_off + 56, code_end); // SizeOfImage (simple upper bound)
w32(opt_off + 60, headers_end); // SizeOfHeaders
w32(opt_off + 108, 0); // NumberOfRvaAndSizes (0 directories)
// Section header (.text) at section_table_off
const std::size_t sh_off = section_table_off;
std::memcpy(buf.data() + sh_off + 0, ".text", 5); // Name[8]
w32(sh_off + 8, size_code); // VirtualSize
w32(sh_off + 12, base_of_code); // VirtualAddress
w32(sh_off + 16, size_code); // SizeOfRawData
w32(sh_off + 20, base_of_code); // PointerToRawData
w32(sh_off + 36, text_characteristics); // Characteristics
// Place code bytes at BaseOfCode
if (base_of_code + code_bytes.size() <= buf.size())
std::memcpy(buf.data() + base_of_code, code_bytes.data(), code_bytes.size());
return buf;
}
TEST(PePatternScanLoaded, FindsPatternAtBase)
{
const std::vector<std::uint8_t> code = {0x90, 0x01, 0x02, 0x03, 0x04};
auto buf = make_fake_module(0x200, static_cast<std::uint32_t>(code.size()), code);
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "90 01 02");
ASSERT_TRUE(res.has_value());
// address should point somewhere in our buffer; check offset
const uintptr_t addr = res.value();
const uintptr_t base = reinterpret_cast<uintptr_t>(buf.data());
EXPECT_EQ(addr - base, 0x200u);
}
TEST(PePatternScanLoaded, WildcardMatches)
{
const std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF};
constexpr std::uint32_t base_of_code = 0x300;
auto buf = make_fake_module(base_of_code, static_cast<std::uint32_t>(code.size()), code);
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE ?? BE", ".text");
ASSERT_TRUE(res.has_value());
const uintptr_t addr = res.value();
const uintptr_t base = reinterpret_cast<uintptr_t>(buf.data());
EXPECT_EQ(addr - base, base_of_code);
}

View File

@@ -0,0 +1,233 @@
// Additional tests for PePatternScanner to exercise edge cases and loaded-module scanning
#include <cstdint>
#include <cstring>
#include <fstream>
#include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp>
#include <vector>
using namespace omath;
static bool write_bytes(const std::string& path, const std::vector<std::uint8_t>& data)
{
std::ofstream f(path, std::ios::binary);
if (!f.is_open())
return false;
f.write(reinterpret_cast<const char*>(data.data()), data.size());
return true;
}
TEST(unit_test_pe_pattern_scan_more, InvalidDosHeader)
{
constexpr std::string_view path = "./test_bad_dos.bin";
std::vector<std::uint8_t> data(128, 0);
// write wrong magic
data[0] = 'N';
data[1] = 'Z';
ASSERT_TRUE(write_bytes(path.data(), data));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
EXPECT_FALSE(res.has_value());
}
TEST(unit_test_pe_pattern_scan_more, InvalidNtSignature)
{
constexpr std::string_view path = "./test_bad_nt.bin";
std::vector<std::uint8_t> data(256, 0);
// valid DOS header
data[0] = 'M';
data[1] = 'Z';
// point e_lfanew to 0x80
constexpr std::uint32_t e_lfanew = 0x80;
std::memcpy(data.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
// write garbage at e_lfanew (not 'PE\0\0')
data[e_lfanew + 0] = 'X';
data[e_lfanew + 1] = 'Y';
data[e_lfanew + 2] = 'Z';
data[e_lfanew + 3] = 'W';
ASSERT_TRUE(write_bytes(path.data(), data));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
EXPECT_FALSE(res.has_value());
}
TEST(unit_test_pe_pattern_scan_more, SectionNotFound)
{
// reuse minimal writer but with section named .data and search .text
constexpr std::string_view path = "./test_section_not_found.bin";
std::ofstream f(path.data(), std::ios::binary);
ASSERT_TRUE(f.is_open());
// DOS
std::vector<std::uint8_t> dos(64, 0);
dos[0] = 'M';
dos[1] = 'Z';
std::uint32_t e_lfanew = 0x80;
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
f.write(reinterpret_cast<char*>(dos.data()), dos.size());
// pad
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
f.write(pad.data(), pad.size());
// NT sig
f.put('P');
f.put('E');
f.put('\0');
f.put('\0');
// FileHeader minimal
std::uint16_t machine = 0x8664;
std::uint16_t num_sections = 1;
std::uint32_t z = 0;
std::uint32_t z2 = 0;
std::uint32_t numsym = 0;
std::uint16_t size_opt = 0xF0;
std::uint16_t ch = 0;
f.write(reinterpret_cast<char*>(&machine), sizeof(machine));
f.write(reinterpret_cast<char*>(&num_sections), sizeof(num_sections));
f.write(reinterpret_cast<char*>(&z), sizeof(z));
f.write(reinterpret_cast<char*>(&z2), sizeof(z2));
f.write(reinterpret_cast<char*>(&numsym), sizeof(numsym));
f.write(reinterpret_cast<char*>(&size_opt), sizeof(size_opt));
f.write(reinterpret_cast<char*>(&ch), sizeof(ch));
// Optional header magic
std::uint16_t magic = 0x20b;
f.write(reinterpret_cast<char*>(&magic), sizeof(magic));
std::vector<std::uint8_t> opt(size_opt - sizeof(magic), 0);
f.write(reinterpret_cast<char*>(opt.data()), opt.size());
// Section header named .data
char name[8] = {'.', 'd', 'a', 't', 'a', 0, 0, 0};
f.write(name, 8);
std::uint32_t vs = 4, va = 0x1000, srd = 4, prd = 0x200;
f.write(reinterpret_cast<char*>(&vs), 4);
f.write(reinterpret_cast<char*>(&va), 4);
f.write(reinterpret_cast<char*>(&srd), 4);
f.write(reinterpret_cast<char*>(&prd), 4);
std::vector<char> rest(16, 0);
f.write(rest.data(), rest.size());
// section bytes
std::vector<std::uint8_t> sec = {0x00, 0x01, 0x02, 0x03};
f.write(reinterpret_cast<char*>(sec.data()), sec.size());
f.close();
auto res = PePatternScanner::scan_for_pattern_in_file(path, "00 01", ".text");
EXPECT_FALSE(res.has_value());
}
TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds)
{
// Create an in-memory buffer that mimics loaded module layout
// Define local header structs matching those in source
struct DosHeader
{
std::uint16_t e_magic;
std::uint16_t e_cblp;
std::uint16_t e_cp;
std::uint16_t e_crlc;
std::uint16_t e_cparhdr;
std::uint16_t e_minalloc;
std::uint16_t e_maxalloc;
std::uint16_t e_ss;
std::uint16_t e_sp;
std::uint16_t e_csum;
std::uint16_t e_ip;
std::uint16_t e_cs;
std::uint16_t e_lfarlc;
std::uint16_t e_ovno;
std::uint16_t e_res[4];
std::uint16_t e_oemid;
std::uint16_t e_oeminfo;
std::uint16_t e_res2[10];
std::uint32_t e_lfanew;
};
struct FileHeader
{
std::uint16_t machine;
std::uint16_t num_sections;
std::uint32_t timedate_stamp;
std::uint32_t ptr_symbols;
std::uint32_t num_symbols;
std::uint16_t size_optional_header;
std::uint16_t characteristics;
};
struct OptionalHeaderX64
{
std::uint16_t magic;
std::uint16_t linker_version;
std::uint32_t size_code;
std::uint32_t size_init_data;
std::uint32_t size_uninit_data;
std::uint32_t entry_point;
std::uint32_t base_of_code;
std::uint64_t image_base;
std::uint32_t section_alignment;
std::uint32_t file_alignment; /* rest omitted */
std::uint32_t size_image;
std::uint32_t size_headers; /* keep space */
std::uint8_t pad[200];
};
struct SectionHeader
{
char name[8];
union
{
std::uint32_t physical_address;
std::uint32_t virtual_size;
};
std::uint32_t virtual_address;
std::uint32_t size_raw_data;
std::uint32_t ptr_raw_data;
std::uint32_t ptr_relocs;
std::uint32_t ptr_line_numbers;
std::uint32_t num_relocs;
std::uint32_t num_line_numbers;
std::uint32_t characteristics;
};
struct ImageNtHeadersX64
{
std::uint32_t signature;
FileHeader file_header;
OptionalHeaderX64 optional_header;
};
const std::vector<std::uint8_t> pattern_bytes = {0xDE, 0xAD, 0xBE, 0xEF, 0x90};
constexpr std::uint32_t base_of_code = 0x200; // will place bytes at offset 0x200
const std::uint32_t size_code = static_cast<std::uint32_t>(pattern_bytes.size());
const std::uint32_t bufsize = 0x400 + size_code;
std::vector<std::uint8_t> buf(bufsize, 0);
// DOS header
const auto dos = reinterpret_cast<DosHeader*>(buf.data());
dos->e_magic = 0x5A4D;
dos->e_lfanew = 0x80;
// NT headers
const auto nt = reinterpret_cast<ImageNtHeadersX64*>(buf.data() + dos->e_lfanew);
nt->signature = 0x4550; // 'PE\0\0'
nt->file_header.machine = 0x8664;
nt->file_header.num_sections = 1;
nt->file_header.size_optional_header = static_cast<std::uint16_t>(sizeof(OptionalHeaderX64));
nt->optional_header.magic = 0x020B; // x64
nt->optional_header.base_of_code = base_of_code;
nt->optional_header.size_code = size_code;
// Compute section table offset: e_lfanew + 4 (sig) + FileHeader + OptionalHeader
const std::size_t section_table_off =
static_cast<std::size_t>(dos->e_lfanew) + 4 + sizeof(FileHeader) + sizeof(OptionalHeaderX64);
nt->optional_header.size_headers = static_cast<std::uint32_t>(section_table_off + sizeof(SectionHeader));
// Section header (.text)
const auto sect = reinterpret_cast<SectionHeader*>(buf.data() + section_table_off);
std::memset(sect, 0, sizeof(SectionHeader));
std::memcpy(sect->name, ".text", 5);
sect->virtual_size = size_code;
sect->virtual_address = base_of_code;
sect->size_raw_data = size_code;
sect->ptr_raw_data = base_of_code;
sect->characteristics = 0x60000020; // code | execute | read
// place code at base_of_code
std::memcpy(buf.data() + base_of_code, pattern_bytes.data(), pattern_bytes.size());
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD BE EF", ".text");
EXPECT_TRUE(res.has_value());
}

View File

@@ -0,0 +1,294 @@
#include <cstdint>
#include <cstring>
#include <fstream>
#include <gtest/gtest.h>
#include <omath/utility/pe_pattern_scan.hpp>
#include <vector>
using namespace omath;
// Local minimal FileHeader used by tests when constructing raw NT headers
struct TestFileHeader
{
std::uint16_t machine;
std::uint16_t num_sections;
std::uint32_t timedate_stamp;
std::uint32_t ptr_symbols;
std::uint32_t num_symbols;
std::uint16_t size_optional_header;
std::uint16_t characteristics;
};
static bool write_bytes(const std::string& path, const std::vector<std::uint8_t>& data)
{
std::ofstream f(path, std::ios::binary);
if (!f.is_open())
return false;
f.write(reinterpret_cast<const char*>(data.data()), data.size());
return true;
}
// Helper: write a trivial PE-like file with DOS header and a single section named .text
static bool write_minimal_pe_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes)
{
std::ofstream f(path, std::ios::binary);
if (!f.is_open())
return false;
// Write DOS header (e_magic = 0x5A4D, e_lfanew at offset 0x3C)
std::vector<std::uint8_t> dos(64, 0);
dos[0] = 'M';
dos[1] = 'Z';
std::uint32_t e_lfanew = 0x80;
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
f.write(reinterpret_cast<const char*>(dos.data()), dos.size());
// Pad up to e_lfanew
if (f.tellp() < static_cast<std::streampos>(e_lfanew))
{
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
f.write(pad.data(), pad.size());
}
// NT headers signature 'PE\0\0'
f.put('P');
f.put('E');
f.put('\0');
f.put('\0');
// FileHeader minimal
std::uint16_t machine = 0x8664; // x64
std::uint16_t num_sections = 1;
std::uint32_t dummy32 = 0;
std::uint32_t dummy32b = 0;
std::uint16_t size_optional = 0xF0;
std::uint16_t characteristics = 0;
f.write(reinterpret_cast<const char*>(&machine), sizeof(machine));
f.write(reinterpret_cast<const char*>(&num_sections), sizeof(num_sections));
f.write(reinterpret_cast<const char*>(&dummy32), sizeof(dummy32));
f.write(reinterpret_cast<const char*>(&dummy32b), sizeof(dummy32b));
std::uint32_t num_symbols = 0;
f.write(reinterpret_cast<const char*>(&num_symbols), sizeof(num_symbols));
f.write(reinterpret_cast<const char*>(&size_optional), sizeof(size_optional));
f.write(reinterpret_cast<const char*>(&characteristics), sizeof(characteristics));
// OptionalHeader minimal filler
std::uint16_t magic = 0x20b;
f.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
std::vector<std::uint8_t> opt(size_optional - sizeof(magic), 0);
f.write(reinterpret_cast<const char*>(opt.data()), opt.size());
// Section header (name 8 bytes, then remaining 36 bytes)
char name[8] = {'.', 't', 'e', 'x', 't', 0, 0, 0};
f.write(name, 8);
constexpr std::uint32_t section_header_rest = 36u;
const std::streampos header_rest_pos = f.tellp();
std::vector<char> placeholder(section_header_rest, 0);
f.write(placeholder.data(), placeholder.size());
// Now write section raw data and remember its file offset
const std::streampos data_pos = f.tellp();
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
// Patch section header fields
const std::uint32_t virtual_size = static_cast<std::uint32_t>(section_bytes.size());
constexpr std::uint32_t virtual_address = 0x1000u;
const std::uint32_t size_raw_data = static_cast<std::uint32_t>(section_bytes.size());
const std::uint32_t ptr_raw_data = static_cast<std::uint32_t>(data_pos);
f.seekp(header_rest_pos, std::ios::beg);
f.write(reinterpret_cast<const char*>(&virtual_size), sizeof(virtual_size));
f.write(reinterpret_cast<const char*>(&virtual_address), sizeof(virtual_address));
f.write(reinterpret_cast<const char*>(&size_raw_data), sizeof(size_raw_data));
f.write(reinterpret_cast<const char*>(&ptr_raw_data), sizeof(ptr_raw_data));
f.seekp(0, std::ios::end);
f.close();
return true;
}
TEST(unit_test_pe_pattern_scan_more2, LoadedModuleNullBaseReturnsNull)
{
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(nullptr, "DE AD");
EXPECT_FALSE(res.has_value());
}
TEST(unit_test_pe_pattern_scan_more2, LoadedModuleInvalidOptionalHeaderReturnsNull)
{
// Construct in-memory buffer with DOS header but invalid optional header magic
std::vector<std::uint8_t> buf(0x200, 0);
struct DosHeader
{
std::uint16_t e_magic;
std::uint8_t pad[0x3A];
std::uint32_t e_lfanew;
};
const auto dos = reinterpret_cast<DosHeader*>(buf.data());
dos->e_magic = 0x5A4D;
dos->e_lfanew = 0x80;
// Place an NT header with wrong optional magic at e_lfanew
const auto nt_ptr = buf.data() + dos->e_lfanew;
// write signature
nt_ptr[0] = 'P';
nt_ptr[1] = 'E';
nt_ptr[2] = 0;
nt_ptr[3] = 0;
// craft FileHeader with size_optional_header large enough
constexpr std::uint16_t size_opt = 0xE0;
// file header starts at offset 4
std::memcpy(nt_ptr + 4 + 12, &size_opt,
sizeof(size_opt)); // size_optional_header located after 12 bytes into FileHeader
// write optional header magic to be invalid value
constexpr std::uint16_t bad_magic = 0x9999;
std::memcpy(nt_ptr + 4 + sizeof(std::uint32_t) + sizeof(std::uint16_t) + sizeof(std::uint16_t), &bad_magic,
sizeof(bad_magic));
const auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD");
EXPECT_FALSE(res.has_value());
}
TEST(unit_test_pe_pattern_scan_more2, FileX86OptionalHeaderScanFindsPattern)
{
constexpr std::string_view path = "./test_pe_x86.bin";
const std::vector<std::uint8_t> pattern = {0xDE, 0xAD, 0xBE, 0xEF};
// Use helper from this file to write a consistent minimal PE file with .text section
ASSERT_TRUE(write_minimal_pe_file(path.data(), pattern));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "DE AD BE EF", ".text");
ASSERT_TRUE(res.has_value());
EXPECT_GE(res->virtual_base_addr, 0u);
EXPECT_GE(res->raw_base_addr, 0u);
EXPECT_EQ(res->target_offset, 0);
}
TEST(unit_test_pe_pattern_scan_more2, FilePatternNotFoundReturnsNull)
{
const std::string path = "./test_pe_no_pattern.bin";
std::vector<std::uint8_t> data(512, 0);
// minimal DOS/NT headers to make extract_section fail earlier or return empty data
data[0] = 'M';
data[1] = 'Z';
constexpr std::uint32_t e_lfanew = 0x80;
std::memcpy(data.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
// NT signature
data[e_lfanew + 0] = 'P';
data[e_lfanew + 1] = 'E';
data[e_lfanew + 2] = 0;
data[e_lfanew + 3] = 0;
// FileHeader: one section, size_optional_header set low
constexpr std::uint16_t num_sections = 1;
constexpr std::uint16_t size_optional_header = 0xE0;
std::memcpy(data.data() + e_lfanew + 6, &num_sections, sizeof(num_sections));
std::memcpy(data.data() + e_lfanew + 4 + 12, &size_optional_header, sizeof(size_optional_header));
// Optional header magic x64
constexpr std::uint16_t magic = 0x020B;
std::memcpy(data.data() + e_lfanew + 4 + sizeof(TestFileHeader), &magic, sizeof(magic));
// Section header .text with small data that does not contain the pattern
constexpr std::size_t offset_to_segment_table = e_lfanew + 4 + sizeof(TestFileHeader) + size_optional_header;
constexpr char name[8] = {'.', 't', 'e', 'x', 't', 0, 0, 0};
std::memcpy(data.data() + offset_to_segment_table, name, 8);
std::uint32_t vs = 4, va = 0x1000, srd = 4, prd = 0x200;
std::memcpy(data.data() + offset_to_segment_table + 8, &vs, 4);
std::memcpy(data.data() + offset_to_segment_table + 12, &va, 4);
std::memcpy(data.data() + offset_to_segment_table + 16, &srd, 4);
std::memcpy(data.data() + offset_to_segment_table + 20, &prd, 4);
// write file
ASSERT_TRUE(write_bytes(path, data));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "AA BB CC", ".text");
EXPECT_FALSE(res.has_value());
}
// Extra tests for pe_pattern_scan edge cases (on-disk API)
TEST(PePatternScanMore2, PatternAtStartFound)
{
const std::string path = "./test_pe_more_start.bin";
const std::vector<std::uint8_t> bytes = {0x90, 0x01, 0x02, 0x03, 0x04};
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "90 01 02", ".text");
EXPECT_TRUE(res.has_value());
}
TEST(PePatternScanMore2, PatternAtEndFound)
{
const std::string path = "./test_pe_more_end.bin";
std::vector<std::uint8_t> bytes = {0x00, 0x11, 0x22, 0x33, 0x44};
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "22 33 44", ".text");
if (!res.has_value())
{
// Try to locate the section header and print the raw section bytes the scanner would read
std::ifstream in(path, std::ios::binary);
ASSERT_TRUE(in.is_open());
// search for ".text" name
in.seekg(0, std::ios::beg);
std::vector<char> filebuf((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
const auto it = std::search(filebuf.begin(), filebuf.end(), std::begin(".text"), std::end(".text") - 1);
if (it != filebuf.end())
{
const size_t pos = std::distance(filebuf.begin(), it);
// after name, next fields: virtual_size (4), virtual_address(4), size_raw_data(4), ptr_raw_data(4)
const size_t meta_off = pos + 8;
uint32_t virtual_size{};
uint32_t virtual_address{};
uint32_t size_raw_data{};
uint32_t ptr_raw_data{};
std::memcpy(&virtual_size, filebuf.data() + meta_off, sizeof(virtual_size));
std::memcpy(&virtual_address, filebuf.data() + meta_off + 4, sizeof(virtual_address));
std::memcpy(&size_raw_data, filebuf.data() + meta_off + 8, sizeof(size_raw_data));
std::memcpy(&ptr_raw_data, filebuf.data() + meta_off + 12, sizeof(ptr_raw_data));
std::cerr << "Parsed section header: virtual_size=" << virtual_size << " virtual_address=0x" << std::hex
<< virtual_address << std::dec << " size_raw_data=" << size_raw_data
<< " ptr_raw_data=" << ptr_raw_data << "\n";
if (ptr_raw_data + size_raw_data <= filebuf.size())
{
std::cerr << "Extracted section bytes:\n";
for (size_t i = 0; i < size_raw_data; i += 16)
{
std::fprintf(stderr, "%04zx: ", i);
for (size_t j = 0; j < 16 && i + j < size_raw_data; ++j)
std::fprintf(stderr, "%02x ", static_cast<uint8_t>(filebuf[ptr_raw_data + i + j]));
std::fprintf(stderr, "\n");
}
}
}
}
EXPECT_TRUE(res.has_value());
}
TEST(PePatternScanMore2, WildcardMatches)
{
const std::string path = "./test_pe_more_wild.bin";
const std::vector<std::uint8_t> bytes = {0xDE, 0xAD, 0xBE, 0xEF};
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "DE ?? BE", ".text");
EXPECT_TRUE(res.has_value());
}
TEST(PePatternScanMore2, PatternLongerThanBuffer)
{
const std::string path = "./test_pe_more_small.bin";
const std::vector<std::uint8_t> bytes = {0xAA, 0xBB};
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "AA BB CC", ".text");
EXPECT_FALSE(res.has_value());
}
TEST(PePatternScanMore2, InvalidPatternParse)
{
const std::string path = "./test_pe_more_invalid.bin";
const std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03};
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "01 GG 03", ".text");
EXPECT_FALSE(res.has_value());
}

View File

@@ -0,0 +1,66 @@
// Tests for PredEngineTrait
#include <gtest/gtest.h>
#include <omath/engines/source_engine/traits/pred_engine_trait.hpp>
#include <omath/projectile_prediction/projectile.hpp>
#include <omath/projectile_prediction/target.hpp>
using namespace omath;
using namespace omath::source_engine;
TEST(PredEngineTrait, PredictProjectilePositionBasic)
{
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
const auto pos = PredEngineTrait::predict_projectile_position(p, /*pitch*/ 0.f, /*yaw*/ 0.f, /*time*/ 1.f,
/*gravity*/ 9.81f);
// With zero pitch and yaw forward vector is along X; expect x ~10, z reduced by gravity*0.5
EXPECT_NEAR(pos.x, 10.f, 1e-3f);
EXPECT_NEAR(pos.z, -9.81f * 0.5f, 1e-3f);
}
TEST(PredEngineTrait, PredictTargetPositionAirborne)
{
projectile_prediction::Target t;
t.m_origin = {0.f, 0.f, 10.f};
t.m_velocity = {1.f, 0.f, 0.f};
t.m_is_airborne = true;
const auto pred = PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 2.f, 1e-6f);
// z should have been reduced by gravity* t^2
EXPECT_NEAR(pred.z, 10.f - 9.81f * 4.f * 0.5f, 1e-6f);
}
TEST(PredEngineTrait, CalcVector2dDistance)
{
constexpr Vector3<float> d{3.f, 4.f, 0.f};
EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance(d), 5.f, 1e-6f);
}
TEST(PredEngineTrait, CalcViewpointFromAngles)
{
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
constexpr Vector3<float> predicted{10.f, 0.f, 0.f};
constexpr std::optional<float> pitch = 45.f;
const auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, predicted, pitch);
// For 45 degrees, height = delta2d * tan(45deg) = 10 * 1 = 10
EXPECT_NEAR(vp.z, 10.f, 1e-6f);
}
TEST(PredEngineTrait, DirectAngles)
{
constexpr Vector3<float> origin{0.f, 0.f, 0.f};
constexpr Vector3<float> target{0.f, 1.f, 1.f};
// yaw should be 90 degrees (pointing along y)
EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle(origin, target), 90.f, 1e-3f);
// pitch should be asin(z/distance)
const float dist = origin.distance_to(target);
EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle(origin, target),
angles::radians_to_degrees(std::asin((target.z - origin.z) / dist)), 1e-3f);
}

View File

@@ -0,0 +1,100 @@
#include <gtest/gtest.h>
#include <omath/projectile_prediction/proj_pred_engine_legacy.hpp>
#include <omath/projectile_prediction/projectile.hpp>
#include <omath/projectile_prediction/target.hpp>
#include <omath/linear_algebra/vector3.hpp>
using omath::projectile_prediction::Projectile;
using omath::projectile_prediction::Target;
using omath::Vector3;
// Fake engine trait where gravity is effectively zero and projectile prediction always hits the target
struct FakeEngineZeroGravity
{
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept
{
return t.m_origin;
}
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept
{
// Return a fixed point matching typical target used in the test
return Vector3<float>{100.f, 0.f, 0.f};
}
static float calc_vector_2d_distance(const Vector3<float>& v) noexcept { return std::hypot(v.x, v.y); }
static float get_vector_height_coordinate(const Vector3<float>& v) noexcept { return v.z; }
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept
{
return Vector3<float>{1.f, 2.f, 3.f};
}
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 12.5f; }
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
};
TEST(ProjPredLegacyMore, ZeroGravityUsesDirectPitchAndReturnsViewpoint)
{
constexpr Projectile proj{ .m_origin = {0.f, 0.f, 0.f}, .m_launch_speed = 10.f, .m_gravity_scale = 0.f };
constexpr Target target{ .m_origin = {100.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineZeroGravity>;
const Engine engine(9.8f, 0.1f, 5.f, 1e-3f);
const auto res = engine.maybe_calculate_aim_point(proj, target);
ASSERT_TRUE(res.has_value());
const auto v = res.value();
EXPECT_NEAR(v.x, 1.f, 1e-6f);
EXPECT_NEAR(v.y, 2.f, 1e-6f);
EXPECT_NEAR(v.z, 3.f, 1e-6f);
}
// Fake trait producing no valid launch angle (root < 0)
struct FakeEngineNoSolution
{
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept { return t.m_origin; }
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept { return Vector3<float>{0.f,0.f,0.f}; }
static float calc_vector_2d_distance(const Vector3<float>& /*v*/) noexcept { return 10000.f; }
static float get_vector_height_coordinate(const Vector3<float>& /*v*/) noexcept { return 0.f; }
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept { return Vector3<float>{}; }
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
};
TEST(ProjPredLegacyMore, NoSolutionRootReturnsNullopt)
{
// Very slow projectile and large distance -> quadratic root negative
constexpr Projectile proj{ .m_origin = {0.f,0.f,0.f}, .m_launch_speed = 1.f, .m_gravity_scale = 1.f };
constexpr Target target{ .m_origin = {10000.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineNoSolution>;
const Engine engine(9.8f, 0.5f, 2.f, 1.f);
const auto res = engine.maybe_calculate_aim_point(proj, target);
EXPECT_FALSE(res.has_value());
}
// Fake trait where an angle exists but the projectile does not reach target (miss)
struct FakeEngineAngleButMiss
{
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept { return t.m_origin; }
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept
{
// always return a point far from the target
return Vector3<float>{0.f, 0.f, 1000.f};
}
static float calc_vector_2d_distance(const Vector3<float>& v) noexcept { return std::hypot(v.x, v.y); }
static float get_vector_height_coordinate(const Vector3<float>& v) noexcept { return v.z; }
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept { return Vector3<float>{9.f,9.f,9.f}; }
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 1.f; }
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
};
TEST(ProjPredLegacyMore, AngleComputedButMissReturnsNullopt)
{
constexpr Projectile proj{ .m_origin = {0.f,0.f,0.f}, .m_launch_speed = 100.f, .m_gravity_scale = 1.f };
constexpr Target target{ .m_origin = {10.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineAngleButMiss>;
const Engine engine(9.8f, 0.1f, 1.f, 0.1f);
const auto res = engine.maybe_calculate_aim_point(proj, target);
EXPECT_FALSE(res.has_value());
}

View File

@@ -8,39 +8,51 @@
class Player final
{
public:
virtual int foo() {return 1;}
virtual int bar() {return 2;}
[[nodiscard]] virtual int foo() const
{
return 1;
}
[[nodiscard]] virtual int bar() const
{
return 2;
}
omath::Vector3<float> m_origin{1.f, 2.f, 3.f};
int m_health{123};
};
class RevPlayer : omath::rev_eng::InternalReverseEngineeredObject
class RevPlayer final : omath::rev_eng::InternalReverseEngineeredObject
{
public:
omath::Vector3<float> get_origin()
[[nodiscard]]
omath::Vector3<float> get_origin() const
{
return get_by_offset<omath::Vector3<float>>(sizeof(std::uintptr_t));
}
int get_health()
[[nodiscard]]
int get_health() const
{
return get_by_offset<int>(sizeof(std::uintptr_t)+sizeof(omath::Vector3<float>));
return get_by_offset<int>(sizeof(std::uintptr_t) + sizeof(omath::Vector3<float>));
}
int rev_foo()
[[nodiscard]]
int rev_foo() const
{
return call_virtual_method<0, int>();
}
int rev_bar()
[[nodiscard]]
int rev_bar() const
{
return call_virtual_method<1, int>();
}
[[nodiscard]] int rev_bar_const() const
{
return call_virtual_method<1, int>();
}
};
TEST(unit_test_reverse_enineering, read_test)
{
Player player_original;

View File

@@ -0,0 +1,54 @@
#include "omath/collision/simplex.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <gtest/gtest.h>
using omath::Vector3;
TEST(SimplexAdditional, RegionACSelectsAC)
{
// Construct points that force the Region AC branch where ac points toward the origin
Vector3<float> a{1.f, 0.f, 0.f};
Vector3<float> b{2.f, 0.f, 0.f};
Vector3<float> c{0.f, 1.f, 0.f};
omath::collision::Simplex<omath::Vector3<float>> s;
s = { a, b, c };
omath::Vector3<float> dir{0.f, 0.f, 0.f};
const bool hit = s.handle(dir);
// Should not report a collision; simplex should reduce to {a, c}
EXPECT_FALSE(hit);
EXPECT_EQ(s.size(), 2u);
EXPECT_TRUE(s[0] == a);
EXPECT_TRUE(s[1] == c);
// direction should be finite and non-zero
EXPECT_TRUE(std::isfinite(dir.x));
EXPECT_TRUE(std::isfinite(dir.y));
EXPECT_TRUE(std::isfinite(dir.z));
}
TEST(SimplexAdditional, AbcAboveSetsDirection)
{
// Choose triangle so abc points roughly toward the origin (abc · ao > 0)
Vector3<float> a{-1.f, 0.f, 0.f};
Vector3<float> b{0.f, 1.f, 0.f};
Vector3<float> c{0.f, 0.f, 1.f};
omath::collision::Simplex<omath::Vector3<float>> s;
s = { a, b, c };
omath::Vector3<float> dir{0.f, 0.f, 0.f};
const bool hit = s.handle(dir);
EXPECT_FALSE(hit);
const auto ab = b - a;
const auto ac = c - a;
const auto abc = ab.cross(ac);
// direction should equal abc (above triangle case)
EXPECT_NEAR(dir.x, abc.x, 1e-6f);
EXPECT_NEAR(dir.y, abc.y, 1e-6f);
EXPECT_NEAR(dir.z, abc.z, 1e-6f);
}

View File

@@ -0,0 +1,174 @@
#include "omath/collision/simplex.hpp"
#include "omath/linear_algebra/vector3.hpp"
#include <gtest/gtest.h>
using omath::Vector3;
using Simplex = omath::collision::Simplex<Vector3<float>>;
TEST(SimplexExtra, HandleLine_CollinearProducesPerp)
{
// a and b placed so ab points roughly same dir as ao and are collinear
Vector3<float> a{2.f, 0.f, 0.f};
Vector3<float> b{1.f, 0.f, 0.f};
Simplex s;
s = {a, b};
Vector3<float> dir{0.f, 0.f, 0.f};
const bool hit = s.handle(dir);
// Should not report collision for a line simplex
EXPECT_FALSE(hit);
// Direction must be finite and not zero
EXPECT_TRUE(std::isfinite(dir.x));
EXPECT_TRUE(std::isfinite(dir.y));
EXPECT_TRUE(std::isfinite(dir.z));
constexpr auto zero = Vector3<float>{0.f, 0.f, 0.f};
EXPECT_FALSE(dir == zero);
// Ensure direction is (approximately) perpendicular to ab
const auto ab = b - a;
const float dot = dir.dot(ab);
EXPECT_NEAR(dot, 0.0f, 1e-4f);
}
TEST(SimplexExtra, HandleLine_NonCollinearProducesValidDirection)
{
Vector3<float> a{2.f, 0.f, 0.f};
Vector3<float> b{1.f, 1.f, 0.f};
Simplex s;
s = {a, b};
Vector3<float> dir{0.f, 0.f, 0.f};
const bool hit = s.handle(dir);
EXPECT_FALSE(hit);
EXPECT_TRUE(std::isfinite(dir.x));
EXPECT_TRUE(std::isfinite(dir.y));
EXPECT_TRUE(std::isfinite(dir.z));
}
TEST(SimplexExtra, HandleTriangle_FlipWinding)
{
// Construct points where triangle winding will be flipped
Vector3<float> a{1.f, 0.f, 0.f};
Vector3<float> b{0.f, 1.f, 0.f};
Vector3<float> c{0.f, -1.f, 0.f};
Simplex s;
s = {a, b, c};
Vector3<float> dir{0.f, 0.f, 0.f};
const bool hit = s.handle(dir);
EXPECT_FALSE(hit);
EXPECT_TRUE(std::isfinite(dir.x));
EXPECT_TRUE(std::isfinite(dir.y));
EXPECT_TRUE(std::isfinite(dir.z));
}
TEST(SimplexExtra, HandleTetrahedron_InsideReturnsTrue)
{
// Simple tetra that should contain the origin
Vector3<float> a{1.f, 0.f, 0.f};
Vector3<float> b{0.f, 1.f, 0.f};
Vector3<float> c{0.f, 0.f, 1.f};
Vector3<float> d{-0.2f, -0.2f, -0.2f};
Simplex s;
s = {a, b, c, d};
Vector3<float> dir{0.f, 0.f, 0.f};
const bool hit = s.handle(dir);
// If origin is inside, handle_tetrahedron should return true
EXPECT_TRUE(hit);
}
// Additional sanity tests (avoid reusing Simplex alias above to prevent ambiguity)
TEST(SimplexMore, PushFrontAndAccess)
{
omath::collision::Simplex<omath::Vector3<float>> s;
s.push_front(omath::Vector3<float>{1.f, 0.f, 0.f});
s.push_front(omath::Vector3<float>{2.f, 0.f, 0.f});
s.push_front(omath::Vector3<float>{3.f, 0.f, 0.f});
EXPECT_EQ(s.size(), 3u);
constexpr omath::Vector3<float> exp_front{3.f, 0.f, 0.f};
constexpr omath::Vector3<float> exp_back{1.f, 0.f, 0.f};
EXPECT_TRUE(s.front() == exp_front);
EXPECT_TRUE(s.back() == exp_back);
const auto d = s.data();
EXPECT_TRUE(d[0] == exp_front);
}
TEST(SimplexMore, ClearAndEmpty)
{
omath::collision::Simplex<omath::Vector3<float>> s;
s.push_front(omath::Vector3<float>{1.f, 1.f, 1.f});
EXPECT_FALSE(s.empty());
s.clear();
EXPECT_TRUE(s.empty());
}
TEST(SimplexMore, HandleLineCollinearProducesPerp)
{
omath::collision::Simplex<omath::Vector3<float>> s;
s = {omath::Vector3<float>{2.f, 0.f, 0.f}, omath::Vector3<float>{1.f, 0.f, 0.f}};
omath::Vector3<float> dir{0.f, 0.f, 0.f};
const bool res = s.handle(dir);
EXPECT_FALSE(res);
EXPECT_GT(dir.length_sqr(), 0.0f);
}
TEST(SimplexMore, HandleTriangleFlipWinding)
{
constexpr omath::Vector3<float> a{1.f, 0.f, 0.f};
constexpr omath::Vector3<float> b{0.f, 1.f, 0.f};
constexpr omath::Vector3<float> c{0.f, 0.f, 1.f};
omath::collision::Simplex<omath::Vector3<float>> s;
s = {a, b, c};
omath::Vector3<float> dir{0.f, 0.f, 0.f};
constexpr auto ab = b - a;
constexpr auto ac = c - a;
const auto abc = ab.cross(ac);
const bool res = s.handle(dir);
EXPECT_FALSE(res);
const auto expected = -abc;
EXPECT_NEAR(dir.x, expected.x, 1e-6f);
EXPECT_NEAR(dir.y, expected.y, 1e-6f);
EXPECT_NEAR(dir.z, expected.z, 1e-6f);
}
TEST(SimplexMore, HandleTetrahedronInsideTrue)
{
omath::collision::Simplex<omath::Vector3<float>> s;
s = {omath::Vector3<float>{1.f, 0.f, 0.f}, omath::Vector3<float>{0.f, 1.f, 0.f},
omath::Vector3<float>{0.f, 0.f, 1.f}, omath::Vector3<float>{-1.f, -1.f, -1.f}};
omath::Vector3<float> dir{0.f, 0.f, 0.f};
const bool inside = s.handle(dir);
EXPECT_TRUE(inside);
}
TEST(SimplexMore, HandlePointSetsDirection)
{
omath::collision::Simplex<omath::Vector3<float>> s;
s = {omath::Vector3<float>{1.f, 2.f, 3.f}};
omath::Vector3<float> dir{0.f, 0.f, 0.f};
EXPECT_FALSE(s.handle(dir));
EXPECT_NEAR(dir.x, -1.f, 1e-6f);
EXPECT_NEAR(dir.y, -2.f, 1e-6f);
EXPECT_NEAR(dir.z, -3.f, 1e-6f);
}
TEST(SimplexMore, HandleLineReducesToPointWhenAoOpposite)
{
omath::collision::Simplex<omath::Vector3<float>> s;
s = {omath::Vector3<float>{1.f, 0.f, 0.f}, omath::Vector3<float>{2.f, 0.f, 0.f}};
omath::Vector3<float> dir{0.f, 0.f, 0.f};
EXPECT_FALSE(s.handle(dir));
EXPECT_EQ(s.size(), 1u);
EXPECT_NEAR(dir.x, -1.f, 1e-6f);
}

View File

@@ -99,15 +99,15 @@ TEST_F(UnitTestTriangle, SideLengths)
// Test side vectors
TEST_F(UnitTestTriangle, SideVectors)
{
const Vector3 sideA_t1 = t1.side_a_vector(); // m_vertex1 - m_vertex2
EXPECT_FLOAT_EQ(sideA_t1.x, 0.0f - 1.0f);
EXPECT_FLOAT_EQ(sideA_t1.y, 0.0f - 0.0f);
EXPECT_FLOAT_EQ(sideA_t1.z, 0.0f - 0.0f);
const Vector3 side_a_t1 = t1.side_a_vector(); // m_vertex1 - m_vertex2
EXPECT_FLOAT_EQ(side_a_t1.x, 0.0f - 1.0f);
EXPECT_FLOAT_EQ(side_a_t1.y, 0.0f - 0.0f);
EXPECT_FLOAT_EQ(side_a_t1.z, 0.0f - 0.0f);
const Vector3 sideB_t1 = t1.side_b_vector(); // m_vertex3 - m_vertex2
EXPECT_FLOAT_EQ(sideB_t1.x, 0.0f - 1.0f);
EXPECT_FLOAT_EQ(sideB_t1.y, 1.0f - 0.0f);
EXPECT_FLOAT_EQ(sideB_t1.z, 0.0f - 0.0f);
const Vector3 side_b_t1 = t1.side_b_vector(); // m_vertex3 - m_vertex2
EXPECT_FLOAT_EQ(side_b_t1.x, 0.0f - 1.0f);
EXPECT_FLOAT_EQ(side_b_t1.y, 1.0f - 0.0f);
EXPECT_FLOAT_EQ(side_b_t1.z, 0.0f - 0.0f);
}
TEST_F(UnitTestTriangle, IsRectangular)

View File

@@ -306,7 +306,7 @@ TEST_F(UnitTestVector2, DivisionAssignmentOperator_VectorWithZero)
// Test operations with infinity and NaN
TEST_F(UnitTestVector2, Operator_WithInfinity)
{
constexpr Vector2 v_inf(INFINITY, INFINITY);
const Vector2 v_inf(INFINITY, INFINITY);
const Vector2 result = v1 + v_inf;
EXPECT_TRUE(std::isinf(result.x));
EXPECT_TRUE(std::isinf(result.y));

View File

@@ -10,6 +10,61 @@
using namespace omath;
TEST(Vector3More, ConstructorsAndEquality)
{
constexpr Vector3<float> a;
EXPECT_EQ(a.x, 0.f);
EXPECT_EQ(a.y, 0.f);
EXPECT_EQ(a.z, 0.f);
constexpr Vector3<float> b{1.f, 2.f, 3.f};
EXPECT_EQ(b.x, 1.f);
EXPECT_EQ(b.y, 2.f);
EXPECT_EQ(b.z, 3.f);
const Vector3<float> c = b;
EXPECT_EQ(c, b);
}
TEST(Vector3More, ArithmeticAndDotCross)
{
constexpr Vector3<float> a{1.f, 0.f, 0.f};
constexpr Vector3<float> b{0.f, 1.f, 0.f};
const auto c = a + b;
constexpr Vector3<float> expect_c{1.f,1.f,0.f};
EXPECT_EQ(c, expect_c);
const auto d = a - b;
constexpr Vector3<float> expect_d{1.f,-1.f,0.f};
EXPECT_EQ(d, expect_d);
const auto e = a * 2.f;
constexpr Vector3<float> expect_e{2.f,0.f,0.f};
EXPECT_EQ(e, expect_e);
EXPECT_FLOAT_EQ(a.dot(b), 0.f);
// manual cross product check
const auto cr = Vector3<float>{ a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x };
constexpr Vector3<float> expect_cr{0.f,0.f,1.f};
EXPECT_EQ(cr, expect_cr);
}
TEST(Vector3More, NormalizationEdgeCases)
{
constexpr Vector3<double> z{0.0,0.0,0.0};
const auto zn = z.normalized();
EXPECT_DOUBLE_EQ(zn.x, 0.0);
EXPECT_DOUBLE_EQ(zn.y, 0.0);
EXPECT_DOUBLE_EQ(zn.z, 0.0);
constexpr Vector3<double> v{3.0,4.0,0.0};
const auto vn = v.normalized();
EXPECT_NEAR(vn.x, 0.6, 1e-12);
EXPECT_NEAR(vn.y, 0.8, 1e-12);
}
class UnitTestVector3 : public ::testing::Test
{
protected:
@@ -260,7 +315,7 @@ TEST_F(UnitTestVector3, Division_ByZeroScalar)
// Test operations with infinity
TEST_F(UnitTestVector3, Addition_WithInfinity)
{
constexpr Vector3 v_inf(INFINITY, INFINITY, INFINITY);
const Vector3 v_inf(INFINITY, INFINITY, INFINITY);
const Vector3 result = v1 + v_inf;
EXPECT_TRUE(std::isinf(result.x));
EXPECT_TRUE(std::isinf(result.y));
@@ -390,8 +445,10 @@ TEST_F(UnitTestVector3, AsTuple)
// Test AsTuple method
TEST_F(UnitTestVector3, AngleBeatween)
{
EXPECT_EQ(Vector3(0.0f, 0.0f, 1.0f).angle_between({1, 0 ,0}).value().as_degrees(), 90.0f);
EXPECT_EQ(Vector3(0.0f, 0.0f, 1.0f).angle_between({0.0f, 0.0f, 1.0f}).value().as_degrees(), 0.0f);
EXPECT_NEAR(Vector3(0.0f, 0.0f, 1.0f).angle_between({1, 0, 0}).value().as_degrees(),
90.0f, 0.001f);
EXPECT_NEAR(Vector3(0.0f, 0.0f, 1.0f).angle_between({0.0f, 0.0f, 1.0f}).value().as_degrees(),
0.0f, 0.001f);
EXPECT_FALSE(Vector3(0.0f, 0.0f, 0.0f).angle_between({0.0f, 0.0f, 1.0f}).has_value());
}

View File

@@ -11,6 +11,32 @@
using namespace omath;
TEST(Vector4More, ConstructorsAndClamp)
{
constexpr Vector4<float> a;
EXPECT_EQ(a.x, 0.f);
EXPECT_EQ(a.y, 0.f);
EXPECT_EQ(a.z, 0.f);
EXPECT_EQ(a.w, 0.f);
Vector4<float> b{1.f, -2.f, 3.5f, 4.f};
b.clamp(0.f, 3.f);
EXPECT_GE(b.x, 0.f);
EXPECT_GE(b.y, 0.f);
EXPECT_LE(b.z, 3.f);
}
TEST(Vector4More, ComparisonsAndHashFormatter)
{
constexpr Vector4<int> a{1,2,3,4};
constexpr Vector4<int> b{1,2,3,5};
EXPECT_NE(a, b);
// exercise to_string via formatting if available by converting via std::format
// call length and comparison to exercise more branches
EXPECT_LT(a.length(), b.length());
}
class UnitTestVector4 : public ::testing::Test
{
protected: