A C++ engine for simulating and visualizing physical systems, rendered with OpenGL.
The engine encodes the math and physics that define how things behave. Applications
under Applications/ set up specific scenarios and run them against that shared
description of reality.
- Relativistic spacetime core: Minkowski, Schwarzschild, Kerr, and FLRW metrics with a geodesic solver
- Gravity as a hierarchy of models: Newtonian (with general spherical-harmonics oblateness), post-Newtonian, Barnes-Hut, and GR on fixed background metrics
- Electromagnetism, acoustics, fluid dynamics (SPH and Eulerian), continuum mechanics, thermodynamics, and quantum mechanics (time-independent and time-dependent Schrödinger equation) — the wave/diffusion/eigenvalue rungs (Maxwell, acoustic, heat equation, Eulerian, Schrödinger) each solved in both 1D and full 3D
- Optics on curved spacetime: light propagation, gravitational lensing, and frequency shift (Doppler, gravitational, cosmological), plus flat-space radiation pressure, diffraction, and relativistic aberration
- Custom math library: vectors, matrices, quaternions, tensors, ODE integrators (including symplectic), general/symmetric eigendecomposition and SVD, special functions, closed-form polynomial roots, Euclidean geometry, spatial partitioning (k-d tree, BVH, octree), and FFT (1D and 3D)
- Strongly-typed, dimensioned quantities (scalar or vector)
- Real-time OpenGL rendering: instanced meshes, multi-light Blinn-Phong, immediate-mode debug drawing with billboard and fixed-orientation text labels, skyboxes, plus a fragment-shader ray tracer with shadows and reflections
- ImGui controls and ImPlot charts (live time-series and scatter) in the same window and frame as the 3D view
- GPU compute backends: OpenGL compute shaders, CUDA, Vulkan
- Example application: solar system (Newtonian N-body, energy/momentum tracked live)
Gravity is modeled as a ladder of approximations to general relativity, since full dynamical numerical relativity is intractable for general many-body systems. Newtonian gravity (GR's weak-field limit) drives dynamical self-gravitating systems; post-Newtonian adds relativistic corrections; and fixed background metrics handle light and test particles — Schwarzschild and Kerr near compact objects, FLRW at cosmological scales. Applications select the model that fits their regime and budget. Because spacetime is the core abstraction, light propagation, lensing, and all frequency shifts are the same computation — a null geodesic through a metric — evaluated in different metrics.
The core (math, physics, integrators, units) is implemented in-house.
External libraries, vendored under third_party/:
| Library | Role |
|---|---|
| OpenGL | Graphics API |
| GLFW | Window, input, OpenGL context |
| GLAD | OpenGL function loader (generated, committed) |
| Dear ImGui | Control and debug panels |
| Dear ImPlot | Charting on top of Dear ImGui |
| stb_image | Image loading for Renderer::Texture |
| spdlog | Logging (configured to use std::format) |
| GoogleTest | Test framework (tests only) |
General string formatting uses C++20 std::format, so there's no separate
formatting dependency.
- C++20 compiler with
std::format(GCC 13+, Clang 17+, or MSVC 19.32+) - CMake 3.20+
Core/Logger.hpp takes its format strings as std::format_string, so a bad
format string is a compile error rather than a runtime throw. That name is
P2508, which MSVC exposes from 19.32 (VS 2022 17.2); <format> alone arrived
earlier, at 19.29.
No GPU is required. The CPU compute backend is the reference implementation, so the engine and its full test suite build and run on any machine. Everything below is optional acceleration, selected at runtime with a fallback to CPU:
| Optional | Enables |
|---|---|
| OpenGL 4.1+ driver | The real-time visualizer |
| OpenGL 4.3+ driver | The OpenGL compute backend (compute shaders are 4.3) |
| CUDA Toolkit | The CUDA compute backend (NVIDIA only) |
| Vulkan SDK | The Vulkan compute backend (via MoltenVK on macOS) |
macOS caps OpenGL at 4.1, so the visualizer works there but the OpenGL compute backend does not. See src/Compute/README.md for how backend selection and fallback work per platform.
GLFW builds both the X11 and Wayland backends, and needs their development packages present at build time. On Debian and Ubuntu:
sudo apt install xorg-dev libwayland-dev libwayland-bin libxkbcommon-devWindows and macOS need nothing beyond a compiler and CMake. A headless build
(-DYSQ_BUILD_GRAPHICS=OFF) needs none of these on any platform.
Optionally, libosmesa6 provides an OpenGL context on a machine with no display
at all, in software. It is loaded at run time rather than linked, so it is not
needed to build; without it, Platform's headless backend has no context to
give and the one test that wants one skips.
git clone --recurse-submodules <repo-url> ysq
cd ysq
cmake -B build
cmake --build buildIf you already cloned without submodules:
git submodule update --init --recursive| Option | Default | Effect |
|---|---|---|
YSQ_BUILD_TESTS |
OFF |
Build the test suite |
YSQ_BUILD_BENCHMARKS |
OFF |
Build the developer benchmark tools under benchmarks/ |
YSQ_BUILD_COMPILE_FAIL_TESTS |
ON |
Build the tests that assert something does not compile. Only reached when YSQ_BUILD_TESTS is on; each costs a nested compiler invocation. |
YSQ_BUILD_GRAPHICS |
ON |
Build against GLFW, GLAD and Dear ImGui. OFF drops them entirely, for headless and CI builds. |
YSQ_WARNINGS_AS_ERRORS |
OFF |
Treat warnings as errors. CI builds with this on. |
YSQ_REQUIRE_HEADLESS_GL |
OFF |
Fail rather than skip when no headless OpenGL context can be created. For machines known to have OSMesa; CI sets it on the one job that does. |
YSQ_BUILD_COMPUTE_CUDA |
ON |
Build the CUDA compute backend if the toolkit is found. Detected, not required: an absent toolkit is skipped, not a build failure. |
YSQ_BUILD_COMPUTE_VULKAN |
ON |
Build the Vulkan compute backend if the SDK is found. Same "use it if found" behaviour as above. |
cmake -B build -DYSQ_BUILD_GRAPHICS=OFF # headless: no graphics dependenciesRelease is the default build type. CMake's own default, with no
CMAKE_BUILD_TYPE set, is unoptimized (no -O2/-O3), which is a real cost
for actually running an application: a large N-body scene evaluates its
gravity in a tight, hot loop that an unoptimized build does not speed up at
all. This project sets Release whenever CMAKE_BUILD_TYPE is not given, so
the default cmake -B build already builds optimized.
Debug is opt-in, for working on the engine itself: every assert() stays
active there, which is what catches a real bug fastest, and is the build the
test suite is normally run against.
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DYSQ_BUILD_TESTS=ON
cmake --build buildEach program under Applications/ builds to its own executable in build/bin/:
./build/bin/solar-systemAvailable: solar-system, lunar-eclipse, kepler-solar-system.
No build tools needed: prebuilt binaries for every application, on Linux,
macOS (Apple Silicon) and Windows, are published from the tip of main to
the latest release
— one zip per app per platform, plus one zip per platform bundling every
app together. It's rebuilt on every push to main, so it always reflects
the current tip, not a fixed version; see "Continuous delivery" below.
These binaries are unsigned — code signing needs a paid certificate this project doesn't have — so the OS will warn on first run:
- Windows shows a SmartScreen warning ("Windows protected your PC"). Click "More info", then "Run anyway".
- macOS Gatekeeper refuses to open the app from a double-click the
first time. Right-click it and choose "Open" instead (needed once), or
run
xattr -d com.apple.quarantine <path>after unzipping.
Tests live under tests/ and run through CTest. Not built by default; enable
with -DYSQ_BUILD_TESTS=ON:
cmake -B build -DYSQ_BUILD_TESTS=ON
cmake --build build
ctest --test-dir buildUnit tests cover modules in isolation — a vector rotation, an integrator's observed order of accuracy, a units dimension check. Integration tests cover combinations — a symplectic integrator with Newtonian gravity holding a stable orbit, or spacetime with optics reproducing a known deflection angle. End-to-end tests run whole applications headless and assert physical invariants such as energy and momentum conservation. Smoke tests sit below all of that and cover the build itself: that each vendored dependency actually links and that build options took effect.
Compile-failure tests are the one category that asserts an absence. Units
guarantees that adding a distance to a mass will not build, and a test suite made
only of programs that compile cannot check a guarantee like that. Those live in
tests/compile_fail/ as targets marked WILL_FAIL, each paired with the
positive form of the same construct in an ordinary test, since a failing build
proves nothing on its own about why it failed.
benchmarks/ holds developer timing tools, not correctness tests — not
built by default; enable with -DYSQ_BUILD_BENCHMARKS=ON. Currently one:
benchmark_compute_thresholds, which measures the CPU-vs-GPU crossover
point for every GPU-dispatchable operation in Math and reports the size
each header's own kXxxGpuDispatchThreshold constant should be set to.
See src/Compute/README.md
for how those measurements feed back into the engine.
Everything runs CPU-only and needs no GPU, no window and no display. The one
exception is the OpenGL context test, which still needs no display: it uses a
software context where there is no display server, and skips where even that is
unavailable. Configure with -DYSQ_REQUIRE_HEADLESS_GL=ON to make those skips
failures. See tests/README.md.
Six jobs: Linux, macOS and Windows, each with graphics on and off, all built
with warnings as errors. Windows is not optional coverage: the project
claims MSVC support and cmake/YsqWarnings.cmake carries an MSVC-specific
warning set, so without a Windows job neither the claim nor the code behind
it would be tested. Every job is CPU-only and needs no display, so none needs
a GPU or a virtual framebuffer.
The one test whose outcome depends on the machine is the OpenGL context test,
which skips where no context can exist. A test that skips on all six jobs
tests nothing, so the Linux graphics-on job installs OSMesa and configures
with YSQ_REQUIRE_HEADLESS_GL=ON, turning that skip into a failure on the one
runner where a context is guaranteed to be available.
.github/workflows/release.yml builds a real, windowed Release build on
Linux, macOS and Windows on every push to main, installs each
application into its own directory (stage/<App>/, via each app's own
install() rule; see src/Applications/README.md's convention section),
and zips them: one archive per app per platform, plus one combined archive
per platform with every app together. A publish step then replaces a
single rolling latest release's assets with the new build. Nothing here
names a fixed list of applications — a new one under Applications/ is
picked up automatically the next time this runs, as long as it follows the
same install() convention.
cmake/YsqWarnings.cmake defines two INTERFACE targets, linked PRIVATE so
they apply to a module's own sources without leaking to its consumers:
ysq::warnings for everything, and ysq::warnings_strict, which adds
-Wconversion -Wsign-conversion -Wdouble-promotion on top and goes on the
engine core (Math, Units, Physics, Compute).
The split is deliberate. In the engine core a silent double-to-float
narrowing is a physics bug: an energy accumulator or an integrator tolerance
quietly loses precision and a conservation invariant drifts. In the
presentation layer (Renderer, UI, Applications) the same conversions are
constant and intentional, since OpenGL and ImGui are float/int APIs, and
the strict set there would only train people to reach for static_cast
without fixing anything. -Wsign-conversion is listed explicitly because
Clang's -Wconversion implies it for C++ and GCC's does not; without it a
build clean on one compiler fails on the other.
Math and Units are header-only INTERFACE targets, so they cannot carry
the strict set on their own target: on an INTERFACE target the flags would
reach every consumer's own sources instead. Both apply it in a smoke test
that includes every header and explicitly instantiates every template, since
an uninstantiated template is barely checked at all.
.clang-format is the whole style. CI checks it on pull requests and on pushes
to main, over the same file list this formats:
git ls-files '*.hpp' '*.cpp' '*.h' '*.c' | grep -v '^third_party/' \
| tr '\n' '\0' | xargs -0 clang-format -iclang-format's output shifts between releases, so CI pins one version
(22.1.8, from PyPI). A different local version can disagree with it on
untouched code. third_party/ is upstream code and is never reformatted.
Each module under src/ is its own library (static, or header-only INTERFACE
where it's template-only), defined by its own CMakeLists.txt. Applications link
against those libraries and build to executables. Every library module carries a
README.md describing its interface and dependencies. Tests under tests/ link
the modules they exercise plus GoogleTest, outside the library dependency graph.
Dependencies flow one way. Core and Platform (window, GL context, input) sit
at the true base. Compute builds on Core alone for its CPU, CUDA and Vulkan
backends, and additionally on Platform for its OpenGL backend and on Apple's
native Metal API for its Metal backend. Every one of those is a domain-neutral
"how do we compute" primitive, not a physical quantity, which is what lets
Math build on Compute in turn: Math's handful of numerical methods large
enough to benefit from GPU dispatch (large FFTs, dense linear solves and
eigendecomposition, batched per-point evaluation, parallel random sampling,
sorting) reach it the same way Physics does. Units builds on
Math. Physics builds on Compute, Math and Units, falling back to the
CPU backend when no GPU is present. Renderer and UI form the presentation
layer, drawing on Platform and Math. Applications sit on top. Nothing
lower depends on anything higher. A headless visual run uses an offscreen
context; the simulation core and tests need no graphics context at all.
Directory layout; each library module under src/ carries its own README.md
listing its headers in full, so the tree below stops at the module boundary
rather than duplicating that per file. That duplication had drifted out of
sync with the code before this line was written, which is the reason it isn't
done that way anymore.
ysq/
├── CMakeLists.txt
├── README.md
├── LICENSE
├── .clang-format
├── .github/workflows/ci.yml
├── cmake/ Shared CMake modules (warning sets, shader embedding)
├── docs/ Consumer-facing documentation
│
├── third_party/ Vendored dependencies
│ ├── README.md
│ ├── glfw/ submodule
│ ├── glad/ generated loader, committed
│ ├── imgui/ submodule
│ ├── implot/ submodule
│ ├── stb/ submodule (stb_image)
│ ├── spdlog/ submodule
│ └── googletest/ submodule (tests)
│
├── src/
│ ├── CMakeLists.txt
│ │
│ ├── Core/ Logging, timing, identity, events, configuration
│ ├── Math/ Vectors, matrices, quaternions, tensors, calculus, ODE integrators
│ ├── Units/ Dimensioned quantities over the SI
│ ├── Platform/ Window, GL context, input (graphics builds only)
│ │
│ ├── Compute/ Backend Physics dispatches to
│ │ ├── CPU/ Reference implementation, always available
│ │ ├── OpenGL/ 4.3+ compute shaders, graphics builds only
│ │ ├── CUDA/ Built only when the CUDA Toolkit is found
│ │ └── Vulkan/ Built only when the Vulkan SDK is found
│ │
│ ├── Physics/ Mechanics, Spacetime, Gravity, Electromagnetism,
│ │ │ Acoustics, Fluids, Continuum, Thermodynamics,
│ │ │ Optics, QuantumMechanics: organized by theory
│ │ ├── Mechanics/
│ │ ├── Spacetime/
│ │ ├── Gravity/
│ │ ├── Electromagnetism/
│ │ ├── Acoustics/
│ │ ├── Fluids/
│ │ ├── Continuum/
│ │ ├── Thermodynamics/
│ │ ├── Optics/
│ │ └── QuantumMechanics/
│ │
│ ├── Renderer/ Camera, shaders, meshes, textures, rasterizer, ray tracer
│ │ └── shaders/ *.vert, *.frag, embedded at configure time
│ │
│ ├── UI/ Dear ImGui panels, Dear ImPlot charts
│ │
│ └── Applications/ Runnable simulation programs
│ ├── Helper/ Scenario-setup code shared between applications
│ ├── SolarSystem/ Real N-body gravity, energy/momentum tracked live
│ ├── LunarEclipse/
│ └── KeplerSolarSystem/ Closed-form Kepler propagation, no integration cost
│
└── tests/
├── support/ Test-only helpers, outside the engine
├── smoke/ Build wiring: dependencies link, options took effect
├── unit/ One module in isolation
├── integration/ Modules in combination
├── compile_fail/ Constructs that must not compile (CTest WILL_FAIL)
└── e2e/ Full application runs, headless
| Module | Contents |
|---|---|
Core |
Logging (spdlog behind a facade), timing (simulation and wall-clock), UUIDs, events, configuration, CSV data loading |
Math |
Vectors, matrices, quaternions, complex/dual numbers, tensors, statistics, sorting and order statistics, interpolation, calculus, ODE interface and integrators (Euler, RK4, adaptive, symplectic), root-finding, general linear solving, symmetric and general eigendecomposition, SVD/QR, special functions (error, gamma, Legendre, Bessel), closed-form polynomial roots, randomness (a sequential engine plus a second, GPU-dispatching parallel/counter-based family), gradient-based optimization, Euclidean geometry (intersection, closest-point, convex hull, oriented bounding boxes), spatial partitioning (k-d tree, BVH, octree), and FFT (1D and 3D) |
Units |
Dimensioned quantities (scalar or vector) built from the SI's seven base dimensions: length, mass, time, velocity, acceleration, force, energy, temperature, electromagnetism, fluids, chemistry, elasticity, luminosity, and the constants that define the SI. Built on Math |
Platform |
Window, GL context, and input, wrapping GLFW |
Compute |
Backend Physics and Math dispatch to: a CPU reference implementation plus GPU acceleration (Metal on Apple platforms, OpenGL compute shaders, CUDA, Vulkan) |
Physics |
Mechanics (incl. springs, drag, collision, friction, constraints, rigid-body inertia); relativistic spacetime (Minkowski, Schwarzschild, Kerr, FLRW) with a geodesic solver; gravity (Newtonian with spherical-harmonics oblateness, post-Newtonian, Barnes-Hut summation); electromagnetism (quasi-static fields plus a 1D/3D Maxwell FDTD solver); acoustics (1D/3D linear wave equation); fluids (SPH, and Eulerian in 1D/3D); continuum mechanics (elastic chains); thermodynamics (ideal gas, black-body, statistical mechanics, radiative transfer, 1D/3D heat equation); optics (propagation, lensing, frequency shift, radiation pressure, diffraction, aberration); quantum mechanics (1D/3D time-independent and time-dependent Schrödinger equation) |
Renderer |
Camera and controllers, shaders, instanced meshes, textures, immediate-mode debug drawing and text labels, and both a forward rasterizer and a fragment-shader ray tracer |
UI |
Dear ImGui panels bound to plain references, Dear ImPlot charts, a stats overlay |
Applications |
Runnable simulation programs built on the engine |
Each library module under src/ has its own README.md describing its
interface, dependencies, and the derivations behind it: the authoritative
reference for how the engine works. docs/ holds consumer-facing
documentation: tutorials and conceptual explanations for building a
simulation in Applications/, cross-linking down into the module READMEs
rather than restating them.
MIT. See LICENSE.