О декомпиляции фрагментных шейдеров Nvidia OpenGL и Vulkan.

Основной инструмент для его работы — nvcachetools от Theron Tarigo.

Это мой учебник/примечание для Linux, но он работает очень похоже на Windows, посмотрите страницу nvcachetools github, чтобы узнать больше об использовании его в других ОС.

Посмотрите пример использования и оптимизация шейдера в конце.

nvcachetools— извлекает шейдеры из указанного toc-файла кеша, обычно $HOME/.nv/GLCache/ драйвер Nvidia помещает сюда шейдеры независимо от API, как Vulkan, так и OpenGL.
Шейдер можно декомпилировать с помощью
nvdisasm( — бинарная опция) или с помощью этого дизассемблера с открытым исходным кодомenvytools, если у вас есть Maxwell или один из старых поддерживаемых графических процессоров.

Подробная инструкция и примеры использования ниже.

Инструменты

  1. Соберите nvcachetools.
  2. Создайте инструменты зависти.
    Альтернатива, проприетарное программное обеспечение, созданное Nvidia — установите CUDA или загрузите cuda-nvdisasm и извлеките файл nvdisasm.

Краткая инструкция

  1. ./nvcachedec nv_bin/*.toc objs
  2. ./nvucdump objs/object00000.nvuc sections
    или object00001.nvuc или другое число
    См. раздел Расширенное использование — имена файлов ниже.
  3. ./envydis -i -mgm107 sections/section4_0001.bin
    or /usr/local/cuda-11.8/bin/nvdisasm --binary SM87 objs/object00000.nvuc

Инструкция

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

Расположение кэша шейдеров по умолчанию: ~/.nv/GLCache

Например — удалите все из «кэша шейдеров по умолчанию» и запустите приложение OpenGL или Vulkan.
Если появится новая папка — это ваши скомпилированные шейдеры.

Такие приложения, как Google Chrome, используют собственное расположение кеша и шифруют его, поэтому его нельзя использовать. Используйте минимальные программы запуска.
У меня есть только программа запуска Vulkan — Vulkan-shadertoy-launcher. (для OpenGL нужно искать что-то минимальное)

Вам нужны файлы – это *.bin и *.toc файлы с одинаковыми именами.
Каждое уникальное имя файла — это все шейдеры для этого приложения.

Скопируйте файлы *.bin и *.toc в новую папку nv_bin в папке nvcachedec файла, просто чтобы получить результат от этого единственного приложения.

И используйте nvcachedecдля декомпиляции.
Затем вызовите nvucdump для извлечения разделов.
Затем envydis с параметрами, параметры можно найти в документации envydis и envyas страницу.
Или варианты usenvdisasm можно найти на портале документов nvidia.

Расширенное использование

Справочник по набору инструкций:

Чтобы рассчитать количество инструкций:

Из вывода nvdisasm.
Визуальный результат этой команды — на скриншотах ниже со статистикой количества инструкций на шейдер.

/usr/local/cuda-11.8/bin/nvdisasm --print-code --binary SM87 objs/object00000.nvuc | sed '1d' | sed -e 's/@[!|A-Za-z0-9]* / /g' | perl -p0 -e 's#/\*.*?\*/##sg' | sed "s/^[{|}| \t]*//" | sed 's/\s.*$//' | sort | uniq -c

Имена файлов в имена шейдеров

В случае использования моей Вулкан-шейдертой-лаунчера
Имена файлов для команды ./nvucdump objs/object<NUMBERS>.nvuc sections
Или/и для команды nvdisasm

Только если код в каждом шейдере уникален:

  • object00000.nvuc is shaders/shadertoy/buf0.glsl
  • object00001.nvuc is shaders/src/buf.vert
  • object00002.nvuc is shaders/shadertoy/buf1.glsl
  • object00003.nvuc is shaders/shadertoy/buf2.glsl
  • object00004.nvuc is shaders/shadertoy/buf3.glsl
  • object00005.nvuc is shaders/shadertoy/main_image.glsl
  • object00006.nvuc is shaders/src/main.vert

Но если код шейдера один и тот же — шейдеры будут скомпилированы в один файл *.nvuc.

Например, при загрузке vulkan-shadertoy-launcher_linux.zip используется этот шейдер шейдеров.
Каждый шейдер буфера и шейдер изображения в этой Shadertoy уникаленпорядок файлов будет соответствовать указанному выше порядку.
За исключением
двух вершинных шейдеров buf.vert и main.vert — они оба будут в файле object00001.nvuc.

И для empty_template_shadertoy.zip, которые используют только Новый шейдер Shadertoy в шейдере изображения.
Код шейдера в buf0-3.glsl и вершинные шейдеры одинаковы.
Поэтому будет всего 3 *.nvucфайла, смотрите скриншот:

Я не знаю, почему у файлов такие имена. (без понятия)
в OpenGL порядок имен файлов зависит от приложения, и иногда в именах бывают огромные пробелы.
Я видел — objs/object00016.nvuc был моим первым фрагментным шейдером , а затем objs/object00019.nvuc — это второй фрагментный шейдер, когда файлов 00017-18 не существует, и много подобных «пробелов» в именах файлов.

Пример использования

Чтобы увидеть источник серьезного замедления в шейдерах:

Основное замедление в шейдере может быть связано с инструкцией STL — Store to Local Memory, которая в основном используется для больших массивов в шейдерах и/или когда массивы копируют себя в качестве аргумента.

А декомпилировав скомпилированный бинарный шейдер, можно увидеть количество STL вызовов в скомпилированном коде:

Этот тестовый шейдер со скриншота — Shadertoy link STL замедление.
Эта ошибка/замедление работает только на Nvidia в OpenGL.

Оптимизация STL в связанном выше шейдере, сделанном Theron Tarigo.

Сравнение одного и того же шейдера GLSL-кода, скомпилированного в OpenGL и Vulkan:

В случае с Nvidia — компилятор шейдеров OpenGL в драйвере Nvidia может быть намного хуже, чем компилятор шейдеров для Vulkan в драйвере. Nvidia имеет много ошибок, связанных только с OpenGL, мой список ошибок перечисляет некоторые ошибки, связанные только с OpenGL.

Шейдер со скриншота выше (с замедлением STL) отлично работает и так же быстро (как и его оптимизированная версия) на Nvidia в Vulkan, так что — давайте посмотрим скомпилированный код этого же шейдера в Vulkan:

Этот тестовый шейдер на скриншоте — Shadertoy link STL замедление.

Понятно, что только шейдер, не оптимизированный под OpenGL, использует STL.

Код Vulkan почти одинаков для обоих шейдеров, возможно, потому, что glslangValidator оптимизирует код шейдера, поэтому оптимизация неоптимизированного шейдера происходила на стороне glslangValidator.

Скомпилированный код оптимизированной версии шейдера в Vulkan и OpenGL различается.

STL всегда плохо! (всегда лучше меньшие массивы и меньшее количество операций чтения/записи массива)

Но STL не всегда плох! (не всегда основной источник замедления)

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

Пример такого шейдера и тестируемого — мой шейдер GLSL Auto Tetris.
На скриншотах ниже — шейдер от BufA при работе ИИ (#define no_AI не задано)

Версия OpenGL использует инструкции 3600+ STL, но шейдер работает одинаково быстро в OpenGL и Vulkan (очень похожий FPS, даже на младших графических процессорах Nvidia).

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

Оптимизация этого моего шейдера GLSL Auto Tetris:

Основное ускорение производительности — переключение с использования массива int[220] на uint[7] .
Я использую int[220] для хранения 220 бит, поэтому для его хранения достаточно семи единиц.

Поиск общего в связанном шейдере Shadertoy — строка 11 #define use_uint_map
Раскомментируйте это определение, чтобы использовать новую карту.

Я вижу примерно 5-кратное ускорение(в OpenGL) — если вы установите #define AI 0 в Common для проверки максимальной нагрузки шейдера.
В Vulkan — я думаю о ~ В 2 раза быстрее за счет использования меньшего массива.

Анализируйте и оптимизируйте нейронные (ML) шейдеры:

Этот шейдер — Нейронная сеть карты освещения кофейной чашки работает у меня (на Nvidia) с 2–5 FPS в превью 800x450. (да, потому что я использую низкопроизводительный графический процессор Nvidia, на графическом процессоре AMD или лучше Nvidia этот шейдер работает также быстро)

При этом другой очень похожий по объему данных шейдер — Suzanne Neural Light Field работает с 60 FPS даже в полноэкранном режиме 1080p.

Давайте сравним.
(Сравниваю только в Вулкане, у меня работает с одинаковым FPS в Вк и OGL)

Из этого я могу сделать вывод:

  • Это не потому, что в инструкции sin/cos одинаковое количество sin/cos в обоих шейдерах.
  • Использование FFMAFP32 — инструкции Fused Multiply и Add примерно в 2 раза больше в первом шейдере.

Это означает, что если я просто выполню второй шейдер дважды (шейдер Suzanne Neural Light Field), производительность шейдера упадет до 2–4 FPS, как и у первого шейдера. …
Давайте проверим:

Все еще 60 FPS
Скомпилированный код:

Почти все умножается на два, даже sin/cos выполняются дважды, но производительность не падает совсем.
Все еще 60 FPS, когда первый шейдер (нейронная сеть карты освещения кофейной чашки) работает очень медленно производительность 2–4 кадра в секунду в превью при том же количестве инструкций.

Единственная разница между этими двумя шейдерами — это количество внутренних данных в этих константах mat4.

Нейронная сеть карты освещения кофейной чашки — использование 7x8x8 (этот блог Jure Triglav объясняет некоторые из них) составляет 448 из mat4 —28 Кбайт данных. (на самом деле сохранено 0x1ef4 байт или 7 Кбайт)

Suzanne Neural Light Fieldиспользуйте модель 16x16, которая содержит 256 констант mat4 в коде шейдера или 16 Кбайт данных в константах. (фактически сохранено 0x43c байт или около 1 КБ)

Я думаю, что моя проблема с производительностью GPU — это «слишком много констант».

Мой вывод из всего этого — около одного или двух килобайт CONST — это предел «хорошей производительности» для моего графического процессора Nvidia.

1 Кбайт — это 256 уникальных значений с плавающей запятой.

Моя попытка исправить это:

У меня есть очевидная и простая идея — преобразовать каждое из уникальных поплавков в некоторый предельный диапазон, а затем заменить исходное значение на сжатое.
Этот простой скрипт на питоне cfloats.py используется для обработки поплавков.

Результат:

Я получил 60 кадров в секунду в предварительном просмотре 800x450 и 30 кадров в секунду в полноэкранном режиме, из исходных 2–4 кадров в секунду в предварительном просмотре и 0 кадров в полноэкранном режиме 1080p.

Оптимизация — просто замена констант с плавающей запятой в коде на другие константы с плавающей запятой, например, 0.011 и 0.012 становятся одинарными 0.011, чтобы использовать меньше места в памяти CONST.

Окончательный оптимизированный шейдер — Оптимизированный ML/нейронный шейдер.

Для этого шейдера я использовал b_scale = 132.0 в скрипте Python cfloats.py для масштабирования плавающих элементов.

Я тестировал с меньшими значениями, а с большими — даже с 1/64 или 1/32, так как производительность шага с плавающей запятой почти такая же, как и при 120–150(b_scale). Когда b_scale равно 180+, при предварительном просмотре частота падает до 30 кадров в секунду.
Похоже, когда размер CONST достигает 3 Кбайт+, мой графический процессор Nvidia становится намного медленнее по сравнению с 1–2 Кбайтами CONST.

Да, есть некоторые потери качества:

Я думаю, что его можно использовать для оптимизации шейдеров машинного обучения/нейронных вычислений для слабого оборудования.
Как старые графические процессоры, которые все еще поддерживают современные графические API, но становятся слишком медленными для обработки современного объема данных.

Наслаждайтесь отладкой xD