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
44 // Ensure the thread is properly shut down before Qt destroys it.
45 // The thread was created with 'this' as parent, so Qt will destroy it
46 // automatically when Display is destroyed. However, we must ensure it's
47 // stopped first to avoid issues.
48 if (physicsThread_ && physicsThread_->isRunning()) {
49 physicsThread_->quit();
50 physicsThread_->wait(); // Wait for the thread to finish.
51 }
52
53 // No explicit deletion needed:
54 // - physicsThread_ is destroyed by Qt (parent = this)
55 // - physicsWorker_ is destroyed by Qt (parent = thread after moveToThread)
56 // - QPointer automatically becomes nullptr when the object is destroyed
57}
58
59std::pair<Qt3DCore::QEntity*, Qt3DCore::QTransform*>
60Window::Display::createArrowEntity(Qt3DCore::QEntity* parent,
61 const QColor& colour, float radius,
62 float length) {
63 auto* arrowEntity = new Qt3DCore::QEntity(parent);
64 auto* arrowTransform = new Qt3DCore::QTransform(arrowEntity);
65
66 // --- Material ---
67 auto* material = new Qt3DExtras::QDiffuseSpecularMaterial(arrowEntity);
68 material->setAmbient(colour);
69
70 // --- Shaft (Cylinder) ---
71 auto* shaftEntity = new Qt3DCore::QEntity(arrowEntity);
72 auto* shaftMesh = new Qt3DExtras::QCylinderMesh();
73 shaftMesh->setRadius(radius);
74 shaftMesh->setLength(length);
75 auto* shaftTransform = new Qt3DCore::QTransform();
76 // The cylinder is oriented along the Y-axis by default. We rotate it to
77 // align with Z.
78 shaftTransform->setRotationX(90);
79 shaftEntity->addComponent(shaftMesh);
80 shaftEntity->addComponent(shaftTransform);
81 shaftEntity->addComponent(material);
82
83 // --- Head (Cone) ---
84 auto* headEntity = new Qt3DCore::QEntity(arrowEntity);
85 auto* headMesh = new Qt3DExtras::QConeMesh();
86 headMesh->setTopRadius(0);
87 headMesh->setBottomRadius(radius * 2.5f);
88 headMesh->setLength(radius * 5.0f);
89 auto* headTransform = new Qt3DCore::QTransform();
90 headTransform->setTranslation(QVector3D(0, 0, length));
91 headTransform->setRotationX(90);
92 headEntity->addComponent(headMesh);
93 headEntity->addComponent(headTransform);
94 headEntity->addComponent(material);
95
96 // The arrow entity itself will be scaled and rotated to match the vector.
97 arrowEntity->addComponent(arrowTransform);
98 arrowEntity->setEnabled(false); // Initially hidden
99
100 return {arrowEntity, arrowTransform};
101}
102
103/**
104 * \brief Create and set up the spherical bodies in the scene.
105 */
107 // Hue in [0, 1) for vibrant, distinct colours (HSV, aligned with Python).
108 std::uniform_real_distribution<> hueDis(0.0, 1.0);
109
110 bodies_.resize(physicsWorker_->getInitialBodies().size());
111
112 for (std::size_t i = 0; i < bodies_.size(); ++i) {
113 // Create an entity for each body. It is parented to the root entity,
114 // so its lifetime is managed by the root.
115 auto* entity = new Qt3DCore::QEntity(rootEntity_.get());
116
117 // Mesh (the shape). Parented to the entity.
118 auto* mesh = new Qt3DExtras::QSphereMesh(entity); // NOLINT
119 mesh->setRadius(physicsWorker_->getInitialBodies()[i].r());
120
121 // Transform (position, rotation, scale). Parented to the entity.
122 auto* transform = new Qt3DCore::QTransform(entity); // NOLINT
123
124 // Create a unique material for each sphere (HSV: saturation 0.9, value
125 // 0.95).
126 const QColor randomColour = QColor::fromHsvF(hueDis(gen), 0.9, 0.95);
127 auto* colouredMaterial = // NOLINT
128 new Qt3DExtras::QDiffuseSpecularMaterial(entity);
129
130 // Attach components to the entity *before* configuring them. This
131 // ensures Qt's backend fully owns the components before their
132 // properties are modified, which can prevent race conditions with the
133 // render thread.
134 entity->addComponent(mesh);
135 entity->addComponent(transform);
136 entity->addComponent(colouredMaterial);
137
138 // Now that the component is part of the scene graph, configure it.
139 colouredMaterial->setDiffuse(randomColour);
140 colouredMaterial->setShininess(200);
141 colouredMaterial->setAmbient(randomColour.darker(110));
142 // Save every new sphere to a body list with its parameters.
143
144 // Create visualisers for torque and angular acceleration
145 auto [torqueArrowEntity, torqueArrowTransform] =
146 createArrowEntity(entity, QColor("magenta"), 0.5f, 10.0f);
147 auto [alphaArrowEntity, alphaArrowTransform] =
148 createArrowEntity(entity, QColor("cyan"), 0.5f, 10.0f);
149
150 bodies_[i] = std::make_tuple(
151 QPointer<Qt3DCore::QEntity>(entity), QPointer(transform),
152 QPointer<Qt3DCore::QEntity>(torqueArrowEntity),
153 QPointer<Qt3DCore::QTransform>(torqueArrowTransform),
154 QPointer<Qt3DCore::QEntity>(alphaArrowEntity),
155 QPointer<Qt3DCore::QTransform>(alphaArrowTransform));
156 }
157}
158
159/**
160 * \brief Set up the scene.
161 */
162void Window::Display::createScene(unsigned int seed) {
163 // Use std::random_device to get a non-deterministic seed if none is
164 // provided.
165 Rng::Pcg32 gen(seed == 0 ? std::random_device{}() : seed);
166 createSpheres(gen);
167}
168
169/**
170 * \brief Starts the physics simulation by starting the worker thread.
171 */
173 // Start the physics thread. The thread's `started` signal will trigger the
174 // first simulation step. The simulation timer will then take over to drive
175 // subsequent steps.
176 physicsThread_->start();
177 // Start the timer with 0ms interval (triggers as soon as possible)
178 // This timer will automatically call performSingleStep() repeatedly
179 simulationTimer_->start(0);
180}
181
182/**
183 * \brief Performs cleanup actions, such as printing profiling reports.
184 */
186 using CleanupClock = std::chrono::high_resolution_clock;
187 auto phaseStart = CleanupClock::now();
188
189 // Prevent cleanup from being called multiple times
190 if (cleanupCalled_) {
191 return;
192 }
193 cleanupCalled_ = true;
194
195 // CRITICAL: Stop the simulation timer FIRST to prevent new steps from being
196 // scheduled
197 if (simulationTimer_) {
198 simulationTimer_->stop();
199 }
200
201 // CRITICAL: Disconnect all signals FIRST to prevent updateFrame from being
202 // called This must be done before stopping the simulation to avoid race
203 // conditions We check the return value of disconnect() to handle cases
204 // where signals are already disconnected (more robust, similar to Python's
205 // try/except pattern)
206 if (physicsWorker_) {
207 // Disconnect updatedBodyData signal (ignore if already disconnected)
208 disconnect(physicsWorker_, &Model::PhysicsWorker::updatedBodyData, this,
210 // Disconnect simulationFinished signal (ignore if already disconnected)
211 disconnect(physicsWorker_, &Model::PhysicsWorker::simulationFinished,
213 // Disconnect simulationFinished from timer (if connected)
214 if (simulationTimer_) {
215 disconnect(physicsWorker_,
217 simulationTimer_, &QTimer::stop);
218 }
219 // Stop the simulation loop to prevent new steps from being scheduled
220 physicsWorker_->stopSimulation();
221 }
222
223 // Disconnect thread started signal (if connected)
224 if (physicsThread_) {
225 disconnect(physicsThread_, &QThread::started, physicsWorker_,
227 }
228
229 // Disconnect timer timeout signal (if connected)
230 if (simulationTimer_) {
231 disconnect(simulationTimer_, &QTimer::timeout, physicsWorker_,
233 }
235 std::chrono::duration<double>(CleanupClock::now() - phaseStart)
236 .count());
237
238 // Process any pending events to ensure all queued signals are handled
239 phaseStart = CleanupClock::now();
240 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
242 std::chrono::duration<double>(CleanupClock::now() - phaseStart)
243 .count());
244
245 // Stop the physics thread before cleanup
246 // This ensures the thread is stopped before the worker is cleaned up
247 phaseStart = CleanupClock::now();
248 if (physicsThread_ && physicsThread_->isRunning()) {
249 physicsThread_->quit();
250 physicsThread_->wait(
251 5000); // Wait up to 5 seconds for thread to finish
252 }
254 std::chrono::duration<double>(CleanupClock::now() - phaseStart)
255 .count());
256
257 // Now cleanup the worker (prints profiling report)
258 phaseStart = CleanupClock::now();
259 if (physicsWorker_) {
260 physicsWorker_->cleanup();
261 }
263 std::chrono::duration<double>(CleanupClock::now() - phaseStart)
264 .count());
265}
266
267void Window::Display::updateVectorArrow(Qt3DCore::QTransform* arrowTransform,
268 const Model::Vector3d& vector,
269 double scaleFactor) const {
270 if (!arrowTransform) {
271 return;
272 }
273
274 const double magnitudeSq = vector.squaredNorm();
275 if (magnitudeSq < Model::EPSILON * Model::EPSILON) {
276 static_cast<Qt3DCore::QEntity*>(arrowTransform->parent())
277 ->setEnabled(false);
278 return;
279 }
280
281 static_cast<Qt3DCore::QEntity*>(arrowTransform->parent())->setEnabled(true);
282
283 // The arrow is created along the Z-axis. We need to find the rotation
284 // that aligns the Z-axis with our target vector.
285 const QVector3D zAxis(0.0f, 0.0f, 1.0f);
286 const QVector3D targetVector(static_cast<float>(vector.x()),
287 static_cast<float>(vector.y()),
288 static_cast<float>(vector.z()));
289
290 const QQuaternion rotation =
291 QQuaternion::rotationTo(zAxis, targetVector.normalized());
292
293 arrowTransform->setRotation(rotation);
294 arrowTransform->setScale3D(QVector3D(
295 1.0f, 1.0f, static_cast<float>(std::sqrt(magnitudeSq) * scaleFactor)));
296}
297
298void Window::Display::updateFrame(const Model::Vector3dVec& positions,
299 const Model::QuaterniondVec& quaternions,
300 const Model::Vector3dVec& torques,
301 const Model::Vector3dVec& alphas, // NOLINT
302 std::size_t iteration) {
304 /* Update each body's transform based on the new state. */
305 for (std::size_t i = 0; i < bodies_.size(); ++i) {
306 auto& transform = std::get<1>(bodies_[i]);
307 if (transform) {
308 transform->setTranslation({static_cast<float>(positions[i].x()),
309 static_cast<float>(positions[i].y()),
310 static_cast<float>(positions[i].z())});
311 transform->setRotation({static_cast<float>(quaternions[i].w()),
312 static_cast<float>(quaternions[i].x()),
313 static_cast<float>(quaternions[i].y()),
314 static_cast<float>(quaternions[i].z())});
315 }
316
317 if (showTorqueArrow_) {
318 updateVectorArrow(std::get<3>(bodies_[i]), torques[i],
319 Model::TORQUE_ARROW_SCALE);
320 } else {
321 std::get<2>(bodies_[i])->setEnabled(false);
322 }
323
324 if (showAlphaArrow_) {
325 updateVectorArrow(std::get<5>(bodies_[i]), alphas[i],
326 Model::ALPHA_ARROW_SCALE);
327 } else {
328 std::get<4>(bodies_[i])->setEnabled(false);
329 }
330 }
331
332 iterationCount_ = iteration + 1;
333 // The simulation timer automatically drives the next physics step.
334 // No need to schedule manually - the timer will trigger performSingleStep()
335 // as soon as the event loop is free (0ms interval).
337}
338
339// Include the MOC-generated file for this class.
340// This ensures the compiler sees the definitions for signals and slots.
341#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:185
void createSpheres(Rng::Pcg32 &gen)
Create and set up the spherical bodies in the scene.
Definition: display.cpp:106
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:60
~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:298
void createScene(unsigned int seed)
Set up the scene.
Definition: display.cpp:162
QThread * physicsThread_
The thread where the physics worker runs. Created with 'this' as parent, so Qt manages its lifetime.
Definition: display.hpp:180
void runSimulation()
Starts the physics simulation by starting the worker thread.
Definition: display.cpp:172
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:267
Displaying spherical moving bodies.
A minimal C++ implementation of the PCG32 random number generator.
N-body simulation space with gravitational interaction and collision response.