1/****************************************************************************
2**
3** Copyright (C) 2020 Klaralvdalens Datakonsult AB (KDAB).
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt3D module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "calcboundingvolumejob_p.h"
41
42#include <Qt3DCore/qattribute.h>
43#include <Qt3DCore/qboundingvolume.h>
44#include <Qt3DCore/qbuffer.h>
45#include <Qt3DCore/qgeometryview.h>
46#include <Qt3DCore/qcoreaspect.h>
47#include <Qt3DCore/private/job_common_p.h>
48#include <Qt3DCore/private/qcoreaspect_p.h>
49#include <Qt3DCore/private/qaspectjob_p.h>
50#include <Qt3DCore/private/qaspectmanager_p.h>
51#include <Qt3DCore/private/qattribute_p.h>
52#include <Qt3DCore/private/qboundingvolume_p.h>
53#include <Qt3DCore/private/qbuffer_p.h>
54#include <Qt3DCore/private/qentity_p.h>
55#include <Qt3DCore/private/qgeometry_p.h>
56#include <Qt3DCore/private/qgeometryview_p.h>
57#include <Qt3DCore/private/qnodevisitor_p.h>
58#include <Qt3DCore/private/qthreadpooler_p.h>
59
60#include <QtCore/qmath.h>
61#if QT_CONFIG(concurrent)
62#include <QtConcurrent/QtConcurrent>
63#endif
64
65QT_BEGIN_NAMESPACE
66
67namespace Qt3DCore {
68
69namespace {
70
71BoundingVolumeComputeData findBoundingVolumeComputeData(QGeometryView *node)
72{
73 if (!node->isEnabled())
74 return {};
75
76 if (node->primitiveType() == QGeometryView::Patches)
77 return {};
78
79 QGeometry *geom = node->geometry();
80 QGeometryPrivate *dgeom = QGeometryPrivate::get(geom);
81 if (!geom)
82 return {};
83
84 int drawVertexCount = node->vertexCount(); // may be 0, gets changed below if so
85
86 QAttribute *positionAttribute = dgeom->m_boundingVolumePositionAttribute;
87 const QList<Qt3DCore::QAttribute *> attributes = geom->attributes();
88
89 // Use the default position attribute if attribute is null
90 if (!positionAttribute) {
91 for (QAttribute *attr : attributes) {
92 if (attr->name() == QAttribute::defaultPositionAttributeName()) {
93 positionAttribute = attr;
94 break;
95 }
96 }
97 }
98
99 if (!positionAttribute
100 || positionAttribute->attributeType() != QAttribute::VertexAttribute
101 || positionAttribute->vertexBaseType() != QAttribute::Float
102 || positionAttribute->vertexSize() < 3) {
103 qWarning("findBoundingVolumeComputeData: Position attribute not suited for bounding volume computation");
104 return {};
105 }
106
107 Qt3DCore::QBuffer *positionBuffer = positionAttribute->buffer();
108 // No point in continuing if the positionAttribute doesn't have a suitable buffer
109 if (!positionBuffer) {
110 qWarning("findBoundingVolumeComputeData: Position attribute not referencing a valid buffer");
111 return {};
112 }
113
114 // Check if there is an index attribute.
115 QAttribute *indexAttribute = nullptr;
116 Qt3DCore::QBuffer *indexBuffer = nullptr;
117
118 for (const auto attr : attributes) {
119 if (attr->attributeType() == QAttribute::IndexAttribute) {
120 indexBuffer = attr->buffer();
121 if (indexBuffer) {
122 indexAttribute = attr;
123
124 if (!drawVertexCount)
125 drawVertexCount = static_cast<int>(indexAttribute->count());
126
127 static const QAttribute::VertexBaseType validIndexTypes[] = {
128 QAttribute::UnsignedShort,
129 QAttribute::UnsignedInt,
130 QAttribute::UnsignedByte
131 };
132
133 if (std::find(std::begin(validIndexTypes),
134 std::end(validIndexTypes),
135 indexAttribute->vertexBaseType()) == std::end(validIndexTypes)) {
136 qWarning() << "findBoundingVolumeComputeData: Unsupported index attribute type" << indexAttribute->name() << indexAttribute->vertexBaseType();
137 return {};
138 }
139
140 break;
141 }
142 }
143 }
144
145 if (!indexAttribute && !drawVertexCount)
146 drawVertexCount = static_cast<int>(positionAttribute->count());
147
148 return { nullptr, nullptr, positionAttribute, indexAttribute, drawVertexCount };
149}
150
151bool isTreeEnabled(QEntity *entity) {
152 if (!entity->isEnabled())
153 return false;
154
155 QEntity *parent = entity->parentEntity();
156 while (parent) {
157 if (!parent->isEnabled())
158 return false;
159 parent = parent->parentEntity();
160 }
161
162 return true;
163}
164
165struct UpdateBoundFunctor
166{
167 // This define is required to work with QtConcurrent
168 typedef std::vector<BoundingVolumeComputeResult> result_type;
169 result_type operator ()(const BoundingVolumeComputeData &data)
170 {
171 return { data.compute() };
172 }
173};
174
175struct ReduceUpdateBoundFunctor
176{
177 void operator ()(std::vector<BoundingVolumeComputeResult> &result, const std::vector<BoundingVolumeComputeResult> &values)
178 {
179 result.insert(result.end(),
180 std::make_move_iterator(values.begin()),
181 std::make_move_iterator(values.end()));
182 }
183};
184
185} // anonymous
186
187
188BoundingVolumeComputeData BoundingVolumeComputeData::fromView(QGeometryView *view)
189{
190 return findBoundingVolumeComputeData(view);
191}
192
193BoundingVolumeComputeResult BoundingVolumeComputeData::compute() const
194{
195 BoundingVolumeCalculator calculator;
196 if (calculator.apply(positionAttribute, indexAttribute, vertexCount,
197 provider->view()->primitiveRestartEnabled(),
198 provider->view()->restartIndexValue()))
199 return {
200 entity, provider, positionAttribute, indexAttribute,
201 calculator.min(), calculator.max(),
202 calculator.center(), calculator.radius()
203 };
204 return {};
205}
206
207
208CalculateBoundingVolumeJob::CalculateBoundingVolumeJob(QCoreAspect *aspect)
209 : Qt3DCore::QAspectJob()
210 , m_aspect(aspect)
211 , m_root(nullptr)
212{
213 SET_JOB_RUN_STAT_TYPE(this, JobTypes::CalcBoundingVolume, 0)
214}
215
216bool CalculateBoundingVolumeJob::isRequired()
217{
218 if (!m_aspect)
219 return true;
220
221 auto daspect = QCoreAspectPrivate::get(m_aspect);
222 return daspect->m_boundingVolumesEnabled;
223}
224
225void CalculateBoundingVolumeJob::run()
226{
227 // There's 2 bounding volume jobs, one here in Core, the other in Render.
228 // - This one computes bounding volumes for entities that have QBoundingVolume
229 // components and use QGeometryViews.
230 // In that case the component is updated directly by this job (since core
231 // aspect does not maintain backend objects for the component)
232 // - The one in render does 2 things:
233 // . Copy the results of this job to the backend object for entities that
234 // use QBoundingVolume and QGeometryView (computed results arrive one
235 // frame later, explicit results arrive on time)
236 // . Compute the BV for old style QGeometryRenderer which use a QGeometry
237 // directly without a QGeometryView
238 //
239 // (see more details in Qt3DRender::CalculateBoundingVolumeJob::run)
240
241 m_results.clear();
242
243 QHash<QEntity *, BoundingVolumeComputeData> dirtyEntities;
244 QNodeVisitor visitor;
245 visitor.traverse(m_root, [](QNode *) {}, [&dirtyEntities](QEntity *entity) {
246 if (!isTreeEnabled(entity))
247 return;
248
249 const auto bvProviders = entity->componentsOfType<QBoundingVolume>();
250 if (bvProviders.isEmpty())
251 return;
252
253 // we go through the list until be find a dirty provider,
254 // or THE primary provider
255 bool foundBV = false;
256 for (auto bv: bvProviders) {
257 auto dbv = QBoundingVolumePrivate::get(bv);
258 if (foundBV && !dbv->m_primaryProvider)
259 continue;
260
261 BoundingVolumeComputeData bvdata;
262 if (!dbv->m_explicitPointsValid && bv->view()) {
263 bvdata = findBoundingVolumeComputeData(bv->view());
264 if (!bvdata.valid())
265 continue;
266 bvdata.entity = entity;
267 bvdata.provider = bv;
268 } else {
269 // bounds are explicitly set, don't bother computing
270 // or no view, can't compute
271 continue;
272 }
273
274 bool dirty = QEntityPrivate::get(entity)->m_dirty;
275 dirty |= QGeometryViewPrivate::get(bv->view())->m_dirty;
276 dirty |= QGeometryPrivate::get(bv->view()->geometry())->m_dirty;
277 dirty |= QAttributePrivate::get(bvdata.positionAttribute)->m_dirty;
278 dirty |= QBufferPrivate::get(bvdata.positionAttribute->buffer())->m_dirty;
279 if (bvdata.indexAttribute) {
280 dirty |= QAttributePrivate::get(bvdata.indexAttribute)->m_dirty;
281 dirty |= QBufferPrivate::get(bvdata.indexAttribute->buffer())->m_dirty;
282 }
283
284 if (dbv->m_primaryProvider) {
285 if (dirty)
286 dirtyEntities[entity] = bvdata;
287 break;
288 } else if (dirty) {
289 dirtyEntities[entity] = bvdata;
290 foundBV = true;
291 }
292 }
293 });
294
295#if QT_CONFIG(concurrent)
296 if (dirtyEntities.size() > 1 && QAspectJobManager::idealThreadCount() > 1) {
297 UpdateBoundFunctor functor;
298 ReduceUpdateBoundFunctor reduceFunctor;
299 m_results = QtConcurrent::blockingMappedReduced<decltype(m_results)>(dirtyEntities, functor, reduceFunctor);
300 } else
301#endif
302 {
303 for (auto it = dirtyEntities.begin(); it != dirtyEntities.end(); ++it) {
304 auto res = it.value().compute();
305 if (res.valid())
306 m_results.push_back(res); // How do we push it to the backends????
307 }
308 }
309
310 // This is needed so that the matching job in the render aspect gets the right data.
311 // It is currently safe since the main thread is locked, there's no other
312 // core aspect jobs in existence, and other aspect jobs only access backend data.
313 // TODO: find a way for aspects to pass around data, or create groups of jobs
314 // that need to run first, sync, then process next group.
315 postFrame(nullptr);
316}
317
318void CalculateBoundingVolumeJob::postFrame(QAspectEngine *aspectEngine)
319{
320 Q_UNUSED(aspectEngine);
321
322 for (auto result: m_results) {
323 // set the results
324 QBoundingVolumePrivate::get(result.provider)->setImplicitBounds(result.m_min, result.m_max, result.m_center, result.m_radius);
325
326 // reset dirty flags
327 QEntityPrivate::get(result.entity)->m_dirty = false;
328 QGeometryViewPrivate::get(result.provider->view())->m_dirty = false;
329 QGeometryPrivate::get(result.provider->view()->geometry())->m_dirty = false;
330 QAttributePrivate::get(result.positionAttribute)->m_dirty = false;
331 QBufferPrivate::get(result.positionAttribute->buffer())->m_dirty = false;
332 if (result.indexAttribute) {
333 QAttributePrivate::get(result.indexAttribute)->m_dirty = false;
334 QBufferPrivate::get(result.indexAttribute->buffer())->m_dirty = false;
335 }
336 }
337
338 m_results.clear();
339}
340
341} // namespace Qt3DCore
342
343QT_END_NAMESPACE
344
345