Статический анализ в видеоигровой индустрии: топ-10 программных ошибок

08558022e4f82d742e459abf45b100b2.png


Если вы занимаетесь разработкой ПО в сфере видеоигровой индустрии и задаётесь вопросом о том, что ещё можно сделать, чтобы повысить качество продукта \ упростить процесс разработки, и при этом не используете статический анализ — самое время начать. Сомневаетесь? Что ж, я попробую вас в этом убедить. Если же вам просто интересно посмотреть на ошибки, которые допускают разработчики из сферы видеоигровой индустрии, то, опять же, вы попали по адресу — специально для вас отобраны наиболее интересные.

Почему вам следует использовать статический анализ


Несмотря на то, что разработка видеоигр включает в себя большое количество этапов, одним из основных остаётся программирование. Даже если вы не пишете тысячи строк кода, так или иначе приходится использовать различные инструменты, от качества которых зависят в том числе удобство работы и конечный результат. Если же вы являетесь разработчиком таких инструментов (например, игровых движков), вам и так должно быть всё понятно.

Почему статический анализ полезен в сфере разработки программного обеспечения в целом?

Есть несколько основных причин:

  • Стоимость и сложность исправления ошибки возрастает со временем. Одно из основных преимуществ статического анализа — обнаружение ошибок на ранних этапах разработки (можно находить ошибку в момент написания кода). Следовательно, используя статический анализатор, можно упростить жизнь себе и коллегам, обнаруживая и исправляя многие ошибки до того, как они станут причиной головной боли.
  • С помощью статического анализа можно выявлять множество различных паттернов ошибок (copy-paste, опечатки, неправильное использование функций и т.д.).
  • Как правило, статический анализ хорош в выявлении тех проблем, которые плохо выявляются с помощью динамического анализа. Впрочем, справедливо и обратное утверждение.
  • Негативные моменты использования (например, ложные срабатывания), как правило, 'сглаживаются' командами-разработчиками серьёзных статических анализаторов за счёт различных функций подавления предупреждений (единичное, по паттерну и т.п.), отключения неактуальных диагностик, исключения из анализа файлов и директорий. Благодаря этому можно значительно уменьшить процент 'шума', правильно настроив анализатор. Мой коллега Андрей Карпов продемонстрировал в статье про проверку EFL Core Libraries, что в случае настройки анализатора количество ложных срабатываний составит всего 10–15%.


Но так или иначе, это всё теория, а вам, наверняка, интересны более практические примеры. Что же, они представлены ниже.

Статический анализ в Unreal Engine


62b06eaca16b544bd66844fc1ad54ce9.png


Если вы дочитали до этого момента, что-то рассказывать вам о том, что такое Unreal Engine или что за компания такая — Epic Games — не стоит, и, если эти ребята не пользуются у вас авторитетом — напишите мне, кто тогда пользуется.

Команда PVS-Studio несколько раз сотрудничала с командой Epic Games, помогая им внедрять и использовать статический анализ в проекте Unreal Engine и исправлять ошибки \ ложные срабатывания, обнаруженные анализатором. Уверен, это был интересный и полезный опыт для обеих сторон.

Одним из следствий стало добавление в Unreal Engine специального флага, который позволил удобно интегрировать статический анализ в сборочную систему Unreal Engine проектов.

Ключевая мысль проста — ребята заботятся о качестве своего кода и используют для этого разные подходы, одним из которых стало внедрение статического анализа.

Джон Кармак о статическом анализе


f9a77799ca0b8bec1119ba800bd0d770.png


Джон Кармак, один из самых известных разработчиков в сфере видеоигровой индустрии, в своё время признал активное использование методики статического анализа одним из главных своих достижений как программиста:»The most important thing I have done as a programmer in recent years is to aggressively pursue static code analysis.» В следующий раз, когда кто-то из ваших знакомых скажет, что статический анализ — инструмент для новичков, покажите ему эту цитату. Своё знакомство со статическим анализом Кармак описал в соответствующей статье, с которой я настоятельно рекомендую ознакомиться: как для мотивации, так и для общего развития.

Ошибки, найденные с помощью статического анализа в играх и игровых движках


127c5a68b6cc1aa927763c1588523233.png


Пожалуй, один из лучших способов показать пользу статического анализа — демонстрация его возможностей на практике. Этим, например, занимается команда PVS-Studio, проверяя open source проекты.

В выигрыше остаются все:

  • Разработчики проектов получают уведомления об ошибках и могут их поправить. В идеале, конечно, подход должен быть иным, нежели исправление ошибок по логу \ статье — стоит самостоятельно проверить анализатором проект и изучить выданные предупреждения. Это важно хотя бы по той причине, что авторы статей могут что-то упускать из виду или непреднамеренно делать акцент на тех ошибках, которые не критичны для проекта.
  • Разработчики анализатора могут улучшить инструмент по результатам анализа, а также демонстрируют возможности анализатора по обнаружению ошибок.
  • Читатели видят, какие паттерны ошибок допускаются, набираются опыта, знакомятся с методикой статического анализа.


В общем — чем не доказательство эффективности и пользы?

Команды, уже использующие статический анализ


802674d7c39c6bfed3b9da532557856a.png


Пока кто-то размышляет о том, стоит ли внедрять статический анализ в свой процесс разработки, некоторые команды уже вовсю его используют и извлекают пользу! Например, Rocksteady, Epic Games, ZeniMax Media, Oculus, Codemasters, Wargaming и другие (источник).

Топ-10 ошибок из программного кода в видеоигровой индустрии


Сразу стоит оговориться, что это не какой-то мега-глобальный 'топ', а те ошибки, найденные анализатором PVS-Studio в играх и игровых движках, которые показались мне наиболее интересными.

Как обычно, для большего интереса предлагаю вам сначала самостоятельно попробовать найти ошибку в приведённом фрагменте кода, а лишь затем читать предупреждение анализатора и описание проблемы.

Десятое место

Источник: Ищем аномалии в X-Ray Engine

1faeffbb2e37ea948cabdd1fc65cc9f0.png

На десятом месте расположилась ошибка из игрового движка X-Ray Engine, использовавшегося в играх S.T. A.L.K. E.R. Те, кто играл в игры из этой серии, наверняка помнят много забавных (и не очень) багов, возникавших во время игрового процесса. Особенно это касается S.T. A.L.K. E.R.: Clear Sky, в которую без патчей играть было вообще нереально (до сих пор помню баг, 'убивавший' все сохранения). Как показал анализ игрового движка — баги в коде действительно есть, и их достаточно много. Ниже представлен один из них.

BOOL CActor::net_Spawn(CSE_Abstract* DC)
{
  ....
  m_States.empty();
  ....
}


Предупреждение PVS-Studio: V530 The return value of function 'empty' is required to be utilized.

Проблема проста — не используют возвращаемое логическое значение метода empty, указывающее, пуст ли контейнер. Судя по тому, что в приведённом выражении кроме вызова метода ничего нет, можно предположить, что разработчик хотел очистить контейнер, но ошибся, вызвав метод empty вместо clear.

Кто-то может сказать, что это слишком простая ошибка для Топ-10. Но в этом и её прелесть! Несмотря на то, что вот так, взглядом со стороны, ошибка кажется достаточно простой и очевидной, подобные 'простые' ошибки не перестают появляться (и обнаруживаться) в различных проектах.

Девятое место

Источник: Долгожданная проверка CryEngine V

Продолжаем раскрывать тему ошибок в игровых движках. На девятом месте — код из CryEngine V. В играх на этом движке я не замечал столько багов, сколько в играх на X-Ray Engine, но 'вскрытие' показало, что подозрительных фрагментов кода движок содержит немало.

void CCryDXGLDeviceContext::
OMGetBlendState(...., FLOAT BlendFactor[4], ....)
{
  CCryDXGLBlendState::ToInterface(ppBlendState, m_spBlendState);
  if ((*ppBlendState) != NULL)
    (*ppBlendState)->AddRef();
  BlendFactor[0] = m_auBlendFactor[0];
  BlendFactor[1] = m_auBlendFactor[1];
  BlendFactor[2] = m_auBlendFactor[2];
  BlendFactor[2] = m_auBlendFactor[3];
  *pSampleMask = m_uSampleMask;
}


Предупреждение PVS-Studio: V519 The 'BlendFactor[2]' variable is assigned values twice successively. Perhaps this is a mistake.

Как неоднократно упоминалось в наших статьях, никто не застрахован от опечаток. На практике также неоднократно подтверждалось, что статический анализ отлично справляется с обнаружением copy-paste и опечаток. Переписывали значения из массива m_auBlendFactor в BlendFactor, но случайно опечатались и два раза написали BlendFactor[2]. Из-за этой путаницы в BlendFactor[2] будет записано значение из m_auBlendFactor[3], а в BlendFactor[3] значение не поменяется.

Восьмое место

Источник: Единорог в космосе: проверяем исходный код 'Space Engineers'

Немного изменим курс и перейдём от C++ к C#. Перед нами — код из проекта Space Engineers — игры-'песочницы' о строительстве и обслуживании космических структур. Сам я в игру не играл, но как сказал один из комментаторов статьи:»Я не сильно удивлён результатами :)». Что ж, интересные ошибки действительно удалось найти, ниже — две из них.

public void Init(string cueName)
{
  ....
  if (m_arcade.Hash    == MyStringHash.NullOrEmpty && 
      m_realistic.Hash == MyStringHash.NullOrEmpty)
    MySandboxGame.Log.WriteLine(string.Format(
      "Could not find any sound for '{0}'", cueName));
  else
  {
    if (m_arcade.IsNull)
      string.Format(
        "Could not find arcade sound for '{0}'", cueName);
    if (m_realistic.IsNull)
      string.Format(
        "Could not find realistic sound for '{0}'", cueName);
  }
}


Предупреждения PVS-Studio:

  • V3010 The return value of function 'Format' is required to be utilized.
  • V3010 The return value of function 'Format' is required to be utilized.


Как видите, ошибки игнорирования возвращаемых значений методов встречаются не только в коде на C++, но и на C#. Метод String.Format составляет результирующую строку на основе строки формата и объектов для форматирования, после чего возвращает результат как возвращаемое значение метода. В коде выше в else-ветви есть два вызова string.Format, но их возвращаемые значения не используются. Скорее всего, необходимо было залогировать эти сообщения по аналогии с тем, как это сделано в then-ветви оператора if, используя метод MySandboxGame.Log.WriteLine.

Седьмое место

Источник: Проверка проекта Quake III Arena GPL

Я уже говорил, что статический анализ хорошо обнаруживает опечатки? Пример ниже — ещё одно тому подтверждение.

void Terrain_AddMovePoint(....) {
  ....
  x = ( v[ 0 ] - p->origin[ 0 ] ) / p->scale_x;
  y = ( v[ 1 ] - p->origin[ 1 ] ) / p->scale_x;
  ....
}


Предупреждение PVS-Studio: V537 Consider reviewing the correctness of 'scale_x' item’s usage.

Происходит запись в переменные x и y, но в обоих присвоениях в выражении вычисления значения используется подвыражение p→scale_x. Выглядит очень подозрительно и похоже на то, что во втором случае необходимо заменить выражение p→scale_x на p→scale_y.

Шестое место

Источник: Проверяем исходный C#-код Unity

Компания Unity Technologies не так давно открыла исходный код собственного игрового движка — Unity. Мы не могли пройти мимо такого события и при анализе нашли немало интересных мест. Ниже — одно из них.

public override bool IsValid()
{
  ....
  return base.IsValid()
    && (pageSize >= 1 || pageSize <= 1000)
    && totalFilters <= 10;
}


Предупреждение PVS-Studio: V3063 A part of conditional expression is always true if it is evaluated: pageSize

Имеем неверную проверку диапазона для pageSize. Скорее всего планировалось проверить, что значение pageSize лежит в пределах [1; 1000]. Однако допущена досадная опечатка: вместо оператора '&&' программист случайно написал '||'. Получается, что подвыражение на самом деле ничего не проверяет.

Пятое место

Источник: Анализируем ошибки в открытых компонентах Unity3D

На соседнем месте расположилась интересная ошибка из компонентов Unity3D. Статья-источник была написана более чем за год до того, как был открыт исходный код движка Unity. Тем не менее, уже тогда можно было найти что-то интересное.

public static CrawledMemorySnapshot Unpack(....)
{
  ....
  var result = new CrawledMemorySnapshot
  {
    ....
    staticFields = packedSnapshot.typeDescriptions
                                 .Where(t =>
                                        t.staticFieldBytes != null &
                                        t.staticFieldBytes.Length > 0)
                                 .Select(t => UnpackStaticFields(t))
                                 .ToArray()
    ....
  };
  ....
}


Предупреждение PVS-Studio: V3080 Possible null dereference. Consider inspecting 't.staticFieldBytes'.

Обратите внимание на лямбда-выражение, переданное в качестве аргумента методу Where. Судя по коду, коллекция typeDescriptions может содержать такие элементы, у которых член staticFieldBytes может иметь значение null. Именно поэтому, прежде чем обратиться к свойству Length, выполняется проверка, что staticFieldBytes!= null. Вот только были перепутаны операторы, и вместо '&&' используется '&'. Это значит, что независимо от результата вычисления левого выражения (true \ false) правое выражение также будет вычисляться, из-за чего, если staticFieldBytes == null, при обращении к свойству Length будет сгенерировано исключение типа NullReferenceException. При использовании '&&' такой ситуации не возникло бы, так как, если staticFieldBytes == null, правая часть выражения не вычислялась бы.

Несмотря на то, что Unity — единственный движок, дважды попавший в данный «Топ-10», энтузиастам это не мешает создавать на нём замечательных игр. В том числе — про борьбу с 'багами'.

ba55d101292addbfb672af5ecf4eacdd.png


Четвертое место

Источник: Анализ исходного кода движка Godot

Иногда встречаются интересные ошибки, связанные с пропущенными ключевыми словами. Пример: создаётся объект исключения, но никак не используется, так как разработчик забывает указать ключевое слово throw. Встречаются такие ошибки и в C# проектах, и в C++ проектах. Пропущенное ключевое слово встретилось и в игровом движке Godot Engine.

Variant Variant::get(const Variant& p_index, bool *r_valid) const 
{
  ....
  if (ie.type == InputEvent::ACTION) 
  {
    if (str =="action") 
    {
      valid=true;
      return ie.action.action;
    }
    else if (str == "pressed") 
    {
      valid=true;
      ie.action.pressed;
    }
  }
  ....
}


Предупреждение PVS-Studio: V607 Ownerless expression 'ie.action.pressed'.

В приведённом фрагменте кода видно, что в зависимости от значений ie.type и str хотели вернуть определённое значение типа Variant. Вот только один раз выражение возврата написали нормально — return ie.action.action; , а во второй раз забыли оператор return, из-за чего нужное значение не будет возвращено, а метод продолжит своё выполнение.

Третье место

Источник: PVS-Studio: анализируем код Doom 3

Вот мы и подобрались к «Топ-3». Замыкает его небольшой фрагмент исходного кода из игры Doom 3. Как я говорил выше, то, что со стороны ошибка выглядит просто и может быть непонятно, как можно так ошибиться, ещё ничего не значит — на практике каких типов ошибок только не встретишь…

void Sys_GetCurrentMemoryStatus( sysMemoryStats_t &stats ) {
  ....
  memset( &statex, sizeof( statex ), 0 );
  ....
}


Предупреждение PVS-Studio: V575 The 'memset' function processes '0' elements. Inspect the third argument.

Чтобы лучше понять проблему, стоит вспомнить сигнатуру функции memset:

void* memset(void* dest, int ch, size_t count);


Сопоставив сигнатуру функции с её вызовом, можно заметить, что последние два аргумента перепутаны местами. Как следствие — какой-то блок памяти, который требовалось обнулить, так и останется неизменённым.

Второе место

Второе место заняла ошибка, найденная в C# коде игрового движка Xenko.

Источник: Ищем ошибки в игровом движке Xenko

private static ImageDescription 
CreateDescription(TextureDimension dimension, 
                  int width, int height, int depth, ....) { .... }

public static Image New3D(int width, int height, int depth, ....)
{
    return new Image(CreateDescription(TextureDimension.Texture3D,  
                                       width, width, depth,  
                                       mipMapCount, format, 1), 
                     dataPointer, 0, null, false);
}


Предупреждение PVS-Studio: V3065 Parameter 'height' is not utilized inside method’s body.

Ошибку допустили, передавая аргументы методу CreateDescription. Если посмотреть на его сигнатуру, можно увидеть, что второй, третий и четвёртый параметры имеют названия width, height и depth соответственно. При вызове же передаются аргументы width, width и depth. Подозрительно? Вот и анализатор тоже счёл это место достаточно подозрительным, чтобы выдать предупреждение.

Первое место

Источник: Долгожданная проверка Unreal Engine 4

Возглавляет наш сегодняшний «Топ-10» ошибка из игрового движка Unreal Engine. Как и в случае со статьёй «Toп 10 ошибок в C++ проектах за 2017 год», увидев приведённую ниже ошибку, я сразу понял, кто должен возглавить список самых интересных ошибок.

bool VertInfluencedByActiveBone(
  FParticleEmitterInstance* Owner,
  USkeletalMeshComponent* InSkelMeshComponent,
  int32 InVertexIndex,
  int32* OutBoneIndex = NULL);

void UParticleModuleLocationSkelVertSurface::Spawn(....)
{
  ....
  int32 BoneIndex1, BoneIndex2, BoneIndex3;
  BoneIndex1 = BoneIndex2 = BoneIndex3 = INDEX_NONE;

  if(!VertInfluencedByActiveBone(
        Owner, SourceComponent, VertIndex[0], &BoneIndex1) &&
     !VertInfluencedByActiveBone(
        Owner, SourceComponent, VertIndex[1], &BoneIndex2) && 
     !VertInfluencedByActiveBone(
        Owner, SourceComponent, VertIndex[2]) &BoneIndex3)
  {
  ....
}


Предупреждение PVS-Studio: V564 The '&' operator is applied to bool type value. You’ve probably forgotten to include parentheses or intended to use the '&&' operator.

Не удивлюсь, если, сопоставляя предупреждение анализатора и приведённый код, вы задались вопросом: «А где здесь, собственно, использование '&' вместо '&&'?». Но, если обратить внимание, что последний параметр функции VertInfluencedByActiveBone имеет значение по умолчанию, и упростить условное выражение оператора if, всё станет более понятно:

if (!foo(....) && !foo(....) && !foo(....) & arg)


Обратите особое внимание на последнее подвыражение:

!VertInfluencedByActiveBone(Owner, SourceComponent, VertIndex[2])  
&BoneIndex3


Параметр со значением по умолчанию сыграл злую шутку — если бы значения по умолчанию не было, данный код бы просто не скомпилировался. Но так как оно есть, всё успешно компилируется, а ошибка не менее успешно маскируется. Вот это подозрительное место и заметил анализатор — инфиксная операция '&', в которой левый операнд имеет тип bool, а правый — int32.

Заключение


asgrlvze0tx6cn3ifugleggi9oc.png


Надеюсь, мне удалось показать, что статический анализ будет полезен при разработке игр и игровых движков и является ещё одним ответом на вопрос о повышении качества кода (и конечного продукта, как следствие). Если вы занимаетесь разработкой в сфере видеоигровой индустрии, то просто обязаны рассказать коллегам о статическом анализе и показать эту статью. Думаете с чего начать? Начните с PVS-Studio.

tsz9kmyjtteajhd4x1au60rsrvq.png

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. Static Analysis in Video Game Development: Top 10 Software Bugs

© Habrahabr.ru