Уже больше 5 лет как использую разные GPU от AMD и сталкиваюсь с разнообразными проблемами, приводящими к его зависанию. К сожалению, в современном линуксовом десктопе, особенно на AMD зависание GPU зачастую не получается обработать прозрачно для софта. В лучшем случае могут попортиться данные, связанные с активными задачами, в худшем - gpu вообще становится неработоспособен до снятия питания (этим грешили некоторые APU на gfx9)
Успешный GPU Reset
Если в случае с неработоспособным GPU другого выхода, кроме как отказ от него нет, дискретные GPU обычно могут успешно пройти сброс.
На моём опыте, с rx580 это приводило к превращению всего содержимого VRAM в шум, на котором слегка можно было различить контуры ранее находившихся там текстур.
На rdna2 же обычно содержимое vram остаётся целым, однако не всегда. С некоторой вероятностью обнаруживается периодический мусор после сброса, обычно в этом случае попытка использовать работающие с GPU приложениями приведёт к повторному зависанию. В общем, никаких гарантий нет и по хорошему все GPU клиенты должны пересоздать все контексты с нуля
Direct Rendering Interface - как это работает
Чтобы вывести текстуру на экран, кто-то должен её разместить в определённом прямоугольнике, с учётом возможного перекрытия другими окнами. Для этого есть разные способы. Где-то раньше применялись маски и color key, где-то просто оверлей - всё это было частью 2D ускорения.
Для indirect рендеринга (AIGLX) всё просто - там рисует opengl на экран сам X11 и может использовать встроенные возможности драйвера по указанию ViewPort. Для direct rendering же всё несколько сложнее.
В DRI2 иксы открывают устройство, создают текстуру для вывода на экран и отдают буфферы на клиент через GEM flink. Так же иксы авторизуют доступ к устройству. Сильно не углублялся в первую версию DRI, но вероятно там вместо gem handle драйвер оперировал физическими адресами напрямую. DRI3 сильно упрощает этот механизм. В эпоху DRI3 у процессов есть доступ к GPU независимо от x-сервера - они могут рендерить всегда. Иксы только реализуют вывод на экран. Причём приложение может как запросить буффер у иксов, так и отправить туда буффер уже отрендеренной текстуры.
Остаётся вопрос - как же эту текстуру поместить в окне?
Ситуация с 2D ускорением
В отличие от других ОС, в linux решили отказаться от 2D ускорения. Это достаточно печально, так как в отличие от opengl композитинга и glamor, 2d ускорение могло бы совсем не зависеть от состояния внутри GPU. Обычно реализации 2D ускорения представляют из себя некоторый stateless command buffer, который ни от каких внешних факторов не зависит. В общем, вероятно, будь у нас 2д ускорение - можно было бы просто перезапустить софт, который привёл к сбою и продолжать работать.
Наиболее активно используемой реализацией 2D ускорения является glamor, работающий поверх opengl. Это чем-то напоминает композитный менеджер, но не умеющий прозрачность и работающий прямо внутри иксов. Сейчас же все xf86-video драйвера, реализующие 2д ускорение, заброшены (intel не работает на последних gpu, nouveau работает даже хуже modesetting и не может в dri3), а разрабы рекомендуют использовать modesetting или вообще wayland. Для amdgpu 2d ускорения и не было никогда, а отключение glamor на amdgpu отключит и dri3. А в wayland такого ускорения вероятно никогда и не будет, т.к добавить его сразу во все реализации, да ещё и для всех GPU практически нереально. Да и ускорять в wayland нечего - приложения уже отдают/получают отрисованные поверхности
Отключение glamor - что произойдёт?
Возможно с этим сталкивались те, кто пробовал использовать xf86-video-intel на последних поколениях, совместимых с i915. Там отключается ускорение и с драйвером iris это может приводить к некорректной отрисовке.
Если включить dri3 «в лоб» - мы увидим opengl текстуры, но они не будут обновляться, так и оставшись на первом кадре. То же самое происходит, если принудительно включить dri3 в коде amdgpu. Причина же в том, что нет возможности прочитать текстуру из CPU напрямую, а при отключении ускорения иксы используют CPU путь.
msdri - патчи, которые так и не смержили
С проблемой столкнулись и на raspberry pi, только там причиной нестабильности драйвера был лимит CMA памяти в 256мб:
https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/945
Реализация GBM на Raspberry PI немного отличается. потому там включение dri3 помогло.
Вероятно, из-за того, что патч не работает на других GPU, его так и не смержили, хотя этот патченный xorg продолжает использоваться в Raspberry OS
TigerVNC - работоспособная, но медленная реализация
В комментариях к предыдущему патчу отметился разработчик TigerVNC, в котором проблема уже решена. Его реализация dri3 перед каждой операцией отрисовки копирует текстуру целиком на CPU буффер, вызывает программную отрисовку, после чего копирует обратно. Медленно, но как-то работает. В целом этого уже достаточно, чтобы пользоваться системой с иксами и не бояться GPU reset. Но не совсем удобно конечно, особенно если dri3 нужен для игор или их разработки.
Out-of-tree драйвер. Эксперименты с GBM
xf86-video-modesetting зачем-то был вмержен в основное дерево Xorg. Цели этого неясны, но работать с in-tree драйвером куда сложнее.
Потому первое что было предпринято - код из ветки с msdri3 был перенесён поверх старого xf86-video-modesetting на autotools. Почему не meson? ИМХО, meson вместо того, чтобы исправить проблемы autotools, перенял их, потому тратить время на то, чтобы с ним разобраться того не стоит. Обе системы сборки максимально плохо выполняют свою задачу и потягаться с ними наверно может только SConstruct.
Что получилось? После пары правок, modesetting собрался. Ещё пара правок и заработал даже glamor.
Но с AccelMethod=msdri3
пусть приложения и запускались, на экране застрявал первый выведенный кадр.
Дальше я начал пытаться вместо GBM пытаться замаппить текстуру напрямую. Если на intel это даже срабатывало (пусть и с неправильным тайлингом), на amdgpu mmap прсото падал. Всё верно - текстура не была аллоцирована как CPU_VISIBLE. Да если бы и была, то читалась бы она со скоростью около 50 мегабит в секунду - доступ к некэшированной памяти очень медленный. Так же я попробовал amdgpu-шные ioctl для маппинга, но они тоже не помогли, да и хочется получить универсальное решение. Обновил исходники modesetting до последней версии (позже выяснил, что это было ошибкой) и решил начать делать с нуля.
Доступ к текстурам из CPU - как это работает
Все текстуры представляются на уровне ядра как dmabuf, который передаётся между процессами в виде файлового дескриптора.
Внутри процесса тексстура может быть представлена помимо дескриптора как prime handle и gem flink.
gem flink - тоже глобальный дескриптор текстуры, но привязанный к конкретному GPU - этот способ появился ещё до унификации текстур в dmabuf, в эпоху dri2, а prime handle валиден только для конкретного открытого дескриптора из /dev/dri - то есть это дескриптор уже открытого gem объекта. В dri2 текстуры передавались только от иксов в клиент через gem flink, в dri3 же в обе стороны.
Для унификации и работы с этими самыми gem’ами используется GBM - то есть каждая текстура для нас представляет пару dmabuf fd и непрозрачную структуру gbm_bo. В более новых версиях появились drm modifier, определяющие формат и способы работы с текстурой, но реализовывать их пока не обязательно. Так же GBM реализует аллокацию, импорт, освобождение текстур, но делается всё это не через стандартные DRM API, а через приватные реализации для конкретного GPU.
Конечно использовать GBM - не лучший способ, т.к он всё равно лезет в реализацию opengl (но не использует сам opengl, а оперирует с теми stateless API, что предоставляет drm драйвер).
К сожалению, никаких API для чтения/записи/flush текстур нет, однако если присмотреться к описанию флагов для gbm_bo_map:
/**
* Flags to indicate the type of mapping for the buffer - these are
* passed into gbm_bo_map(). The caller must set the union of all the
* flags that are appropriate.
*
* These flags are independent of the GBM_BO_USE_* creation flags. However,
* mapping the buffer may require copying to/from a staging buffer.
*
* See also: pipe_map_flags
*/
enum gbm_bo_transfer_flags {
/**
* Buffer contents read back (or accessed directly) at transfer
* create time.
*/
GBM_BO_TRANSFER_READ = (1 << 0),
/**
* Buffer contents will be written back at unmap time
* (or modified as a result of being accessed directly).
*/
GBM_BO_TRANSFER_WRITE = (1 << 1),
/**
* Read/modify/write
*/
GBM_BO_TRANSFER_READ_WRITE = (GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE),
};
void *
gbm_bo_map(struct gbm_bo *bo,
uint32_t x, uint32_t y, uint32_t width, uint32_t height,
uint32_t flags, uint32_t *stride, void **map_data);
Становится понятно, почему трюк из msdri3 не сработал: GBM_BO_TRANSFER_READ_WRITE
гарантирует только последовательность read-modify-write.
То есть нам не нужно копировать всю текстуру вручную, достаточно замаппить её через GBM_BO_TRANSFER_READ_WRITE на время операции записи, а так же повторно маппить с GBM_BO_TRANSFER_READ на время чтения.
Первая реализация - заставляем GL/Vulkan рисоваться
Для простоты вместо актуальной версии dri3 я указал самую первую - она требует предоставить всего 3 функции - открытие девноды, импорт внешнего дескриптора как Pixmap и получение дескриптора для существуюшего Pixmap. Для отрисовки opengl приложений обычно хватает первых двух.
Открытие ноды взял как есть из msdri3:
Остальные изменения из msdri3 пока не пригодились, sync заработал просто включением стандартного SyncShm и этого достаточно для работы приложений
Заставить текстуры обновляться оказалось проще простого - перед чтением текстуры исполняется screen->SourceValidate
- в нём можно и подготовить текстуру к чтению.
И вот тут то я столкнулся с проблемой. Если я просто заменяю SourceValidate на свой - всё работает как надо и текстура обновляется, но стоит мне вызвать оригинальную функцию - мой враппер перестаёт работать… Но в любом случае, без вызова оригинальной функции текстура уже начала обновляться
Страшные костыли в xfree86 и почему (возможно) разрабы ушли на wayland
Был убит целый день, на то, чтобы разрбраться почему врапперы не работают. Так же я пытался враппить функции отрисовки, но безуспешно.
В конце концов я выяснил, что SourceValidate переписывает адрес на какой-то другой. Гугление привело вот к такому врапперу:
https://cgit.freedesktop.org/xorg/xserver/commit/?id=b89e6dbdfbb50e3b5bc7fcb7eccc397c467c92f8
Чуть позже я выяснил, что отсальные врапперы не работают по этой же причине - только вот переписывание их повторно вообще отправляло иксы в вечную рекурсию.
Да, вызов каждой функции отрисовки может переписывать функции в Screen по нескольку раз. Это создаёт хитрые и довольно сложные для понимания чейны вызовов - ты никога не знаешь, какой модуль будет дальше и что он будет делать - одни модули переписывают функции, другие просто вызывают не переписывая, из-за чего и получалась рекурсия…
Вторая проблема, которая тоже отняла много времени - это сломанный код в апстриме.
Ещё несколько лет назад, чтобы исправить тиринг (которого нет на специализированных дровах вроде amdgpu, но есть на modesetting) в иксы начали внедрять TearFree, попутно ломая внутренние API.
И пусть сам TearFree отключается (мне пришлось его выключить из-за более старой версии xorg в системе), изменения затрагивают и обычный код обновления/vblank…
И судя по всему, даже несмотря на наличие таких прекрасных инструментов, как address sanitizer, эти новые изменения никто не проверял. Когда мне надоело ловить странные зависания и переписывания указателей и я начал запускать xorg с предзагруженным санитайзером, нашлось несколько use-after-free. И пусть мне кое-как удалось их заткнуть (не уверен в корректности логики), я в итоге вернулся на старую ветку, с которой экспериментировал ранее. Да, xf86-video-modesetting в апстриме не готов, не используйте его по возможности.
Тиринг, композитинг, отрисовка
Когда олучилость заставить dri3 поверхности обновляться, столкнулся с другой проблемой - захват окон через xcomposite не работает. Да и вообще, gl окна рисуются с постоянным тирингом, который на софтовой отрисовки вряд ли получится исправить.
С полноэкранными окнами дела обстоят несколько проще - для них предусмотрен механизм PageFlip. Когда окно покрывает всю поврехность экрана. драйвер отправляет его текстуру напрямую в kmsdrm, тем самым минуя программную отрисовку. Это уже было реализовано в msdri3, потому сделал по аналогии - glxgears -fullscreen стал рисоваться без тиринга.
Таким образом, я могу использовать иксовые opengl композиторы и тогда оконные приложения тоже смогут избежать тиринг - надо только починить функцию редиректа окон в composite
В dri3 это отчевает третья функция - pixmap_to_fd - именно она вызывается, когда клиент делает XCompositeNameWindowPixmap чтобы представить текстуру чужого окна в opengl.
Причём мало того, что она должна экспортировать существующую пиксмапу как текстуру, так ещё и держать её в актуальном состоянии.
Тут то мне и потребовалось перегружать всю отрисовку. Чтобы понять, какие функции могут писать пиксмапы, пришлось подсмотреть в код glamor и его врапперы для amdgpu. А так же куча времени потрачена на попытки не переписывать врапперы, но в итоге просто была написана общая функция, возвращающая указатель на момент вызова, на подобии того враппера для SourceValidate:
// call function, but restore original pointer (some methods may replace API)
#define CallWrap(real, saved, func) do { void *tmp = real; real = saved, func, saved = real, real = tmp;} while(0)
Композитинг заработал, но производительность оставляет желать лучшего - в окне терминала konsole (версия из TDE) отрисовка каждого символа дёргает маппинг на запись, а потом обратно на чтение, тем самым вызывая аж 3 копирования тексуры целиком. Это можно попытаться оптимизировтаь, проверяя состояние текстуры и пропуская синхронизации, но здесь возможен и другой путь.
DUMB Buffer и прямой маппинг памяти
В отличие от полученных от приложений текстур, текстуру для иксовых пиксмап выделяет сам иксовый драйвер. И она может быть CPU_VISIBLE и маппиться напрямую, а не через staging buffer.
GBM так сделать не позволяет, однако он позволяет импортировать текстуру созданную другим способом.
И этот способ есть в KMSDRM устройствоах - DRM_IOCTL_MODE_CREATE_DUMB
. Это универсальное API для аллокации текстуры с DMABUF, который можно читать и писать из CPU, но к сожалению только в синхронном режиме. Текстура, выводимая на экран в modesetting без glamor создаётся именно через него.
Первая мысль была - замапить dumb object напрямую для всех текстур, для которых получается pixmap’а… Но не тут то было - когда я это сделал, после запуска композитора иксы зависли в memcpy
навечно…
Да, чтение без кэша настолько медленное, что пришлось подключаться по ssh и прибивать иксы удалённо - пускай там и 50 мегабит в секунду, вывод нескольких кадров без композитинга копировал много текстур и застопорился надолго…
К сожалению, DRM api не имеет универсального способа управление кэшем. Есть приватные команды для конкретных GPU, есть кастомные dmabuf драйверы, но через сам drm включить кэширование нельзя.
Потому как обходной способ я стал включать dmabuf только на время записи, для чтения возвращая старый CPU указатель, который иксы обновят при необходимости через SourceValidate.
Производительность, конечно, всё равно неидеальная, konsole всё ещё подтормаживает, но не так дико, как с лишними копирвоаними текстуры.
Ну и конечно не хватает какого-либо ускорения. В идеале хотя бы замаппить текстуры через Vulkan, при необходимости откатываясь на программную реализацию. Даже для маппинга текстур он предоставляет куда большие возможности, чем GBM.
Исходный код получившегося драйвера доступен по ссылке:
https://git.disroot.org/mittorn/xf86-video-modesetting/src/branch/softdri3-old Это вынесенный обратно в out-of-tree modesetting драйвер, поскольку с ним так будет проще работать. Для установки не обязательно ставить в систему, можно установить прямо в хомяк:
./configure --prefix /home/user/xf86-video-modesetting-prefix --with-xorg-module-dir=/home/user/xorg-modules
make install
И прописать в xorg-conf
Section "Files"
ModulePath "/home/user/xorg-modules"
ModulePath "/usr/lib64/xorg/modules"
EndSection
Другие изменения
Так как изначальная задача касалась работы в условиях возможного GPU reset, с ним есть и другие нюансы.
К сожалению, GPU reset не всегда происходит успешно. Возможно, из-за багов в прошивке или драйвере SMU, а возможно из-за особенностей конкретного GPU, часто через 3-4 секунды после gpu reset, резетилась железно вся система, если софт продолжал что-то рендерить.
Т.к я использую довольно старое ядро и не хочу пока обновлять, немного попатчил его, чтобы обойти проблему:
diff --git a/drivers/gpu/drm/amd/amdgpu/amdgpu_device.c b/drivers/gpu/drm/amd/amdgpu/amdgpu_device.c
index 49f734137f15..ba67bdd65b47 100644
--- a/drivers/gpu/drm/amd/amdgpu/amdgpu_device.c
+++ b/drivers/gpu/drm/amd/amdgpu/amdgpu_device.c
@@ -4794,7 +4795,7 @@ int amdgpu_do_asic_reset(struct list_head *device_list_handle,
if (r)
goto out;
- vram_lost = amdgpu_device_check_vram_lost(tmp_adev);
+ vram_lost = false; //amdgpu_device_check_vram_lost(tmp_adev);
if (vram_lost) {
DRM_INFO("VRAM is lost due to GPU reset!\n");
amdgpu_inc_vram_lost(tmp_adev);
@@ -5060,7 +5061,9 @@ int amdgpu_device_gpu_recover_imp(struct amdgpu_device *adev,
/*
* Special case: RAS triggered and full reset isn't supported
*/
- need_emergency_restart = amdgpu_ras_need_emergency_restart(adev);
+ need_emergency_restart = 0; //amdgpu_ras_need_emergency_restart(adev);
+ if(amdgpu_ras_need_emergency_restart(adev))
+ DRM_WARN("EMERGENCY REBOOT REQUESTED!!!");
/*
* Flush RAM to disk so that after reboot
@@ -5072,9 +5075,17 @@ int amdgpu_device_gpu_recover_imp(struct amdgpu_device *adev,
ksys_sync_helper();
emergency_restart();
}
+
dev_info(adev->dev, "GPU %s begin!\n",
need_emergency_restart ? "jobs stop":"reset");
+ {
+ char path[] = "/gpu_reset_helper.sh";
+ char *argv[] = {path, NULL};
+ char *envp[] = {NULL};
+ int ret = call_usermodehelper(path, argv, envp, UMH_WAIT_PROC);
+ printk("helper script ret=%d\n", ret);
+ }
if (!amdgpu_sriov_vf(adev))
hive = amdgpu_get_xgmi_hive(adev);
Хотел сначала на pastebin залить, но тот устроил мне DoS атаку через cloudflare и часть статьи была утеряна :(
- Во-первых, выставляю vram lost всегда в 0, чтобы софт мог продолжать работаь
- Во-вторых, вызываю usermode helper перед началом резета.
Хелпером является специальный скрипт, который останавливает все процессы и переключает VT. Делался он ещё до софтовой реализации dri3, но с ней работает надёжнее.
Пример скриптов, заточенных под используемый софт
/gpu_reset-helper_sh
#!/bin/sh
echo reset >> /tmp/reset_helper_log
/sbin/start-stop-daemon -S -b -x /emergency-chvt.sh
for p in $(lsof -t /dev/dri/card0 /dev/dri/renderD128); do
case $(readlink /proc/$p/exe) in
/usr/bin/Xorg)
echo skipping Xorg >> /tmp/reset_helper_log
;;
*hlvr*|*.exe|*chamfer*|*monado-service|*wivrn-service|*ovr-utils*)
echo killing $p $(/usr/bin/readlink /proc/$p/exe) >> /tmp/reset_helper_log
kill -9 $p
;;
*)
echo stopping $p $(/usr/bin/readlink /proc/$p/exe) >> /tmp/reset_helper_log
kill -STOP $p
;;
esac
done
echo done >> /tmp/reset_helper_log
sleep 0.5
emergency-chvt.sh
#!/bin/sh
/usr/bin/chvt 1
sleep 0.6
export DISPLAY=:0
for p in $(lsof -t /dev/dri/card0 /dev/dri/renderD128); do
echo $p
if [ $(cat /proc/$p/status|grep State:|cut -f2 |cut -d ' ' -f1) = "T" ]
then echo T;for wid in $(/usr/bin/xdotool search --pid $p --onlyvisible); do /usr/bin/xdotool windowminimize $wid; done
fi
done
После gpu reset необязательные процессы вроде игор прибиваются, а все остальные останавливаются и по возможности сворачиваются.
Свёрнутые остановленные окна twin (из TDE) автоматически предалгает развернуть с SIGCONT.
После последних изменений в убиваемые приложения скорее всего добавится ещё и композитор