Timebug Часть 2: Интересные Решения От Ea Black Box

Привет, хабр! В его предыдущий В статье я рассказал об интересном баге в старой игрушке, наглядно продемонстрировал явление накопления ошибок округления и просто поделился опытом реверс-инжиниринга.

Я надеялся, что на этом все закончится, но сильно ошибался.

Поэтому под катом я расскажу вам продолжение истории о звере по имени Timebug, о 60 кадрах в секунду и об очень интересных решениях в разработке игр.



Timebug часть 2: интересные решения от EA Black Box



Фон

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

Мне было лень выполнять дополнительную работу, поэтому я искал симптомы во всех играх NFS, которые у меня были на тот момент. Под мишенью также был Underground 2, но первоначальных симптомов я там не обнаружил:

Timebug часть 2: интересные решения от EA Black Box

Как видно по картинке (она, кстати, кликабельна), IGT рассчитывается другим способом, который явно привязан к int, и поэтому никакого накопления ошибок быть не должно.

Я с радостью сообщил об этом в нашем сообществе и планировал забыть об этом, но нет: Эвил вручную пересчитал некоторые видео и снова обнаружил разницу во времени.

Мы решили, что пока нет времени этим заниматься и я сконцентрировался на уже известной на тот момент проблеме, но теперь смог выделить время для себя и поработать именно над этой игрой.



Симптомы

Я изучил поведение глобального таймера и обнаружил, что он «сбрасывается» при каждом перезапуске гонки, при каждом выходе в меню и вообще в любой удобный момент. Это не входило в мои планы, поскольку связь с уже известной проблемой была полностью потеряна, и этот таймер просто не должен был сломаться.

От отчаяния я записал 10 кругов и посчитал время руками.

К моему удивлению, время круга было на 100% точным.

Интересные факты На самом деле был обнаружен еще один таймер, который тоже считал время в режиме float, но он оказался немного бесполезным.

Меняя его, я не добился видимых результатов.

И почему-то этот таймер int не сбрасывается в 0, а устанавливается на 4000. Оставалось только разобрать и посмотреть в чем дело.

Не вдаваясь в подробности, покажу псевдокод процедуры, считающей этот несчастный ИГТ:

  
  
   

if ( g_fFrameLength != 0.0 ) { float v0 = g_fFrameDiff + g_fFrameLength; int v1 = FltToDword(v0); g_dwUnknown0 += v1; g_dwUnknown1 = v1; g_dwUnknown2 = g_dwUnknown0; g_fFrameDiff = v0 - v1 * 0.016666668; g_dwIGT += FltToDword(g_fFrameLength * 4000.0 + 0.5); LODWORD(g_fFrameLength) = 0; ++g_dwFrameCount; g_fIGT = (double)g_dwIGT * 0.00025000001; // Divides IGT by 4000 to get time in seconds }

Во-первых:

g_dwIGT += FltToDword(g_fFrameLength * 4000.0 + 0.5);



Timebug часть 2: интересные решения от EA Black Box

Изначально этот код показался мне совершенно бессмысленным.

Зачем умножать это на 4000, а потом прибавлять половину? На самом деле это очень хитрая магия.

4000 — это просто константа, пришедшая в голову одному из разработчиков.

А вот +0,5 — это такой интересный способ округления по законам математики.

Прибавьте половину к 4,7, и когда вы сократите его до целого числа, вы получите 5, а когда вы прибавите и округлите 4,3, вы получите 4, как вы и хотели.

Метод не самый точный, но, наверное, работает быстрее.

Лично я возьму на заметку.

А теперь, дорогие читатели, я хочу сыграть с вами в игру.

Посмотрите на полный псевдокод выше и попытайтесь найти там ошибку.

Если вы устали или вам просто не интересно, переходите к следующей части.



Ошибка

Линия g_fFrameDiff = v0 — (double)v1 * 0,016666668; Ошибка находится под спойлером, чтобы случайно не заглянуть.

Позвольте мне немного объяснить: 0,01(6) — это 1/60 секунды.

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

Вот отсюда и интересный результат, когда у меня на видео все кружочки совпали с реальностью, а у Эвила нет. Он играет с выключенной вертикальной синхронизацией, а игра залочена на максимум 120 фпс и соответственно на его компьютере код работал некорректно.

Я немного модифицировал код выше и привел его в человеческий вид:

if ( g_fFrameLength != 0.0 ) { float tmpDiff = g_fFrameDiff + g_fFrameLength; int diffTime = FltToDword(v0); g_dwUnknown0 += diffTime; // Some unknown vars g_dwUnknown1 = diffTime; g_dwUnknown2 = g_dwUnknown0; g_fFrameDiff = tmpDiff - diffTime * 1.0/60; g_dwIGT += FltToDword(g_fFrameLength * 4000 + 0.5); g_fFrameLength = 0; ++g_dwFrameCount; g_fIGT = (float)g_dwIGT / 4000; // Divides IGT by 4000 to get time in seconds }

Здесь вы можете видеть, что при расчете задержки изначально используется фактическое время кадра, а затем жестко запрограммированные 60 кадров в секунду.

ПОДОЗРИТЕЛЬНЫЙ ! Выключаю vsync и получаю 120 кадров в секунду.

Я иду записывать видео и получаю разницу примерно в 0,3 секунды за круг.

Бинго! Осталось только прошить хардкорные 60фпс на хардкорные 120фпс.

Для этого посмотрите ассемблерный код и найдите адрес, где находится эта волшебная константа: 0x007875BC. Спойлер На самом деле известно, что эта константа имеет тип float/double и будет загружена в FPU. FPU не умеет загружаться из регистра, поэтому ему суждено было оказаться где-то в памяти.

Хорошо, что его не было в стеке, иначе я бы так легко не отделался.

Пришлось бы вносить изменения в сам код игры, чего делать не хотелось.

В этот раз я не писал никаких специальных программ, а просто изменил руками значение этой переменной в Cheat Engine на необходимое значение.

После этого я снова записал 10 кругов и посчитал время — IGT и RTA наконец совпали.

На самом деле они не совпадали на 100%.

Но в основном из-за того, что запись видео сильно снизила у меня частоту кадров, из-за чего игра перестала адекватно рассчитывать разницу во времени.

Но в целом разница составила около 0,02 секунды.

Я еще немного заглянул в код, чтобы узнать, на что влияют те переменные, которые так старательно рассчитывались в процедуре расчета времени.

Я особо не нашел, но где-то в движке рядом с g_fFrameTime используется g_fDiffTime. Скорее всего, мое предположение о компенсации заикания оказалось верным.

Но кто знает этих разработчиков?

Послесловие

Я даже не знаю, сколько раз я сталкивался с игрой, требующей 60 кадров в секунду.

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

Особенно, если вы инди-разработчик.

И особенно если вы разрабатываете на ПК.

Для консолей добиться разной аппаратной мощности не получится, а вот на ПК с этим постоянно возникают проблемы.

А еще есть мониторы с частотой обновления 120/144 Гц и даже больше.

И g-sync уже приехал.

Но NFS - это порты с консолей, поэтому в решениях часто бывает чисто консольный подход: считать, что FPS не поднимется выше 60 (30, 25, любое число), а многие решения жестко заточены именно под это количество кадров в секунду.

.

К сожалению, в новых частях серии это стало более заметно.

На этот раз статья получилась не такой объемной, хотя простор для исследований есть.

Надеюсь, в этих играх будет еще больше интересного, о чем можно поговорить.

Теги: #обратное проектирование #обратное проектирование #ошибка #обратное проектирование

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.