I. Création des nœuds de calcul▲
L'une des meilleures choses avec Qt 3D est sa capacité de rendu très élevée. Elle est atteinte en exécutant des tâches dans la pile d'exécution en arrière-plan. Pour y arriver sans avoir des entrelacs de points de synchronisation (ce qui limiterait les possibilités d'exécution parallèle), nous allons devoir sacrifier de la mémoire au profit de la vitesse, compromis classique en informatique. Si chaque aspect fonctionne sur sa propre copie de donnée, les données de chaque tâche sont protégées, ce qui évite une situation de compétition (situation qui survient lorsque différents objets tentent d'accéder à une même ressource en même temps).
Cela n'est pas aussi coûteux qu'il n'y paraît, car le nœud de calcul n'hérite pas de QObject : sa classe de base est Qt3DCore::QBackendNode, qui est beaucoup plus légère. Cela est dû au fait que les aspects enregistrent seulement les données dont ils ont besoin pour leurs calculs. Par exemple, si une même entité possède à la fois un aspect d'animation et un composant Material, l'animation n'enregistrera aucune donnée provenant du composant. Réciproquement, l'aspect de rendu ne s'intéresse pas aux clips ou aux composants d'animation.
Dans notre petit aspect, nous avons seulement un des types de composants de premier plan : FpsMonitor. Logiquement, nous aurons un seul type d'arrière-plan correspondant, que nous pouvons logiquement appeler FpsMonitorBackend :
class
FpsMonitorBackend : public
Qt3DCore
::
QBackendNode
{
public
:
FpsMonitorBackend()
:
Qt3DCore
::
QBackendNode
(Qt3DCore
::
QBackendNode
::
ReadWrite)
, m_rollingMeanFrameCount(5
)
{}
private
:
void
initializeFromPeer(const
Qt3DCore
::
QNodeCreatedChangeBasePtr
&
change) override
{
// TODO: Implement me!
}
int
m_rollingMeanFrameCount;
}
;
La déclaration de cette classe est très simple. Comme prévu, nous héritons de Qt3DCore::QBackendNode, puis nous ajoutons une variable membre pour garder une copie des informations depuis le composant de premier plan FpsMonitor. Enfin, nous réécrivons la fonction virtuelle initializeFromPeer(). Elle sera appelée après que Qt 3D ait créé une instance de notre type d'arrière-plan. L'argument nous permet de récupérer les données envoyées depuis l'objet de premier plan correspondant, comme nous avions vu précédemment.
II. Enregistrer les types auprès de l'aspect▲
Nous savons maintenant implémenter des composants d'arrière-plan et de premier plan. La prochaine étape est de les enregistrer auprès de l'aspect : ainsi, ce dernier peut instancier le nœud de calcul à chaque fois qu'un nœud de premier plan est créé, de même pour la destruction. Nous faisons cela en passant par un intermédiaire connu comme un mapper de nœuds.
Pour créer un mapper de nœuds, il suffit d'écrire une sous-classe de Qt3DCore::QNodeMapper et de réécrire les fonctions virtuelles de création, de consultation et de destruction d'objets lorsqu'il y a lieu. La manière dont vous créerez, consulterez et détruirez les objets dépend uniquement de vous. Qt 3D n'impose aucun schéma d'organisation particulier. L'aspect qui gère le rendu fait des choses plutôt absconses avec un gestionnaire de mémoire par seaux et l'alignement de la mémoire pour les types SIMD, mais nous pouvons faire ici quelque chose de beaucoup plus simple.
Nous allons stocker des pointeurs vers les nœuds d'arrière-plan dans une table de hachage QHash avec la classe CustomAspect et les indexer par l'identifiant de chaque nœud Qt3DCore::QNodeId. L'identifiant du nœud est utilisé pour identifier un nœud donné de manière unique, même entre le premier plan et tous les aspects d'arrière-plan. Dans Qt3DCore::QNode, l'identifiant est disponible avec la fonction id(), tandis que, dans QBackendNode, ce dernier est donné par la fonction peerId(). Pour les deux objets correspondants qui représentent le composant, les fonctions id() et peerId() renvoient la même valeur QNodeId.
Il n'y a plus qu'à écrire le code ! Ajoutons un peu de stockage aux nœuds d'arrière-plan pour la classe CustomAspect.
class
CustomAspect : public
Qt3DCore
::
QAbstractAspect
{
Q_OBJECT
public
:
...
void
addFpsMonitor(Qt3DCore
::
QNodeId
id, FpsMonitorBackend *
fpsMonitor)
{
m_fpsMonitors.insert(id, fpsMonitor);
}
FpsMonitorBackend *
fpsMonitor(Qt3DCore
::
QNodeId
id)
{
return
m_fpsMonitors.value(id, nullptr
);
}
FpsMonitorBackend *
takeFpsMonitor(Qt3DCore
::
QNodeId
id)
{
return
m_fpsMonitors.take(id);
}
...
private
:
QHash
<
Qt3DCore
::
QNodeId
, FpsMonitorBackend *>
m_fpsMonitors;
}
;
Maintenant, nous pouvons tout simplement implémenter un mapper de nœuds dans cette classe :
class
FpsMonitorMapper : public
Qt3DCore
::
QBackendNodeMapper
{
public
:
explicit
FpsMonitorMapper(CustomAspect *
aspect);
Qt3DCore
::
QBackendNode
*
create(const
Qt3DCore
::
QNodeCreatedChangeBasePtr
&
change) const
override
{
auto
fpsMonitor =
new
FpsMonitorBackend;
m_aspect->
addFpsMonitor(change->
subjectId(), fpsMonitor);
return
fpsMonitor;
}
Qt3DCore
::
QBackendNode
*
get(Qt3DCore
::
QNodeId
id) const
override
{
return
m_aspect->
fpsMonitor(id);
}
void
destroy(Qt3DCore
::
QNodeId
id) const
override
{
auto
fpsMonitor =
m_aspect->
takeFpsMonitor(id);
delete
fpsMonitor;
}
private
:
CustomAspect *
m_aspect;
}
;
Pour poser la dernière pièce du puzzle, nous devons maintenant indiquer à l'aspect comment ces types et ces mappers fonctionnent entre eux. Nous effectuons cela en appelant la fonction QAbstractAspect::registerBackendType(), que l'on passe dans un pointeur commun au mapper qui créera, trouvera et détruira le nœud d'arrière-plan correspondant au moment voulu. L'argument de la fonction est le type du nœud de premier plan pour lequel le mapper devrait être appelé. L'endroit approprié pour faire cela est tout simplement le constructeur de la classe CustomAspect. Dans notre cas, il ressemble à ça :
CustomAspect::
CustomAspect(QObject
*
parent)
:
Qt3DCore
::
QAbstractAspect
(parent)
{
// Register the mapper to handle creation, lookup, and destruction of backend nodes
auto
mapper =
QSharedPointer
<
FpsMonitorMapper>
::
create(this
);
registerBackendType<
FpsMonitor>
(mapper);
}
Et c'est tout ! Avec cette déclaration, à chaque fois qu'un composant FpsMonitor est ajouté au premier plan (cette scène), l'aspect cherchera le mapper pour ce type d'objet. Ici, il trouvera notre objet enregistré FpsMonitorMapper et il appellera la fonction create() pour créer le nœud d'arrière-plan correspondant et gérer sa mémoire. Même histoire pour la destruction (techniquement, la suppression du composant de la scène) du nœud de premier plan. La fonction get() du mapper est utilisée en interne pour appeler les fonctions virtuelles sur le nœud d'arrière-plan au moment approprié (par exemple, lorsque les propriétés informent qu'elles ont été changées).
III. Communications entre le premier plan et l'arrière-plan▲
Maintenant, nous pouvons créer, accéder et détruire le nœud d'arrière-plan depuis n'importe quel nœud de premier plan. Allons voir comment nous pouvons les faire parler entre eux. Il y a trois étapes pour effectuer cela :
- Initialisation – lorsque notre nœud d'arrière-plan est créé, nous avons la possibilité de l'initialiser avec les données envoyées depuis le nœud de premier plan ;
- Premier plan vers arrière-plan – lorsque les propriétés du nœud de premier plan changent, il peut envoyer les nouvelles valeurs au nœud d'arrière-plan pour qu'il se mette à jour ;
- Arrière-plan vers premier plan – lorsque nos tâches traitent les données stockées dans les nœuds d'arrière-plan, il en résulte parfois une valeur de retour qui devra être envoyée au nœud de premier plan.
Voici la réponse aux deux premières étapes ; quant à la troisième, nous verrons cela dans le prochain article lorsque nous introduirons les tâches.
IV. Initialisation du nœud d'arrière-plan▲
Toutes les communications entre le premier plan et les objets d'arrière-plan fonctionnent en envoyant des sous-classes de Qt3DCore::QSceneChanges. Leur nature et leur concept sont similaires à QEvent, mais l'arbitre des changements a la possibilité de les manipuler en cas de conflits entre plusieurs aspects, il peut les remettre dans l'ordre selon leur priorité ou effectuer toutes les autres manipulations qui seraient requises plus tard.
Dans le but d'initialiser le nœud d'arrière-plan au moment de la création, nous utilisons la classe Qt3DCore::QSceneChanges afin d'envelopper notre type spécifique de données. Quand Qt 3D veut informer l'arrière-plan de l'état initial du nœud de premier plan, il appelle la fonction privée QNode::createNodeCreationChange(). Cette fonction retourne un nœud contenant chaque information dont nous avons besoin pour accéder à notre nœud d'arrière-plan. Nous devons le faire en copiant les données plutôt qu'en déréférençant un pointeur de l'objet de premier plan, car, au moment où l'arrière-plan traite la demande, on risque une situation de compétition. Pour notre simple composant, son implémentation ressemblera à cela :
struct
FpsMonitorData
{
int
rollingMeanFrameCount;
}
;
Qt3DCore
::
QNodeCreatedChangeBasePtr
FpsMonitor::
createNodeCreationChange() const
{
auto
creationChange =
Qt3DCore
::
QNodeCreatedChangePtr
<
FpsMonitorData>
::
create(this
);
auto
&
data =
creationChange->
data;
data.rollingMeanFrameCount =
m_rollingMeanFrameCount;
return
creationChange;
}
Le changement initié par le nœud de premier plan est passé en paramètre au nœud de calcul (par le truchement de l'arbitre des changements) et est traité par la fonction virtuelle initializeFromPeer() :
void
FpsMonitorBackend::
initializeFromPeer(const
Qt3DCore
::
QNodeCreatedChangeBasePtr
&
change)
{
const
auto
typedChange =
qSharedPointerCast
<
Qt3DCore
::
QNodeCreatedChange
<
FpsMonitorData>>
(change);
const
auto
&
data =
typedChange->
data;
m_rollingMeanFrameCount =
data.rollingMeanFrameCount;
}
V. Communication du premier plan à l'arrière-plan▲
À ce stade, les nœuds d'arrière-plan renvoient l'état initial du nœud de premier plan. Que se passerait-il si l'utilisateur changeait une propriété du nœud de premier plan ? Les données de notre nœud d'arrière-plan seraient alors périmées.
La bonne nouvelle est que cette situation est facile à gérer. L'implémentation de Qt3DCore::QNode prend soin du moindre problème pour nous. Intérieurement, il écoute les signaux de notification Q_PROPERTY et, quand il voit qu'une propriété a changé, il crée un objet QPropertyUpdatedChange pour nous et l'envoie à l'arbitre des changements qui, à son tour, le délivre à la fonction sceneChangeEvent() du nœud d'arrière-plan.
Ainsi, notre rôle en tant que créateur de ce nœud d'arrière-plan se limite à déclarer cette fonction, d'extraire les données depuis l'objet de changement et de mettre à jour notre situation interne. Souvent, vous voudrez alors définir le nœud d'arrière-plan comme invalide : c'est comme cela que l'aspect sait qu'il doit traiter le prochain rafraîchissement. Cependant, nous allons juste mettre à jour son état pour renvoyer la dernière valeur au premier plan.
void
FpsMonitorBackend::
sceneChangeEvent(const
Qt3DCore
::
QSceneChangePtr
&
e)
{
if
(e->
type() ==
Qt3DCore
::
PropertyUpdated) {
const
auto
change =
qSharedPointerCast
<
Qt3DCore
::
QPropertyUpdatedChange
>
(e);
if
(change->
propertyName() ==
QByteArrayLiteral
("rollingMeanFrameCount"
)) {
const
auto
newValue =
change->
value().toInt();
if
(newValue !=
m_rollingMeanFrameCount) {
m_rollingMeanFrameCount =
newValue;
// TODO: Update fps calculations
}
return
;
}
}
QBackendNode
::
sceneChangeEvent(e);
}
Si vous ne voulez pas utiliser l'envoi automatique des changements de propriété intégré de Qt3DCore::QNode, vous pouvez le désactiver en enveloppant la propriété du signal d'émission avec un appel de QNode::blockNotifications(). Cela fonctionne exactement de la même manière que QObject::blockSignals(), mis à part que seuls les blocs de notification sont envoyés au nœud d'arrière-plan, pas le signal lui-même. Cela veut dire que d'autres connexions ou propriétés liées qui comptent sur vos signaux continueront de fonctionner.
Si vous bloquez les notifications par défaut, vous devrez envoyer votre propre notification pour garantir que le nœud d'arrière-plan a mis à jour l'information. Libre à vous d'utiliser les sous-classes dans la hiérarchie de Qt3DCore::QSceneChange et de les ployer à vos besoins. Une méthode commune est d'utiliser la sous-classe Qt3DCore::QStaticPropertyUpdatedChangeBase, qui gère le nom de la propriété et qui, de plus, ajoute un membre fortement typé pour la charge utile de la propriété. L'avantage de surplomber le mécanisme intégré est que cela évite l'usage de QVariant qui, lorsque qu'il y a trop de tâches parallèles, est ralenti - même si, généralement, la propriété d'arrière-plan ne change pas trop souvent.
VI. Résumé▲
Dans cet article, nous vous avons montré comment implémenter la plupart des nœuds d'arrière-plan ; comment enregistrer le mapper de nœuds avec l'aspect pour créer, consulter et détruire les nœuds d'arrière-plan ; comment initialiser le nœud d'arrière-plan depuis le nœud de premier plan de manière sûre et aussi comment synchroniser ses données avec le premier plan.
Dans le prochain article, nous achèverons notre aspect — il effectuera réellement son travail — et apprendrons comment récupérer les nœuds d'arrière-plan pour envoyer des mises à jour au nœud de premier plan (la valeur moyenne des FPS). Nous nous assurerons que la partie lourde des calculs soit exécutée par le groupe de fils d'exécution de Qt 3D, pour que vous puissiez avoir une idée de son ampleur. À la prochaine fois !
VII. Remerciements▲
Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à KDAB pour nous avoir autorisés à traduire cet article !
Je tiens à remercier Thibaut Cuvelier et Alexandre Laurent pour leurs conseils et relectures.