Мы побеседовали с Беном, одним из наших программистов пользовательского интерфейса, который исправил ошибку “исковерканного текста”, долгое время преследовавшую Path of Exile. Поскольку сама по себе ошибка вызвала некоторый интерес, Бен любезно написал для нас разбор полётов. Читайте его ниже!



После сообщения о том, что мы наконец нашли решение печально знаменитой ошибки с “исковерканным текстом”, с которой игроки сталкивались на протяжении последних 6 лет, некоторые игроки выразили интерес к подробностям этой старой как мир неполадки. Мне всегда хотелось попробовать однажды написать статью технического толка, поэтому вот она! Ошибка была известна под разными именами; текст называли “кривым”, “искорёженным” или “корявым”. Мы в студии в основном называли его “искажённым”, поэтому так я его и буду звать.

Я точно знаю, что баг появился в кодовой базе 25 апреля 2016 г. и попал в рабочую версию 2.3.0 с лигой Пророчество. Он был привнесён каким-то рефакторингом текстового движка в целях поддержки тогда ещё грядущей Xbox-версии Path of Exile.

Симптомы

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



Во-вторых, отдельные буквы визуально отображались не тем символом:


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

Например: “Pevpf`^l D^j^db” -> “Physical Damage”, ^ заменяется на “a”.

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

Охота

Первый тикет по поводу этой ошибки был открыт 4 июня 2016 г. на основе отчётов с форумов сразу после запуска Пророчества. Самым большим препятствием было то, что мы не могли надёжно воспроизвести ошибку на наших компьютерах, где она появлялась только очень редко и произвольно. Насколько мне известно, на рабочей станции программиста мы её увидели только раз или два, а это ключевой фактор для того, чтобы проверить содержимое памяти и попытаться понять, что пошло не так. Пока у нас не было алгоритма воспроизводства ошибки, лучшее, что мы могли сделать – это пробовать гипотетические исправления и надеяться, что сообщения о проблеме прекратятся. Из-за невозможности что-либо найти и потому что это не критически важная ошибка, её приоритет был снижен, чтобы больше времени можно было уделить новым функциям и другим исправлениям.

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

  • Она касалась отдельных стилей шрифтов (комбинаций гарнитуры, кегля, полужирного/наклонного начертания), а не конкретных блоков текста или строк.
  • Она не выглядела как ошибка генерации или как искажение текстур или текстурного атласа, поскольку ни один из символов ни разу не был обрезан или разделён пополам. Редкий случай поимки этой ошибки на программистском копьютере также это подтверждал.
  • Отключение от сервера в большинстве случаев не устраняло проблему, только перезапуск клиента.
  • Я обратил внимание, что мы ни разу не получили отчёта о подобном на Xbox, PlayStation или MacOS, что в конце концов помогло мне с наибольшей точностью локализовать ошибку в конкретном участке текстового движка.

В районе запуска Нашествия об ошибке как будто стали писать чаще, и я сам стал чаще сталкиваться с ней во время моих собственных игровых сессий. Я отметил большую часть таких случаев, собрал изображения от игроков и начал строить некоторые догадки, но так и не нашёл лучшего способа воспроизведения ошибки, нежели “поиграйте подольше”. Несколько недель назад у меня выдался небольшой перерыв в задачах, и я решил ещё раз всерьёз попытаться, потратив несколько дней на полноценное погружение в текстовый движок с вычитыванием и полным пониманием всех его тонкостей.

Правка

Когда я глубоко влез в код текстового движка, я в конце концов наткнулся на следующую функцию:

SCRIPT_CACHE* ShapingEngineUniscribe::GetFontScriptCache( const Resources::Font& font )
{
    const auto font_resource = font.GetResource()->GetPointer();
    // `font_script_caches` here is a map of `const FontResource*` to `SCRIPT_CACHE` values*
    auto it = font_script_caches.find( font_resource );
    if( it == std::end( font_script_caches ) )
        it = font_script_caches.emplace( std::make_pair( font_resource, nullptr ) ).first;
    return &it->second;
}

*Перевод комментария: `font_script_caches` здесь отображает `const FontResource*` на значения `SCRIPT_CACHE`
Для не программистов: эта функция ссылается на конкретный ресурс шрифта и использует его расположение в памяти как ключ (искомое значение) для объекта данных SCRIPT_CACHE, создавая новую запись, если она отсутствует. Затем функция возвращает указатель к объекту SCRIPT_CACHE, что позволяет вызову функции менять сохранённый SCRIPT_CACHE вместо копии, изменения в которой не сохранились бы в отображении `font_script_caches`.
Объект SCRIPT_CACHE здесь является непрозрачным объектом данных, используемым библиотекой Windows Uniscribe (которой мы пользуемся только в Windows-версии клиента). Документация Uniscribe не даёт представления о том, какая информация фактически сохраняется этим объектом, только указывает требование к приложению сохранять по одному такому объекту для каждого используемого “стиля символов”. Хотя судя по эффектам бага с искажённым текстом, можно сделать вывод, что объект используется как минимум для кернинга и привязки символов к их изображениям.

На первый взгляд эта функция делает нечто совершенно оправданное. Видимо, поэтому проблему с ней и не замечали все эти годы. А обнаруживается она только тогда, когда понимаешь, что ресурсы шрифта могут быть выгружены нашим менеджером ресурсов, когда больше не используются. Ошибка затем проявляется, когда другой шрифт (другой гарнитуры, стиля и/или кегля) оказывается загружен менеджером ресурсов в тот же самый участок памяти, из-за чего новый шрифт задействует SCRIPT_CACHE старого.
Как только я это обнаружил, я провёл несколько тестов для подтверждения, что это и есть причина ошибки.

Принудительное назначение каждому шрифту одного и того же кэша немедленно привело к следующему при запуске игры:


Ура! Оба вида симптомов налицо, что заодно подтвердило их корень в одной и той же неисправности, а не в разных. После этого я смог естественным образом воспроизвести ошибку, нарочно загружая и выгружая как можно больше шрифтов, пока новый шрифт не займёт в памяти место старого:


Теперь, когда причина проблемы была известна, к её исправлению можно было подойти несколькими способами: можно отнести объект SCRIPT_CACHE к объекту Resource::Font, можно удалять старый SCRIPT_CACHE при каждой загрузке шрифта, либо можно заменить искомое значение с адреса памяти на зависящее от гарнитуры, кегля и стиля шрифта, которые, фактически, и придают шрифту уникальность. Сработает любой из вариантов, но у каждого есть свои плюсы и минусы, которые нужно взвесить исходя из того, как тот или иной подход укладывается в общую систему.

Итоги

Фактическая причина ошибки не так уж интересна, просто надо понимать, что адреса памяти могут и будут использоваться повторно. Поэтому надо быть внимательнее, когда/если используешь указатели памяти в качестве ключей. Однако этот баг мне запомнится своими особо странными симптомами, конкретной вредностью в том, чтобы его отследить, да и просто тем, что сидел в игре так долго. В некотором смысле мне даже будет его не хватать, ибо стало одной “великой загадкой”, над которой можно поломать голову, меньше. Наверное, надо просто найти себе следующую таинственную неполадку!

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


Прощай, T l e e v
Сообщение 
Grinding Gear Games
Ааа, вот оно что..
действительно интересная новость, спасибо, серьезно
по-больше бы таких вместо историй и концептов
Последняя редакция: I_GameR. Время: 1 июня 2022 г., 17:36:02
Это ещё раз подтверждает мой любимый тезис: "GGG - всё для людей, но не сразу".
Красавчики! Обожаю вашу игру. Новость классная, интересная. Хочется больше таких статей. "Как я ловил баг", "Нулевой день", "Как преодолеть творческий кризис".
Бокал шампанского этому любезному господину!
🍻🥂
Интересно, а баг с описанием гадальных карт, в которых буквы превращались в прямоугольники, сюда входит?
Молодцы
Один из худших интерфейсов которые только видели игроки, надеюсь в ПОЕ 2 додумаются сделать так, что бы людям не приходилось лезть в поб что бы посмотреть свой урон) Или урон приспешников, ну и вообще на информативностью интерфейса стоит поработать, он очень устарел, игре не мало годиков.
"
Opa_Zigota написал:
Интересно, а баг с описанием гадальных карт, в которых буквы превращались в прямоугольники, сюда входит?

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

Хорошая статья, почаще бы такое.
"Есть такой пунктик у некоторых метарабов, они считают что есть только единственно верный сетап для каждого скилла. Все кто собрался по своему это еретики, чушьбрингеры и вообще, я видел как они едят детенышей роа" © ThreeOEight
Всего 6 лет понадобилось. Еще немного подождём и исправят баг с посохом Шепчущий холод, который не могу пофиксить еще со времён гарены. Троекратное ура!
ХК'ашеры умственно отсталые и деградирующие особи.
Сорри, если вдруг кого обидел этим.
Просто такое мнение складывается при чтении чата.

Пожаловаться на запись форума

Пожаловаться на учетную запись:

Тип жалобы

Дополнительная информация