Недавно столкнулся с весьма неприятным багом в движке The Great Tribes.
После обновления драйвера AMD игра начала намертво зависать при перемещении камеры, особенно когда на карте открывалась большая территория. Иногда зависал графический драйвер, иногда JVM падала внутри atio6axx.dll.
Самое интересное — на старом драйвере и других видеокартах всё работало нормально!
Начались поиски. Под подозрение попало практически всё: многопоточность OpenGL, VAO, VBO, индексные буферы, тени, модели юнитов и даже glfwSwapBuffers().
Постепенно удалось выяснить, что зависание вызывает отрисовка поверхности карты. А затем внимание переключилось на фрагментный шейдер.
В нём обнаружилась вот такая конструкция:
int segment = (3 - int(min(3, outTexCoord.y * 4.0))) * 4 + int(min(3, outTexCoord.x * 4.0));
Переменная segment использовалась для обращения к массивам из 16 элементов. Проблема заключалась в том, что координаты ограничивались сверху, но не снизу. При отрицательных текстурных координатах индекс мог выходить за пределы массива.
Исправление оказалось удивительно простым:
ivec2 cell = clamp(ivec2(floor(outTexCoord * 4.0)), ivec2(0), ivec2(3)); int segment = (3 - cell.y) * 4 + cell.x;
И зависания исчезли!
Получилась отличная иллюстрация неопределённого поведения в GLSL. Старый драйвер каким‑то образом справлялся с некорректным обращением к массиву, другие видеокарты тоже не показывали проблемы, а новая версия драйвера AMD превратила его в настоящий кошмар.
Мораль истории: если ваш шейдер прекрасно работает на одной видеокарте, это ещё не означает, что в нём нет ошибок.
А иногда за многочасовой отладкой собственного 3D‑движка скрываются всего две строчки кода. ?
P. S. Я уже десять лет разрабатываю собственный игровой движок на Java, LWJGL и OpenGL для стратегии The Great Tribes. За это время накопилось немало интересных историй — от многопоточности рендеринга до оптимизации огромных игровых карт и работы с графическими драйверами.
Если вам интересна внутренняя кухня разработки собственного 3D‑движка без Unity и Unreal, напишите в комментариях. Возможно, стоит сделать из этого небольшую серию технических статей.:‑)
Комментарии (8)

pvvv
28.10.2026 18:26segment &= 0xF;

Zemlaynin Автор
28.10.2026 18:26Результат не будет эквивалентен clamp(). При outTexCoord.x == 1.0 и outTexCoord.y == 0.0: clamp(): cell = (3, 0), segment = 15. & 0xF: cell = (4, 0), segment = 16 -> после маски — 0.

pvvv
28.10.2026 18:26Естественно не будет, это ограничение с переполнением, а не с насыщением, но просто это "стандартный" способ ограничивать индексы особенно если они приходят непонятно откуда и надо быть уверенным что за границы массива не вылезем.

DimPal
28.10.2026 18:26Я правильно понял? Ошибка в GPU-шейдере привела к повисанию CPU-процессов? Фрагментный шейдер как то зациклился?

Zemlaynin Автор
28.10.2026 18:26Да, в целом правильно :) Но сам фрагментный шейдер, скорее всего, не зацикливался, ошибка приводила к обращению к массиву по недопустимому индексу, что вызывало неопределённое поведение. Могу только предположить, из-за этого зависало выполнение команд на GPU или возникала проблема в драйвере, а CPU ложился в ожидании glfwSwapBuffers.

DimPal
28.10.2026 18:26Честно говоря, именно это и было интересно узнать - как обращение шейдера мимо GPU-памяти вызывает повисание CPU-процессов. Как исправить адрес (поставив clamp ) - это и так понятно (статья получилась именно про это). А вот какие процессы вызывает Memory violation, как это найти, отладить и обезопасить код от зависания в будущем, вот это я бы почитал.
ArkadiyMak
Zemlaynin Автор
fract(1.0) возвращает 0.0, не пойдет.