PVS-Studio a jeté un coup d'œil au moteur de Red Dead Redemption - Bullet

Image 4

De nos jours, pour, par exemple, développer des jeux, il n'est plus nécessaire d'implémenter indépendamment la physique des objets à partir de zéro, car il existe un grand nombre de bibliothèques pour cela. Bullet était autrefois activement utilisé dans de nombreux jeux AAA, projets de réalité virtuelle, diverses simulations et apprentissage automatique. Oui, et il est toujours utilisé aujourd'hui, étant, par exemple, l'un des moteurs Red Dead Redemption et Red Dead Redemption 2. Alors pourquoi ne pas vérifier Bullet avec PVS-Studio pour voir quelles erreurs l'analyse statique peut détecter dans un projet à si grande échelle, liés à la simulation physique.

Cette bibliothèque est librement distribuée , afin que chacun puisse éventuellement l'utiliser dans ses projets. En plus de Red Dead Redemption, ce moteur physique est également utilisé dans l'industrie cinématographique pour créer des effets spéciaux. Par exemple, il a participé au tournage de Sherlock Holmes de Guy Ritchie pour calculer les collisions.

Si c'est votre première rencontre avec un article où PVS-Studio vérifie les projets, alors je vais faire une petite digression. PVS-Studio est un analyseur de code statique qui aide à trouver les erreurs, les lacunes et les vulnérabilités potentielles dans le code source des programmes C, C ++, C #, Java. L'analyse statique est une sorte de processus automatisé de révision de code.

Pour se réchauffer


Exemple 1:

Commençons par une erreur amusante:

V624 Il y a probablement une faute d' impression dans la constante '3.141592538'. Pensez à utiliser la constante M_PI de <math.h>. PhysicsClientC_API.cpp 4109

B3_SHARED_API void b3ComputeProjectionMatrixFOV(float fov, ....) { float yScale = 1.0 / tan((3.141592538 / 180.0) * fov / 2); .... } 

Une petite faute de frappe dans le nombre Pi (3.141592653 ...), manquant le nombre "6" à la 7ème position dans la partie fractionnaire.

Image 1
Peut-être que l'erreur à la dix millionième décimale n'entraînera pas de conséquences tangibles, mais vous devriez toujours utiliser les constantes de bibliothèque existantes sans fautes de frappe. Pour Pi, il existe une constante M_PI de l'en-tête math.h.

Copier coller


Exemple 2:

Parfois, l'analyseur vous permet de trouver l'erreur indirectement. Ainsi, par exemple, ici trois arguments liés halfExtentsX, halfExtentsY, halfExtentsZ sont passés à la fonction, mais ce dernier n'est utilisé nulle part dans la fonction. Vous pouvez remarquer que lors de l'appel de la méthode addVertex , la variable halfExtentsY est utilisée deux fois. C'est peut-être une erreur de copier-coller et ici un argument oublié devrait être utilisé.

V751 Le paramètre 'halfExtentsZ' n'est pas utilisé dans le corps de la fonction. TinyRenderer.cpp 375

 void TinyRenderObjectData::createCube(float halfExtentsX, float halfExtentsY, float halfExtentsZ, ....) { .... m_model->addVertex(halfExtentsX * cube_vertices_textured[i * 9], halfExtentsY * cube_vertices_textured[i * 9 + 1], halfExtentsY * cube_vertices_textured[i * 9 + 2], cube_vertices_textured[i * 9 + 4], ....); .... } 

Exemple 3:

L'analyseur a également trouvé le fragment intéressant suivant, je vais l'apporter d'abord dans sa forme originale.

Image 11

Vous voyez cette dernière ligne?

Image 12

Il est très étrange que le programmeur ait décidé d'écrire une condition aussi longue sur une seule ligne. Mais le fait qu'une erreur s'y soit probablement glissée n'est pas du tout surprenant.

L'analyseur a émis les avertissements suivants sur cette ligne.

V501 Il existe des sous-expressions identiques 'rotmat.Column1 (). Norm () <1.0001' à gauche et à droite de l'opérateur '&&'. LinearR4.cpp 351

V501 Il existe des sous-expressions identiques '0.9999 <rotmat.Column1 (). Norm ()' à gauche et à droite de l'opérateur '&&'. LinearR4.cpp 351

Si nous écrivons tout sous une forme «tabulaire» visuelle, il deviendra clair que les mêmes vérifications s'appliquent à Colonne1 . Les deux dernières comparaisons montrent qu'il y a Colonne1 et Colonne2 . Très probablement, les troisième et quatrième comparaisons auraient dû vérifier la valeur de Colonne2 .

  Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() && Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() &&(Column1() ^ Column2()) < 0.001 && (Column1() ^ Column2()) > -0.001 

Sous cette forme, la même comparaison devient beaucoup plus perceptible.

Exemple 4:

Erreur de type similaire:

V501 Il existe des sous-expressions identiques «cs.m_fJacCoeffInv [0] == 0» à gauche et à droite de l'opérateur «&&». b3CpuRigidBodyPipeline.cpp 169

 float m_fJacCoeffInv[2]; static inline void b3SolveFriction(b3ContactConstraint4& cs, ....) { if (cs.m_fJacCoeffInv[0] == 0 && cs.m_fJacCoeffInv[0] == 0) { return; } .... } 

Dans ce cas, le même élément de tableau est revérifié. Très probablement, la condition aurait dû ressembler à ceci: cs.m_fJacCoeffInv [0] == 0 && cs.m_fJacCoeffInv [1] == 0 . Il s'agit d'un exemple classique d'erreur de copier-coller.

Exemple 5:

Une autre lacune a été découverte:

V517 L'utilisation du modèle 'if (A) {...} else if (A) {...}' a été détectée. Il y a une probabilité de présence d'erreur logique. Vérifiez les lignes: 79, 112. main.cpp 79

 int main(int argc, char* argv[]) { .... while (serviceResult > 0) { serviceResult = enet_host_service(client, &event, 0); if (serviceResult > 0) { .... } else if (serviceResult > 0) { puts("Error with servicing the client"); exit(EXIT_FAILURE); } .... } .... } 

La fonction enet_host_service , dont le résultat est affecté à serviceResult , renvoie un en cas de succès et -1 en cas d'échec. Très probablement, la branche else if aurait dû répondre à la valeur négative de serviceResult , mais la condition de vérification a été dupliquée. Il s'agit très probablement aussi d'une erreur de copier-coller.

Un avertissement similaire à l'analyseur, qui n'a pas de sens à décrire dans l'article:

V517 L'utilisation du modèle 'if (A) {...} else if (A) {...}' a été détectée. Il y a une probabilité de présence d'erreur logique. Vérifiez les lignes: 151, 190. PhysicsClientUDP.cpp 151

Au-delà des limites: dépasser les limites du tableau


Exemple 6:

L'une des choses désagréables à rechercher des erreurs est de sortir du tableau. Cette erreur se produit souvent en raison d'une indexation complexe dans la boucle.

Ici, dans la condition de boucle, la variable dofIndex est limitée à 128 par le haut et dof à 4 inclus. Mais m_desiredState ne contient également que 128 éléments. Par conséquent, l'index [dofIndex + dof] peut conduire à la sortie du tableau.

V557 Le dépassement de matrice est possible. La valeur de l'indice «dofIndex + dof» pourrait atteindre 130. PhysicsClientC_API.cpp 968

 #define MAX_DEGREE_OF_FREEDOM 128 double m_desiredState[MAX_DEGREE_OF_FREEDOM]; B3_SHARED_API int b3JointControl(int dofIndex, double* forces, int dofCount, ....) { .... if ( (dofIndex >= 0) && (dofIndex < MAX_DEGREE_OF_FREEDOM ) && dofCount >= 0 && dofCount <= 4) { for (int dof = 0; dof < dofCount; dof++) { command->m_sendState.m_desiredState[dofIndex+dof] = forces[dof]; .... } } .... } 

Exemple 7:

Une erreur similaire, mais maintenant la sommation y conduit non pas lors de l'indexation d'un tableau, mais dans une condition. Si le nom de fichier est aussi long que possible, le terminal zéro sera écrit en dehors du tableau ( erreur Off-by-one ). Naturellement, la variable len sera égale à MAX_FILENAME_LENGTH uniquement dans des cas exceptionnels, mais cela n'élimine pas l'erreur, mais rend simplement son occurrence rare.

V557 Le dépassement de matrice est possible. La valeur de l'index 'len' pourrait atteindre 1024. PhysicsClientC_API.cpp 5223

 #define MAX_FILENAME_LENGTH MAX_URDF_FILENAME_LENGTH 1024 struct b3Profile { char m_name[MAX_FILENAME_LENGTH]; int m_durationInMicroSeconds; }; int len = strlen(name); if (len >= 0 && len < (MAX_FILENAME_LENGTH + 1)) { command->m_type = CMD_PROFILE_TIMING; strcpy(command->m_profile.m_name, name); command->m_profile.m_name[len] = 0; } 

Mesurer une fois - couper sept fois


Exemple 8:

Dans les cas où vous devez utiliser le résultat d'une certaine fonction plusieurs fois ou utiliser à plusieurs reprises une variable à laquelle vous devez accéder via une série d'appels, vous devez utiliser des variables temporaires pour optimiser et mieux lire le code. L'analyseur a trouvé plus de 100 emplacements dans le code où une telle correction peut être effectuée.

V807 Performances réduites . Envisagez de créer un pointeur pour éviter d'utiliser à plusieurs reprises l'expression 'm_app-> m_renderer-> getActiveCamera ()'. InverseKinematicsExample.cpp 315

 virtual void resetCamera() { .... if (....) { m_app->m_renderer->getActiveCamera()->setCameraDistance(dist); m_app->m_renderer->getActiveCamera()->setCameraPitch(pitch); m_app->m_renderer->getActiveCamera()->setCameraYaw(yaw); m_app->m_renderer->getActiveCamera()->setCameraPosition(....); } } 

Ici, la même chaîne d'appel est réutilisée, qui peut être remplacée par un seul pointeur.

Exemple 9:

Performances réduites du V810 . La fonction 'btCos (euler_out.pitch)' a été appelée plusieurs fois avec des arguments identiques. Le résultat devrait éventuellement être enregistré dans une variable temporaire, qui pourrait ensuite être utilisée lors de l'appel de la fonction 'btAtan2'. btMatrix3x3.h 576

Performances réduites du V810 . La fonction 'btCos (euler_out2.pitch)' a été appelée plusieurs fois avec des arguments identiques. Le résultat devrait éventuellement être enregistré dans une variable temporaire, qui pourrait ensuite être utilisée lors de l'appel de la fonction 'btAtan2'. btMatrix3x3.h 578

 void getEulerZYX(....) const { .... if (....) { .... } else { .... euler_out.roll = btAtan2(m_el[2].y() / btCos(euler_out.pitch), m_el[2].z() / btCos(euler_out.pitch)); euler_out2.roll = btAtan2(m_el[2].y() / btCos(euler_out2.pitch), m_el[2].z() / btCos(euler_out2.pitch)); euler_out.yaw = btAtan2(m_el[1].x() / btCos(euler_out.pitch), m_el[0].x() / btCos(euler_out.pitch)); euler_out2.yaw = btAtan2(m_el[1].x() / btCos(euler_out2.pitch), m_el[0].x() / btCos(euler_out2.pitch)); } .... } 

Dans ce cas, vous pouvez créer deux variables et stocker les valeurs renvoyées par la fonction btCos pour euler_out.pitch et euler_out2.pitch , au lieu d'appeler la fonction quatre fois pour chaque argument.

Fuite


Exemple 10:

De nombreuses erreurs du type suivant ont été trouvées dans le projet:

V773 La portée de visibilité du pointeur «importateur» a été fermée sans libérer la mémoire. Une fuite de mémoire est possible. SerializeSetup.cpp 94

 void SerializeSetup::initPhysics() { .... btBulletWorldImporter* importer = new btBulletWorldImporter(m_dynamicsWorld); .... fclose(file); m_guiHelper->autogenerateGraphicsObjects(m_dynamicsWorld); } 

Ici, aucune mémoire n'a été libérée du pointeur d' importation . Cela peut provoquer une fuite de mémoire. Et pour le moteur physique, cela peut être une mauvaise tendance. Pour éviter les fuites, il suffit que la variable ne soit plus nécessaire pour ajouter un importateur de suppression . Mais bien sûr, il vaut mieux utiliser des pointeurs intelligents.

Voici vos lois


Exemple 11:

L'erreur suivante apparaît dans le code car les règles C ++ ne coïncident pas toujours avec les règles mathématiques ou le «bon sens». Remarquez par vous-même où l'erreur se trouve dans un petit extrait de code?

 btAlignedObjectArray<btFractureBody*> m_fractureBodies; void btFractureDynamicsWorld::fractureCallback() { for (int i = 0; i < numManifolds; i++) { .... int f0 = m_fractureBodies.findLinearSearch(....); int f1 = m_fractureBodies.findLinearSearch(....); if (f0 == f1 == m_fractureBodies.size()) continue; .... } .... } 

L'analyseur génère l'avertissement suivant:

V709 Comparaison suspecte trouvée: 'f0 == f1 == m_fractureBodies.size ()'. N'oubliez pas que «a == b == c» n'est pas égal à «a == b && b == c». btFractureDynamicsWorld.cpp 483

Il semblerait que la condition vérifie que f0 est égal à f1 et égal au nombre d'éléments dans m_fractureBodies . Il semble que cette comparaison aurait dû vérifier si f0 et f1 sont à la fin du tableau m_fractureBodies , car ils contiennent la position de l'objet trouvé par la méthode findLinearSearch () . Cependant, en réalité, cette expression se transforme en une vérification pour voir si f0 et f1 sont égales, puis en une vérification si m_fractureBodies.size () est égal au résultat de f0 == f1 . Par conséquent, le troisième opérande est comparé à 0 ou 1 ici.

Belle erreur! Et, heureusement, assez rare. Jusqu'à présent, nous ne l'avons rencontrée que dans deux projets ouverts et, fait intéressant, tous n'étaient que des moteurs de jeu.

Exemple 12:

Lorsque vous travaillez avec des chaînes, il est souvent préférable d'utiliser les fonctionnalités fournies par la classe de chaînes . Ainsi, pour les deux cas suivants, il est préférable de remplacer respectivement strlen (MyStr.c_str ()) et val = "" par MyStr.length () et val.clear () .

Performances réduites du V806 . L'expression du type strlen (MyStr.c_str ()) peut être réécrite en MyStr.length (). RobotLoggingUtil.cpp 213

 FILE* createMinitaurLogFile(const char* fileName, std::string& structTypes, ....) { FILE* f = fopen(fileName, "wb"); if (f) { .... fwrite(structTypes.c_str(), strlen(structTypes.c_str()), 1, f); .... } .... } 

V815 Performances réduites . Pensez à remplacer l'expression 'val = ""' par 'val.clear ()'. b3CommandLineArgs.h 40

 void addArgs(int argc, char **argv) { .... std::string val; .... val = ""; .... } 

Il y a eu d'autres avertissements, mais je pense que vous pouvez arrêter. Comme vous pouvez le voir, l'analyse de code statique peut révéler un large éventail d'erreurs diverses.

La lecture des vérifications de projet ponctuelles est intéressante, mais ce n'est pas la bonne façon d'utiliser des analyseurs de code statiques. Et à ce sujet, nous parlerons ci-dessous.

Erreurs trouvées devant nous


Il était intéressant dans l'esprit du récent article « Erreurs que l'analyse de code statique ne trouve pas parce qu'il n'est pas utilisé » d'essayer de trouver des bogues ou des défauts qui ont déjà été corrigés, mais que l'analyseur statique pourrait détecter.
Image 2

Il n'y avait pas autant de demandes d'extraction dans le référentiel, et beaucoup d'entre elles sont liées à la logique interne du moteur. Mais il y avait aussi des erreurs que l'analyseur pouvait détecter.

Exemple 13:

 char m_deviceExtensions[B3_MAX_STRING_LENGTH]; void b3OpenCLUtils_printDeviceInfo(cl_device_id device) { b3OpenCLDeviceInfo info; b3OpenCLUtils::getDeviceInfo(device, &info); .... if (info.m_deviceExtensions != 0) { .... } } 

Le commentaire d'édition indique qu'il était nécessaire de vérifier le tableau pour le fait qu'il n'est pas vide, mais à la place une vérification de pointeur inutile a été effectuée, qui renvoyait toujours true. Ceci est indiqué par l'avertissement PVS-Studio sur le type de contrôle initial:

V600 Envisagez d'inspecter l'état. Le pointeur 'info.m_deviceExtensions' n'est toujours pas égal à NULL. b3OpenCLUtils.cpp 551

Exemple 14:

Pouvez-vous découvrir immédiatement quel est le problème dans la fonction suivante?

 inline void Matrix4x4::SetIdentity() { m12 = m13 = m14 = m21 = m23 = m24 = m13 = m23 = m41 = m42 = m43 = 0.0; m11 = m22 = m33 = m44 = 1.0; } 

L'analyseur génère les avertissements suivants:

V570 La même valeur est affectée deux fois à la variable 'm23'. LinearR4.h 627

V570 La même valeur est affectée deux fois à la variable 'm13'. LinearR4.h 627

Les affectations répétées avec cette forme d'enregistrement sont difficiles à suivre à l'œil nu et, par conséquent, certains éléments de la matrice n'ont pas reçu la valeur initiale. Cette erreur a été corrigée dans la forme tabulaire de l'enregistrement d'affectation:

 m12 = m13 = m14 = m21 = m23 = m24 = m31 = m32 = m34 = m41 = m42 = m43 = 0.0; 

Exemple 15:

L'erreur suivante dans l'une des conditions de la fonction btSoftBody :: addAeroForceToNode () a conduit à un bogue explicite. Selon le commentaire de la demande de traction, des forces ont été appliquées aux objets du mauvais côté.

 struct eAeroModel { enum _ { V_Point, V_TwoSided, .... END }; }; void btSoftBody::addAeroForceToNode(....) { .... if (....) { if (btSoftBody::eAeroModel::V_TwoSided) { .... } .... } .... } 

PVS-Studio peut également trouver cette erreur et afficher l'avertissement suivant:

V768 La constante d'énumération 'V_TwoSided' est utilisée comme variable de type booléen. btSoftBody.cpp 542

Le contrôle corrigé est le suivant:

 if (m_cfg.aeromodel == btSoftBody::eAeroModel::V_TwoSided) { .... } 

Au lieu de l'équivalence de la propriété d'objet à l'un des énumérateurs, l'énumérateur V_TwoSided lui-même a été vérifié.

Il est clair que je n'ai pas vu toutes les demandes de tirage, car je ne me suis pas fixé une telle tâche. Je voulais juste montrer que l'utilisation régulière d'un analyseur de code statique peut détecter des erreurs à un stade très précoce. C'est la bonne façon d'utiliser l'analyse de code statique. L'analyse statique doit être intégrée au processus DevOps et être le filtre principal des bogues. Tout cela est bien décrit dans l'article " Intégrez l'analyse statique dans le processus, et ne cherchez pas de bugs avec ."

Conclusion


Image 6

À en juger par certaines demandes de tirage, un projet est parfois vérifié à l'aide de divers outils d'analyse de code, mais les modifications sont effectuées non pas progressivement, mais en groupes et à de grands intervalles de temps. Dans certaines demandes, un commentaire indique que des modifications ont été apportées uniquement pour supprimer les avertissements. Cette approche de l'utilisation de l'analyse réduit considérablement son utilité, car c'est une vérification constante du projet qui vous permet de corriger les erreurs immédiatement et de ne pas attendre l'apparition de bogues évidents.

Si vous souhaitez être toujours au courant des actualités et événements de notre équipe, abonnez-vous à nos services sociaux. Réseaux: Instagram , Twitter , Vkontakte , Telegram .



Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien de traduction: PVS-Studio a examiné le moteur de puces de Red Dead Redemption

Source: https://habr.com/ru/post/fr461849/


All Articles