pure-cpp 1.0.0
A C++ physics simulation benchmark comparing performance with Python implementations
Pure C++

This project aims at benchmarking the impact of using Python when doing some time-consuming calculations and using a framework.

Table of Contents

  • About
  • Project Goals
  • Compiling
  • Running
  • Running Tests
  • Using the Logging System
  • Architecture Overview
  • Performance Optimisations
  • Licence

About

Documentation specific to this code is available on-line.

Project Goals

This project serves several key purposes:

  • Performance Benchmarking: To act as a real-world test case for evaluating the performance of compiled C++ code in computationally intensive tasks, particularly when compared to interpreted languages like Python.
  • Demonstrate Modern C++ Practices: To showcase how data-oriented design (Structure-of-Arrays), parallel processing with OpenMP, and modern C++20 features can be used to write high-performance code.
  • UI and Physics Integration: To provide a clear example of how to integrate a demanding physics simulation with a responsive, multi-threaded Qt6 graphical user interface.
  • Educational Resource: To serve as a learning tool for students and developers interested in physics simulation, performance optimisation in C++, or advanced application architecture.

Compiling

Prerequisites

Before you begin, ensure you have the following installed:

  • A C++20 compatible compiler (e.g., GCC, Clang, or MSVC).
  • CMake (version 3.19 or higher).
  • Ninja (recommended) or another build tool like Make.
  • **(Optional) Conan**: If you plan to use Conan for dependency management.

If you are not using Conan, you will need to install the following dependencies via your system's package manager:

  • Boost (program-options)
  • Eigen
  • Qt 6 (Core, Gui, Widgets, Quick, QuickWidgets, 3DCore, 3DExtras, 3DRender, LinguistTools, Concurrent)
  • Doxygen and Graphviz (for documentation)

Note for VS Code Users: The recommended C/C++ and CMake Tools extensions provide a seamless experience and are highly recommended.

Building the Project

This project uses a modern CMake setup with presets to ensure a consistent and straightforward build process across different platforms.

Method 1: Using the Helper Scripts (Recommended)

The easiest way to configure the project is to use the provided helper scripts located in the utils/ directory. They will create the correct build directory and run CMake with the appropriate preset for you.

On Linux, macOS, or other Unix-like systems:

Use the setup-build.sh script. You can see all options by running it with --help.

# Example: Configure with GCC and Ninja (the defaults)
./utils/setup-build.sh
# Example: Configure with Clang, Make, and use Conan for dependencies
./utils/setup-build.sh --compiler clang --generator make --conan
# Example: Generate an Xcode project
./utils/setup-build.sh --generator xcode

On Windows:

Use the setup-build.ps1 PowerShell script. You can see all options by running Get-Help .\utils\setup-build.ps1 -Full.

# Example: Configure for MSVC with Ninja (the defaults)
./utils/setup-build.ps1
# Example: Configure for Visual Studio 2022 with Conan
./utils/setup-build.ps1 -Generator vs -Compiler msvc -Conan
# Example: Configure with MinGW (GCC)
./utils/setup-build.ps1 -Compiler gcc

After configuration, you can build the project using CMake:

# The helper script will tell you the correct preset name to use
cmake --build build/<preset-name>

Method 2: Manual Configuration (Advanced)

If you prefer not to use the helper scripts, you can configure the project manually using CMake Presets.

  1. List available presets:

    cmake --list-presets
  2. **(Optional) Use Conan for dependencies:** If you choose a conan-* preset, you must first run conan install.

    # Example for a specific preset
    conan install . --output-folder=build/conan-gcc-ninja --build=missing -s build_type=Debug
  3. Configure with CMake:

    # Example: Configure for GCC with Ninja
    cmake --preset dev-gcc-ninja
    # On Windows with Visual Studio
    cmake --preset dev-msvc-vs
  4. Build the project:

    # The build directory is defined by the preset
    cmake --build build/dev-gcc-ninja

Using Visual Studio Code

The project is fully configured for a seamless experience in VS Code:

  1. Open the project folder in VS Code.
  2. Install the recommended extensions when prompted (especially C/C++ and CMake Tools).
  3. The CMake Tools extension will automatically detect the CMakePresets.json file.
  4. Use the status bar at the bottom to select your desired Configure Preset (e.g., dev-gcc-ninja).
  5. Build the project by clicking the "Build" button in the status bar or by running the **CMake: Build** command.
  6. Debug the executable by pressing **F5**. The launch.json is pre-configured to work with CMake Tools.

Translation files

To update the translation source (.ts) files after making changes to the UI or source code, build the lupdate target:

cmake --build build/<preset-name> --target lupdate

To add a new language (e.g., German), follow these steps:

  1. Generate the new .ts file: From your build directory, run the following command:

    cmake --build build/<preset-name> --target new_lang -DLANG=de

    This will create an empty pure-cpp/locales/pure-cpp_de.ts file.

  2. Populate the new file with strings: Run the lupdate target again to scan your source code and fill the new file with translatable strings.

    cmake --build build/<preset-name> --target lupdate
  3. Translate the strings: Open pure-cpp_de.ts in Qt Linguist, fill in the translations, and save the file.
  4. Rebuild the project: Simply build the project as usual.

Advanced Targets

The build system provides several additional targets for common tasks:

Documentation

To generate the documentation with Doxygen, build the doxygen target:

cmake --build build/<preset-name> --target doxygen

The output will be in build/<preset-name>/doc/html/. You can enable other formats (e.g., LaTeX) by setting CMake options like -DGEN_LATEX=ON during the configuration step.

Installation

To install the compiled binary and any associated resources, build the install target. This may require administrator privileges.

cmake --build build/<preset-name> --target install

Packaging

You can create a distributable package (e.g., TGZ, ZIP) using CPack after building the project.

# Navigate to the build directory first
cd build/<preset-name>
cpack -G "STGZ;TGZ"

Running

To see all available command-line options and their descriptions, run the executable with the --help flag:

./build/<preset-name>/pure-cpp --help

Running Tests

The project includes a comprehensive test suite using Google Test. To run all tests:

# After building the project
./build/<preset-name>/Debug/bin/core_tests

To run tests with CTest (CMake's test runner):

cd build/<preset-name>
ctest

To run a specific test:

./build/<preset-name>/Debug/bin/core_tests --gtest_filter=SpaceTest.*

To run tests with coverage reporting (requires coverage flags enabled during build):

# Coverage flags must be enabled during CMake configuration
cmake --preset dev-gcc-ninja -DENABLE_COVERAGE=ON
cmake --build build/dev-gcc-ninja
./build/dev-gcc-ninja/Debug/bin/core_tests

Using the Logging System

The application includes a flexible logging system to monitor its behaviour and diagnose issues. You can control the log output and verbosity using the following command-line arguments:

  • -l, --log-level {debug,info,warning,critical,fatal}: Sets the logging verbosity. Higher levels include lower ones (e.g., info will also show warning, critical, and fatal messages). The default level is critical.
  • -c, --debug-console: Prints log messages to the standard output (your terminal).
  • -f, --debug-file <path/to/logfile.log>: Writes log messages to the specified file.
  • -s, --debug-syslog <identifier>: Sends log messages to the system's Syslog service, using the given identifier (Unix-like systems only).

You can combine these options. For example, to see all debug messages on the console and also save them to a file, you would use:

./build/<preset-name>/pure-cpp -l debug -c -f simulation.log

Architecture Overview

The simulation is built on a modular, multi-threaded architecture designed to separate the physics calculations from the user interface, ensuring a responsive application.

Core Components

  • **main.cpp (Entry Point)**: Orchestrates the application lifecycle. It initializes the QApplication, parses command-line arguments (using cmd_line_parser.hpp), sets up logging (logger.hpp), and creates the MainWindow.
  • **display.hpp/cpp (View)**: Manages the Qt3DWindow and all visual aspects of the scene. It is responsible for setting up the camera and lighting, and for creating and updating the visual representation of each body. It owns and manages the PhysicsWorker thread.
  • **physics_worker.cpp (Controller)**: Acts as a bridge between the UI and the physics engine. It runs in a separate QThread and owns the Space object. Its primary role is to trigger physics steps and emit signals with the updated data.
  • **space.hpp/cpp (Physics Model)**: The core physics engine. It is completely decoupled from Qt and the UI. It manages all physical state and contains the main computeDynamics loop, which handles integration, force calculation, and collision detection/response.
  • **body.hpp (Data Structure)**: Implements a high-performance Structure-of-Arrays (SoA) data layout with the Bodies class. This ensures that data for physics calculations (e.g., all positions, all velocities) is stored contiguously in memory for cache efficiency and vectorization. The BodyProxy class provides a convenient Array-of-Structures (AoS)-like interface for easy access to individual body data.

Asynchronous Simulation Loop

The application uses a signal-and-slot mechanism to create a non-blocking simulation loop, which keeps the UI responsive at all times.

  1. Initiation: The Display class moves a PhysicsWorker instance to a separate QThread and starts the thread.
  2. Physics Step: The PhysicsWorker calls space.computeDynamics() to run one full step of the simulation. This is a blocking call that runs entirely in the physics thread.
  3. Data Emission: Once the step is complete, the PhysicsWorker emits an updatedBodyData signal, passing the new positions and orientations as raw data for efficiency.
  4. UI Update: The Display class's updateFrame slot, which runs in the main UI thread, receives this signal. It iterates through the visual objects and updates their 3D transforms using optimised in-place updates to avoid object allocations.
  5. Scheduling the Next Step: After updating the UI, updateFrame uses QTimer::singleShot(0, ...) to schedule the next call to the PhysicsWorker's performSingleStep slot. Using a zero-delay timer posts an event to the Qt event loop, which will be processed as soon as the UI thread is idle. This prevents the physics steps from queuing up faster than the UI can render them, ensuring a smooth and stable frame rate.

This architecture effectively decouples the rendering rate from the simulation rate. The physics engine can run as fast as possible in its own thread, while the UI updates at a smooth, consistent pace.

Performance Optimisations

This project employs several key strategies to achieve high performance in a C++ environment.

  • Data-Oriented Design (Structure-of-Arrays): The body.hpp module uses a Structure-of-Arrays (SoA) layout. Instead of a list of body objects each holding their own data (AoS), all positions, velocities, and masses are stored in contiguous std::vector containers. This design is highly cache-friendly and enables effective vectorization and parallelization.
  • Parallel Processing with OpenMP: The most computationally intensive tasks (gravity calculation, collision detection, and graph coloring) are parallelised using OpenMP. The workload is automatically distributed across available CPU cores, significantly reducing computation time.
  • Efficient Algorithms:
    • Two-Stage Collision Detection: A k-d tree (nanoflann::KDTree) is used for a "broad-phase" check to quickly discard pairs of bodies that are too far apart to collide. Only the remaining candidate pairs are passed to a more precise, but more expensive, "narrow-phase" check.
    • Adaptive Time-Stepping: The simulation dynamically adjusts the time step (dt) based on the maximum acceleration, velocity, angular velocity, and angular acceleration in the system. This improves both performance and stability by allowing for larger, faster steps when bodies are far apart and smaller, more precise steps during close encounters or collisions.
    • Stable Rotational Integration: Quaternion-based integration is used for rotational dynamics, providing better stability and accuracy compared to Euler angle methods.
  • Graph Coloring for Collision Resolution: Collisions are partitioned into independent sets using graph coloring, allowing parallel resolution of non-conflicting collisions.
  • Rendering Optimisations:
    • In-Place Object Updates: Transform updates reuse objects, updating them in-place to avoid allocations.
    • Efficient Iteration: Uses direct array access for efficient iteration over body data.
    • Optimised Vector Operations: Vector operations are optimized for cache locality.

Licence

This programme is free software; you can redistribute it and/or modify it under the terms of the GNU General Public Licence as published by the Free Software Foundation; either version 3 of the licence, or (at your option) any later version. See the file named “LICENSE.txt” or on-line.

POSIX is a registered trademark owned by the Open Group.

Linux is a registered trademark owned by Torvalds, Linus.

macOS and Xcode are registered trademarks owned by Apple.

Microsoft Windows and Visual studio are registered trademarks owned by Microsoft.

Oracle Solaris is a registered trademark owned by Oracle Corporation.

Copyright © 2024 – 2025 Le Bars, Yoann.