PVS-Studio echó un vistazo al motor Red Dead Redemption - Bullet

Cuadro 4

Hoy en día, por ejemplo, para desarrollar juegos, ya no es necesario implementar de forma independiente la física de los objetos desde cero, ya que hay una gran cantidad de bibliotecas para esto. Bullet se usó activamente en muchos juegos AAA, proyectos de realidad virtual, varias simulaciones y aprendizaje automático. Sí, y todavía se usa hoy en día, siendo, por ejemplo, uno de los motores Red Dead Redemption y Red Dead Redemption 2. Entonces, ¿por qué no ver Bullet con PVS-Studio para ver qué errores puede detectar el análisis estático en un proyecto a gran escala? relacionado con la simulación física.

Esta biblioteca se distribuye libremente , de modo que todos puedan usarla opcionalmente en sus proyectos. Además de Red Dead Redemption, este motor de física también se usa en la industria del cine para crear efectos especiales. Por ejemplo, estuvo involucrado en la filmación de Sherlock Holmes por Guy Ritchie para calcular las colisiones.

Si este es su primer encuentro con un artículo donde PVS-Studio verifica proyectos, entonces haré una pequeña digresión. PVS-Studio es un analizador de código estático que ayuda a encontrar errores, deficiencias y vulnerabilidades potenciales en el código fuente de C, C ++, C #, programas Java. El análisis estático es un tipo de proceso de revisión de código automatizado.

Para el calentamiento


Ejemplo 1:

Comencemos con un error gracioso:

V624 Probablemente haya un error de imprenta en la constante '3.141592538'. Considere usar 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); .... } 

Un pequeño error tipográfico en el número Pi (3.141592653 ...), falta el número "6" en la séptima posición en la parte fraccionaria.

Imagen 1
Tal vez el error en la décima millonésima posición decimal no tendrá consecuencias tangibles, pero aún debe usar las constantes de biblioteca existentes sin errores tipográficos. Para Pi, hay una constante M_PI del encabezado math.h.

Copiar y pegar


Ejemplo 2

Algunas veces el analizador le permite encontrar el error indirectamente. Entonces, por ejemplo, aquí se pasan tres argumentos relacionados halfExtentsX, halfExtentsY, halfExtentsZ a la función, pero este último no se usa en ninguna parte de la función. Puede notar que al llamar al método addVertex , la variable halfExtentsY se usa dos veces. Entonces, tal vez sea un error de copiar y pegar, y aquí debería usarse un argumento olvidado.

El parámetro V751 'halfExtentsZ' no se usa dentro del cuerpo de la función. 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], ....); .... } 

Ejemplo 3

El analizador también encontró el siguiente fragmento interesante, lo traeré primero en su forma original.

Cuadro 11

¿Ves esta última línea?

Cuadro 12

Es muy extraño que el programador haya decidido escribir una condición tan larga en una línea. Pero el hecho de que lo más probable es que se haya introducido un error no es del todo sorprendente.

El analizador emitió las siguientes advertencias en esta línea.

V501 Hay subexpresiones idénticas 'rotmat.Column1 (). Norm () <1.0001' a la izquierda y a la derecha del operador '&&'. LinearR4.cpp 351

V501 Hay subexpresiones idénticas '0.9999 <rotmat.Column1 (). Norm ()' a la izquierda y a la derecha del operador '&&'. LinearR4.cpp 351

Si escribimos todo en forma visual "tabular", quedará claro que las mismas verificaciones se aplican a la Columna1 . Las dos últimas comparaciones muestran que hay Column1 y Column2 . Lo más probable es que la tercera y cuarta comparaciones deberían haber verificado el valor de Column2 .

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

De esta forma, la misma comparación se vuelve mucho más notable.

Ejemplo 4

Error de un tipo similar:

V501 Hay subexpresiones idénticas 'cs.m_fJacCoeffInv [0] == 0' a la izquierda y a la derecha del operador '&&'. 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; } .... } 

En este caso, el mismo elemento de matriz se verifica dos veces. Lo más probable es que la condición debería haberse visto así: cs.m_fJacCoeffInv [0] == 0 && cs.m_fJacCoeffInv [1] == 0 . Este es un ejemplo clásico de error de copiar y pegar.

Ejemplo 5:

Se descubrió otra deficiencia:

V517 El uso del patrón 'if (A) {...} else if (A) {...}' fue detectado. Hay una probabilidad de presencia de error lógico. Líneas de verificación: 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 función enet_host_service , cuyo resultado se asigna a serviceResult , devuelve uno si tiene éxito y -1 si falla. Lo más probable es que la rama si hubiera respondido al valor negativo de serviceResult , pero la condición de verificación se duplicó. Lo más probable es que esto también sea un error de copiar y pegar.

Una advertencia similar del analizador, que no tiene sentido describir en el artículo:

V517 El uso del patrón 'if (A) {...} else if (A) {...}' fue detectado. Hay una probabilidad de presencia de error lógico. Líneas de verificación: 151, 190. PhysicsClientUDP.cpp 151

Más allá de los límites: ir más allá de los límites de la matriz


Ejemplo 6

Una de las cosas desagradables para buscar errores es salir de la matriz. A menudo, este error ocurre debido a una indexación compleja en el bucle.

Aquí, en la condición de bucle, la variable dofIndex está limitada a 128 desde arriba, y dof a 4, inclusive. Pero m_desiredState también contiene solo 128 elementos. Como resultado, el índice [dofIndex + dof] puede conducir a la salida de la matriz.

V557 Array overrun es posible. El valor del índice 'dofIndex + dof' podría alcanzar 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]; .... } } .... } 

Ejemplo 7

Un error similar, pero ahora la suma no lo lleva a indexar una matriz, sino a una condición. Si el nombre del archivo es lo más largo posible, el terminal cero se escribirá fuera de la matriz ( Error Off-by-one ). Naturalmente, la variable len será igual a MAX_FILENAME_LENGTH solo en casos excepcionales, pero esto no elimina el error, sino que simplemente hace que su ocurrencia sea rara.

V557 Array overrun es posible. El valor del índice 'len' podría llegar a 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; } 

Mida una vez - corte siete veces


Ejemplo 8

En los casos en que necesite usar el resultado de una determinada función muchas veces o usar repetidamente una variable a la que necesita acceder a través de una serie de llamadas, debe usar variables temporales para optimizar y leer mejor el código. El analizador encontró más de 100 lugares en el código donde se puede realizar dicha corrección.

V807 Disminución del rendimiento. Considere crear un puntero para evitar usar la expresión 'm_app-> m_renderer-> getActiveCamera ()' repetidamente. 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(....); } } 

Aquí se reutiliza la misma cadena de llamadas, que se puede reemplazar con un solo puntero.

Ejemplo 9

V810 Disminución del rendimiento. La función 'btCos (euler_out.pitch)' se llamó varias veces con argumentos idénticos. El resultado posiblemente debería guardarse en una variable temporal, que luego podría usarse mientras se llama a la función 'btAtan2'. btMatrix3x3.h 576

V810 Disminución del rendimiento. La función 'btCos (euler_out2.pitch)' se llamó varias veces con argumentos idénticos. El resultado posiblemente debería guardarse en una variable temporal, que luego podría usarse mientras se llama a la función '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)); } .... } 

En este caso, puede crear dos variables y almacenar los valores devueltos por la función btCos para euler_out.pitch y euler_out2.pitch en ellas , en lugar de llamar a la función cuatro veces para cada argumento.

Fuga


Ejemplo 10

Se encontraron muchos errores del siguiente tipo en el proyecto:

V773 El alcance de visibilidad del puntero 'importador' se salió sin liberar la memoria. Una pérdida de memoria es posible. SerializeSetup.cpp 94

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

No se ha liberado memoria del puntero del importador aquí. Esto puede causar una pérdida de memoria. Y para el motor físico, esta puede ser una mala tendencia. Para evitar fugas, es suficiente después de que la variable ya no sea necesaria para agregar un importador de eliminación . Pero, por supuesto, es mejor usar punteros inteligentes.

Aquí están tus leyes


Ejemplo 11

El siguiente error aparece en el código debido al hecho de que las reglas de C ++ no siempre coinciden con las reglas matemáticas o el "sentido común". ¿Te das cuenta de dónde está el error en un pequeño fragmento de código?

 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; .... } .... } 

El analizador genera la siguiente advertencia:

V709 Comparación sospechosa encontrada: 'f0 == f1 == m_fractureBodies.size ()'. Recuerde que 'a == b == c' no es igual a 'a == b && b == c'. btFractureDynamicsWorld.cpp 483

Parece que la condición comprueba que f0 es igual a f1 e igual al número de elementos en m_fractureBodies . Parece que esta comparación debería haber verificado si f0 y f1 están al final de la matriz m_fractureBodies , ya que contienen la posición del objeto encontrado por el método findLinearSearch () . Sin embargo, de hecho, esta expresión se convierte en una prueba para ver si f0 y f1 son iguales, y luego para verificar si m_fractureBodies.size () es igual al resultado de f0 == f1 . Como resultado, el tercer operando se compara con 0 o 1 aquí.

Hermoso error! Y, afortunadamente, bastante raro. Hasta ahora la hemos conocido solo en dos proyectos abiertos, y curiosamente, todos ellos eran solo motores de juegos.

Ejemplo 12

Cuando se trabaja con cadenas, a menudo es mejor usar las facilidades proporcionadas por la clase de cadena . Entonces, para los siguientes dos casos, es mejor reemplazar strlen (MyStr.c_str ()) y val = "" con MyStr.length () y val.clear (), respectivamente.

V806 Disminución del rendimiento. La expresión de strlen (MyStr.c_str ()) puede reescribirse como 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 Disminución del rendimiento. Considere reemplazar la expresión 'val = ""' con 'val.clear ()'. b3CommandLineArgs.h 40

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

Hubo otras advertencias, pero creo que puedes parar. Como puede ver, el análisis de código estático puede revelar una amplia gama de diversos errores.

Leer sobre verificaciones de proyectos por única vez es interesante, pero esta no es la forma correcta de usar analizadores de código estático. Y sobre esto hablaremos a continuación.

Errores encontrados ante nosotros


Fue interesante en el espíritu del reciente artículo " Errores que el análisis de código estático no encuentra porque no se usa " para tratar de encontrar errores o deficiencias que ya se han solucionado, pero que el analizador estático podría detectar.
Imagen 2

No había tantas solicitudes de extracción en el repositorio, y muchas de ellas están relacionadas con la lógica interna del motor. Pero también hubo errores que el analizador pudo detectar.

Ejemplo 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) { .... } } 

El comentario de edición dice que fue necesario verificar la matriz por el hecho de que no está vacía, sino que se realizó una comprobación de puntero sin sentido, que siempre devolvió verdadero. Esto se indica mediante la advertencia PVS-Studio en el tipo de verificación inicial:

V600 Considere inspeccionar la condición. El puntero 'info.m_deviceExtensions' no siempre es igual a NULL. b3OpenCLUtils.cpp 551

Ejemplo 14

¿Puedes averiguar inmediatamente cuál es el problema en la próxima función?

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

El analizador genera las siguientes advertencias:

V570 Se asigna el mismo valor dos veces a la variable 'm23'. LinealR4.h 627

V570 Se asigna el mismo valor dos veces a la variable 'm13'. LinealR4.h 627

Las tareas repetidas con esta forma de grabación son difíciles de rastrear a simple vista y, como resultado, algunos de los elementos de la matriz no recibieron el valor inicial. Este error se corrigió en la forma tabular del registro de asignación:

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

Ejemplo 15

El siguiente error en una de las condiciones de la función btSoftBody :: addAeroForceToNode () condujo a un error explícito. Según el comentario en la solicitud de extracción, se aplicaron fuerzas a los objetos del lado equivocado.

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

PVS-Studio también podría encontrar este error y mostrar la siguiente advertencia:

V768 La constante de enumeración 'V_TwoSided' se usa como una variable de tipo booleano. btSoftBody.cpp 542

La verificación corregida es la siguiente:

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

En lugar de la equivalencia de la propiedad del objeto con uno de los enumeradores, se verificó el enumerador V_TwoSided .

Está claro que no vi todas las solicitudes de extracción, ya que no me puse esa tarea. Solo quería mostrar que el uso regular de un analizador de código estático puede detectar errores en una etapa muy temprana. Esta es la forma correcta de usar el análisis de código estático. El análisis estático debe integrarse en el proceso DevOps y ser el filtro principal de errores. Todo esto está bien descrito en el artículo " Incruste el análisis estático en el proceso y no busque errores ".

Conclusión


Cuadro 6

A juzgar por algunas solicitudes de extracción, un proyecto a veces se verifica a través de varias herramientas de análisis de código, sin embargo, las ediciones no se realizan gradualmente, sino en grupos y a grandes intervalos de tiempo. En algunas solicitudes, un comentario indica que se hicieron cambios solo para suprimir las advertencias. Este enfoque para el uso del análisis reduce significativamente su utilidad, ya que es una verificación constante del proyecto que le permite corregir los errores de inmediato y no esperar hasta que aparezcan errores obvios.

Si quiere estar siempre al tanto de las noticias y eventos de nuestro equipo, suscríbase a nuestros servicios sociales. Redes: Instagram , Twitter , Vkontakte , Telegram .



Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace de traducción: PVS-Studio examinó el motor de bala de Red Dead Redemption

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


All Articles