pure-cpp 1.0.0
A C++ physics simulation benchmark comparing performance with Python implementations
display.cpp
Go to the documentation of this file.
1/**
2 * \file display.cpp
3 * \brief Implements the Qt3D window for displaying spherical moving bodies.
4 * \author Laurent, Jules
5 * \author Le Bars, Yoann
6 *
7 * This file is part of the pure C++ benchmark.
8 *
9 * This program is free software: you can redistribute it and/or modify it
10 * under the terms of the GNU General Public License as published by the Free
11 * Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17 * more details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
23#include "display.hpp"
24
25#include <QApplication>
26#include <QColor>
27#include <QTimer>
28#include <QVector3D>
29#include <Qt3DCore/QEntity>
30#include <Qt3DCore/QTransform>
31#include <Qt3DExtras/QConeMesh>
32#include <Qt3DExtras/QCylinderMesh>
33#include <Qt3DExtras/QDiffuseSpecularMaterial>
34#include <Qt3DExtras/QSphereMesh>
35#include <Qt3DExtras/Qt3DWindow> // NOLINT
36#include <chrono>
37#include <random>
38
39#include "app_profiler.hpp"
40#include "pcg_random.hpp"
41#include "space.hpp"
42
45}
46
47std::pair<Qt3DCore::QEntity*, Qt3DCore::QTransform*>
48Window::Display::createArrowEntity(Qt3DCore::QEntity* parent,
49 const QColor& colour, float radius,
50 float length) {
51 auto* arrowEntity = new Qt3DCore::QEntity(parent);
52 auto* arrowTransform = new Qt3DCore::QTransform(arrowEntity);
53
54 // --- Material ---
55 auto* material = new Qt3DExtras::QDiffuseSpecularMaterial(arrowEntity);
56 material->setAmbient(colour);
57
58 // --- Shaft (Cylinder) ---
59 auto* shaftEntity = new Qt3DCore::QEntity(arrowEntity);
60 auto* shaftMesh = new Qt3DExtras::QCylinderMesh();
61 shaftMesh->setRadius(radius);
62 shaftMesh->setLength(length);
63 auto* shaftTransform = new Qt3DCore::QTransform();
64 // The cylinder is oriented along the Y-axis by default. We rotate it to
65 // align with Z.
66 shaftTransform->setRotationX(90);
67 shaftEntity->addComponent(shaftMesh);
68 shaftEntity->addComponent(shaftTransform);
69 shaftEntity->addComponent(material);
70
71 // --- Head (Cone) ---
72 auto* headEntity = new Qt3DCore::QEntity(arrowEntity);
73 auto* headMesh = new Qt3DExtras::QConeMesh();
74 headMesh->setTopRadius(0);
75 headMesh->setBottomRadius(radius * 2.5f);
76 headMesh->setLength(radius * 5.0f);
77 auto* headTransform = new Qt3DCore::QTransform();
78 headTransform->setTranslation(QVector3D(0, 0, length));
79 headTransform->setRotationX(90);
80 headEntity->addComponent(headMesh);
81 headEntity->addComponent(headTransform);
82 headEntity->addComponent(material);
83
84 // The arrow entity itself will be scaled and rotated to match the vector.
85 arrowEntity->addComponent(arrowTransform);
86 arrowEntity->setEnabled(false); // Initially hidden
87
88 return {arrowEntity, arrowTransform};
89}
90
91/**
92 * \brief Create and set up the spherical bodies in the scene.
93 */
95 // Hue in [0, 1) for vibrant, distinct colours (HSV, aligned with Python).
96 std::uniform_real_distribution<> hueDis(0.0, 1.0);
97
98 bodies_.resize(physicsWorker_->getInitialBodies().size());
99
100 for (std::size_t i = 0; i < bodies_.size(); ++i) {
101 // Create an entity for each body. It is parented to the root entity,
102 // so its lifetime is managed by the root.
103 auto* entity = new Qt3DCore::QEntity(rootEntity_);
104
105 // Mesh (the shape). Parented to the entity.
106 auto* mesh = new Qt3DExtras::QSphereMesh(entity); // NOLINT
107 mesh->setRadius(physicsWorker_->getInitialBodies()[i].r());
108
109 // Transform (position, rotation, scale). Parented to the entity.
110 auto* transform = new Qt3DCore::QTransform(entity); // NOLINT
111
112 // Create a unique material for each sphere (HSV: saturation 0.9, value
113 // 0.95).
114 const QColor randomColour = QColor::fromHsvF(hueDis(gen), 0.9, 0.95);
115 auto* colouredMaterial = // NOLINT
116 new Qt3DExtras::QDiffuseSpecularMaterial(entity);
117
118 // Attach components to the entity *before* configuring them. This
119 // ensures Qt's backend fully owns the components before their
120 // properties are modified, which can prevent race conditions with the
121 // render thread.
122 entity->addComponent(mesh);
123 entity->addComponent(transform);
124 entity->addComponent(colouredMaterial);
125
126 // Now that the component is part of the scene graph, configure it.
127 colouredMaterial->setDiffuse(randomColour);
128 colouredMaterial->setShininess(200);
129 colouredMaterial->setAmbient(randomColour.darker(110));
130 // Save every new sphere to a body list with its parameters.
131
132 // Create visualisers for torque and angular acceleration
133 auto [torqueArrowEntity, torqueArrowTransform] =
134 createArrowEntity(entity, QColor("magenta"), 0.5f, 10.0f);
135 auto [alphaArrowEntity, alphaArrowTransform] =
136 createArrowEntity(entity, QColor("cyan"), 0.5f, 10.0f);
137
138 bodies_[i] = std::make_tuple(
139 QPointer<Qt3DCore::QEntity>(entity), QPointer(transform),
140 QPointer<Qt3DCore::QEntity>(torqueArrowEntity),
141 QPointer<Qt3DCore::QTransform>(torqueArrowTransform),
142 QPointer<Qt3DCore::QEntity>(alphaArrowEntity),
143 QPointer<Qt3DCore::QTransform>(alphaArrowTransform));
144 }
145}
146
147/**
148 * \brief Set up the scene.
149 */
150void Window::Display::createScene(unsigned int seed) {
151 // Use std::random_device to get a non-deterministic seed if none is
152 // provided.
153 Rng::Pcg32 gen(seed == 0 ? std::random_device{}() : seed);
154 createSpheres(gen);
155}
156
157/**
158 * \brief Starts the physics simulation by starting the worker thread.
159 */
161 // Start the physics thread. The thread's `started` signal will trigger the
162 // first simulation step. The simulation timer will then take over to drive
163 // subsequent steps.
164 physicsThread_->start();
165 // Start the timer with 0ms interval (triggers as soon as possible)
166 // This timer will automatically call performSingleStep() repeatedly
167 simulationTimer_->start(0);
168}
169
171 if (physicsShutdown_) {
172 return;
173 }
174 physicsShutdown_ = true;
175
176 if (simulationTimer_) {
177 simulationTimer_->stop();
178 }
179
180 if (physicsWorker_) {
181 disconnect(physicsWorker_, &Model::PhysicsWorker::updatedBodyData, this,
183 disconnect(physicsWorker_, &Model::PhysicsWorker::simulationFinished,
185 if (simulationTimer_) {
186 disconnect(physicsWorker_,
188 simulationTimer_, &QTimer::stop);
189 }
190 disconnect(simulationTimer_, &QTimer::timeout, physicsWorker_,
192 physicsWorker_->stopSimulation();
193 }
194
195 if (physicsThread_) {
196 disconnect(physicsThread_, &QThread::started, physicsWorker_,
198 }
199
200 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
201
202 if (physicsThread_ && physicsThread_->isRunning()) {
203 physicsThread_->quit();
204 if (!physicsThread_->wait(5000)) {
205 physicsThread_->terminate();
206 physicsThread_->wait();
207 }
208 }
209
210 // moveToThread() does not set a parent: the worker must be deleted explicitly
211 // after the thread has stopped (Qt threading best practice).
212 if (physicsWorker_) {
213 physicsWorker_->cleanup();
214 delete physicsWorker_;
215 }
216}
217
218/**
219 * \brief Performs cleanup actions, such as printing profiling reports.
220 */
222 using CleanupClock = std::chrono::high_resolution_clock;
223 auto phaseStart = CleanupClock::now();
224
225 if (cleanupCalled_) {
226 return;
227 }
228 cleanupCalled_ = true;
229
230 phaseStart = CleanupClock::now();
231 shutdownPhysicsThread();
233 std::chrono::duration<double>(CleanupClock::now() - phaseStart)
234 .count());
238}
239
240void Window::Display::updateVectorArrow(Qt3DCore::QTransform* arrowTransform,
241 const Model::Vector3d& vector,
242 double scaleFactor) const {
243 if (!arrowTransform) {
244 return;
245 }
246
247 const double magnitudeSq = vector.squaredNorm();
248 if (magnitudeSq < Model::EPSILON * Model::EPSILON) {
249 static_cast<Qt3DCore::QEntity*>(arrowTransform->parent())
250 ->setEnabled(false);
251 return;
252 }
253
254 static_cast<Qt3DCore::QEntity*>(arrowTransform->parent())->setEnabled(true);
255
256 // The arrow is created along the Z-axis. We need to find the rotation
257 // that aligns the Z-axis with our target vector.
258 const QVector3D zAxis(0.0f, 0.0f, 1.0f);
259 const QVector3D targetVector(static_cast<float>(vector.x()),
260 static_cast<float>(vector.y()),
261 static_cast<float>(vector.z()));
262
263 const QQuaternion rotation =
264 QQuaternion::rotationTo(zAxis, targetVector.normalized());
265
266 arrowTransform->setRotation(rotation);
267 arrowTransform->setScale3D(QVector3D(
268 1.0f, 1.0f, static_cast<float>(std::sqrt(magnitudeSq) * scaleFactor)));
269}
270
271void Window::Display::updateFrame(const Model::Vector3dVec& positions,
272 const Model::QuaterniondVec& quaternions,
273 const Model::Vector3dVec& torques,
274 const Model::Vector3dVec& alphas, // NOLINT
275 std::size_t iteration) {
277 /* Update each body's transform based on the new state. */
278 for (std::size_t i = 0; i < bodies_.size(); ++i) {
279 auto& transform = std::get<1>(bodies_[i]);
280 if (transform) {
281 transform->setTranslation({static_cast<float>(positions[i].x()),
282 static_cast<float>(positions[i].y()),
283 static_cast<float>(positions[i].z())});
284 transform->setRotation({static_cast<float>(quaternions[i].w()),
285 static_cast<float>(quaternions[i].x()),
286 static_cast<float>(quaternions[i].y()),
287 static_cast<float>(quaternions[i].z())});
288 }
289
290 if (showTorqueArrow_) {
291 updateVectorArrow(std::get<3>(bodies_[i]), torques[i],
292 Model::TORQUE_ARROW_SCALE);
293 } else {
294 std::get<2>(bodies_[i])->setEnabled(false);
295 }
296
297 if (showAlphaArrow_) {
298 updateVectorArrow(std::get<5>(bodies_[i]), alphas[i],
299 Model::ALPHA_ARROW_SCALE);
300 } else {
301 std::get<4>(bodies_[i])->setEnabled(false);
302 }
303 }
304
305 iterationCount_ = iteration + 1;
306 // The simulation timer automatically drives the next physics step.
307 // No need to schedule manually - the timer will trigger performSingleStep()
308 // as soon as the event loop is free (0ms interval).
310}
311
312// Include the MOC-generated file for this class.
313// This ensures the compiler sees the definitions for signals and slots.
314#include "moc_display.cpp"
Global application profiling instrumentation.
static void addCleanupDisplayProcessEvents(double seconds)
Adds measured time spent in Display cleanup processEvents.
static void addCleanupDisplayPrep(double seconds)
Adds measured time for Display cleanup preparation.
static void addCleanupDisplayWorkerCleanup(double seconds)
Adds measured time for physics worker cleanup.
static void stopFrameRender()
Stops timing a frame render.
static void startFrameRender()
Starts timing a frame render.
static void addCleanupDisplayThreadStop(double seconds)
Adds measured time for stopping the physics thread.
void updatedBodyData(const Vector3dVec &positions, const QuaterniondVec &quaternions, const Vector3dVec &torques, const Vector3dVec &alphas, std::size_t iteration)
Emitted after each simulation step with the updated state of all bodies.
void startSimulation()
Starts the simulation loop.
void performSingleStep()
Performs a single step of the physics simulation and emits the results.
void simulationFinished()
Emitted when the simulation has completed all iterations.
A 32-bit Permuted Congruential Generator (pcg32).
Definition: pcg_random.hpp:37
void cleanup()
Performs cleanup actions, such as printing profiling reports.
Definition: display.cpp:221
void createSpheres(Rng::Pcg32 &gen)
Create and set up the spherical bodies in the scene.
Definition: display.cpp:94
std::pair< Qt3DCore::QEntity *, Qt3DCore::QTransform * > createArrowEntity(Qt3DCore::QEntity *parent, const QColor &colour, float radius, float length)
Creates a 3D arrow entity for vector visualization.
Definition: display.cpp:48
~Display() override
Destructor to ensure worker thread is cleaned up.
Definition: display.cpp:43
void simulationFinished()
Emitted when the simulation has run for n_iter iterations.
void updateFrame(const Model::Vector3dVec &positions, const Model::QuaterniondVec &quaternions, const Model::Vector3dVec &torques, const Model::Vector3dVec &alphas, std::size_t iteration)
Slot to receive updated data from the physics worker and update the scene.
Definition: display.cpp:271
void createScene(unsigned int seed)
Set up the scene.
Definition: display.cpp:150
void runSimulation()
Starts the physics simulation by starting the worker thread.
Definition: display.cpp:160
void shutdownPhysicsThread()
Stops the physics thread and destroys the worker.
Definition: display.cpp:170
void updateVectorArrow(Qt3DCore::QTransform *arrowTransform, const Model::Vector3d &vector, double scaleFactor) const
Updates the transform of an arrow entity to represent a 3D vector.
Definition: display.cpp:240
Displaying spherical moving bodies.
A minimal C++ implementation of the PCG32 random number generator.
N-body simulation space with gravitational interaction and collision response.