Форум Игромании

Форум Игромании (http://forum.igromania.ru/index.php)
-   Программирование (http://forum.igromania.ru/forumdisplay.php?f=243)
-   -   [статья] Путешествие сквозь графический конвейер, часть 1. (http://forum.igromania.ru/showthread.php?t=131746)

[CCCP] Monster 19.10.2012 02:35

[статья] Путешествие сквозь графический конвейер, часть 1.
 
Оригинал:
http://cccp-monster.livejournal.com/565.html

Скрытый текст:

Пользовательское приложение.
Это ваш код. И также ваши баги. Правда. Да, в API-библиотеках времени выполнения и драйверах есть баги, но вот это - не они. А теперь идите и почините их уже.
API-библиотеки времени выполнения.
Вы выполняете создание своих ресурсов, изменение свойств состояния, вызовы отрисовки через API. API -библиотека отслеживает состояние системы, которые вы установили, проверяет параметры и выполняет другие проверки на ошибки и корректность состояния, управляет видимыми пользователю ресурсами, проверяет или не проверяет код шейдеров и линковку шейдеров (вернее, D3D проверяет, в OpenGL это делается на уровне драйвера), возможно выполняет еще какую-то работу, и потом передает это все в драйвер - если точнее, то в драйвер, работающий в пользовательском режиме.
Графический драйвер для пользовательского режима (UMD - User-Mode Driver)
Вот где происходит большая часть "магии" на стороне CPU. Если ваше приложение валится из-за какого-то вызова API, то это случается обычно здесь. Этот драйвер называется "nvd3udm.dll" (для NVidia) или "atiumd.dll" (для ATI). Название намекает на то, что это код, выполняющийся в режиме пользователя. Он выполняется в том же контексте и адресном пространстве, что и ваше приложение (и библиотека времени выполнения), и не имеет никаких повышенных привилегий. UMD реализует API нижнего уровня (DDI), который вызывается Direct3D. Этот API вполне похож на тот, что вы видите на поверхности, но он более явно управляет памятью и другими подобными вещами.
Этот модуль - как раз то место, где происходят вещи вроде компиляции шейдеров. D3D передает предварительно проверенный поток токенов шейдера в UMD - т.е. он уже проверен на предмет валидности кода, в плане синтаксической корректности, следовании ограничениям D3D (использовании правильных типов данных, не использовании большего количества текстур/сэмплеров, чем это возможно, не превышении максимального числа константных буферов и всему такому прочему). Все это компилируется из кода HLSL и уже имеет какое-то количество высокоуровневых оптимизаций (оптимизации циклов, вырезание мертвых участков кода, распространение констант, предикация переходов и т.д.) - хорошие новости, так как это означает выигрыш в производительности со всех этих сравнительно дорогих операций, выполняемых прямо во время компиляции. Однако, код также подвергается некоторым низкоуровневым оптимизациям (таким, как распределение регистров и размотка циклов), которые драйверам бы лучше делать самостоятельно. Короче, здесь код обычно сразу превращается в промежуточное представление (IR - Intermediate representation) и компилируется еще немножко. Система команд шейдров достаточно близка к байткоду D3D, так что в процессе компиляции не приходится делать какие-то чудеса, чтобы достигнуть хороших результатов (и то, что HLSL-компилятор уже провел большинство этих высокоуровневых оптимизаций, несомненно помогает), но остается еще много низкоуровневых нюансов (таких как аппаратные ограничения и ограничения планирования), о которых D3D не знает да и не заботится, так что это не тривиальный процесс.
И конечно, если ваше приложение - какая-то знаменитая игра, программисты Nvidia/AMD уже посмотрели на ваши шейдеры и написали оптимизированные вручную подмены кода для своего оборудования - это позволит выдавать те же результаты быстрее, и избежать скандала:). Эти шейдеры тоже детектируются и подменяются с помощью UMD. Пожалуйста.
Еще веселее: некоторые состояния API вообще могут компилироваться в шейдеры - например, сравнительно экзотичная (ну или как минимум - не часто используемая) возможность вроде окантовки текстур (texture borders), вероятно, выполняется не на сэмплере текстур, а эмулируется дополнительным кодом в шейдере (или не поддерживается вообще). Это значит, что иногда существует несколько версий одного и того же шейдера, для разных комбинаций состояния системы.
Между прочим, это причина задержки при первом использовании шейдера или ресурса - много работы по созданию/компиляции было отложено драйвером до момента, пока не возникнет действительная необходимость (вы не поверите, сколько не используемой дряни программы иногда создают!). Программисты графики знают и оборотную сторону такого подхода - если ты хочешь быть уверен в том, что что-то было действительно создано (в противоположность просто выделению памяти по это нечто), выполни холостой вызов отрисовки, использующий эти ресурсы - "для прогрева". Отвратительно и надоедливо, но это так с момента, когда я впервые начал программировать 3D-железо в 1999 - значит, это уже вполне устоявшийся жизненный факт - привыкайте:)
В общем, идем дальше. UMD приходится поддерживать также все веселье вроде "унаследованных" D3D9 версий шейдеров и фиксированного конвейера - да, D3D добросовестно проходит через все это. Шейдеры версии 3.0 не так уж плохи (это вполне логичный факт), но 2.0 ужасен, а также различные версии 1.х просто чудовищны - помните пиксельные шейдеры версии 1.3? Или, если на то пошло, фиксированный конвейер обработки вершин с вершинным освещением и всего подобного? Да, поддержка всего этого еще там, в Direct3D и в кишках каждого современного графического драйвера, хотя, конечно, они просто транслируют вызовы старых стандартов в новые шейдеры (и делают так уже достаточно долго).
Далее, есть такие вещи, как управление памятью. UMD получит команды вроде создания текстур и ему нужно предоставить место под них. На самом деле, UMD просто разбивает на меньшие куски большие объемы памяти, которые он получает от KMD (Kernel Mode Driver), свертывание же и развертывание страниц памяти (и управление видимыми для UMD страницами видеопамяти, и к каким участкам системной памяти GPU может иметь доступ) вообще является привилегией режима ядра и, очевидно, UMD делать этого не может.
Однако UMD может делать такие штуки, как Texture Swizzling (если только GPU не делает этого аппаратно, обычно с использованием блоков 2D-блиттинга а не живого 3D-конвейера), а также может управлять передачей данных между системной памятью и видеопамятью, ну и тому подобное. Самое важное, что что он также может писать в буферы команд (или DMA - буферы прямого доступа к памяти, я буду пользоваться обоими терминами), если KMD их выделил и обработал. Буфер команд содержит, очевидно, команды:) Все изменения состояний, внесенные вами, и все вызовы отрисовки будут сконвертированы UMD в команды, которые понимает железо. Также как и множество других вещей, которые вы не делаете явно - например, загрузка текстур и шейдров в видеопамять.
Вообще, драйвера будут стараться переложить как можно больше работы именно на UMD. Все что делается в UMD, происходит в пользовательском режиме, и поэтому не требует дорогостоящих операций передачи данных из режима ядра, код может без проблем выделять память, разделять задачи на отдельные потоки, и все в таком духе - это всего лишь обычная DLL (даже не смотря на то, что она загружена API, а не вашим приложением непосредственно). Также это дает преимущества при разработке драйверов - если UMD падает, с ним падает ваша программа, но не вся система целиком. Все это может быть просто перезапущено при работающей, как и раньше, системе. UMD может быть отлажен с помощью нормального дебагеера (это же всего лишь DLL!). Так что это не только эффективно, но еще и удобно.
Однако существует один очень большой рояль в кустах, о котором я еще не упомянул.
Я сказал "драйвер для пользовательского режима"? Я имел ввиду "драйверы пользовательского режима".
Как я и сказал, UMD - это обычная DLL. Ну, ладно, на самом деле - та самая DLL, которую D3D благословил быть прямым тоннелем в драйвер режима ядра (KMD), но это все еще обычная DLL, и она развернута и выполняется в адресном пространстве родительского процесса.
Но в наши дни мы используем многозадачные операционная системы. На самом деле, мы уже используем их какое-то время.
Вот помните эту штуковину, "GPU", о которой я все время говорю? Она - разделяемый ресурс. Одновременно только один процесс может управлять вашим основным монитором (даже если у вас SLI/Crossfire технология). Ну и мы имеем кучу программ, которые пытаются одновременно использовать этот ресурс (и думать, что они в системе одни такие, красавцы). Проблема доступа не решается автоматически. Раньше, в Старые Добрые Времена, решение проблемы было просто - достаточно дать доступ к 3D всего одному приложению, и пока оно работает, все остальные доступа не получат. Но это не работает, если у вас есть система окон, и вы хотите дать ей возможность использовать GPU для рендеринга. И поэтому вам необходим некий компонент, планирует доступ к GPU и выделяет кванты времени и все такое.
Входим в планировщик.
Имейте ввиду, что в данном случае речь идет вовсе не о компоненте ядра ОС, а о планировщике графической подсистемы. Он делает именно то, о чем вы думаете - управляет доступом к 3D-конвейеру путем квантования времени между различными программами, которые хотят им попользоваться. Переключение контекстов включает в себя, по крайней мере, смену некоторых состояний на GPU (что генерирует некоторые дополнительные команды для буфера команд) и возможно также обмен некоторыми ресурсами между системной и видеопамятью. И, конечно, только один процесс получает возможность отправки команд в командный буфер 3D-конвейера в любой момент времени.
Вам часто будут встречаться программисты, пишущие под приставки, которые жалуются на довольно высокий уровень абстракции, невозможность глубокого контроля поведения 3D API на PC и связанных с этим издержек производительности. Но дело в том, что 3D API/драйвера на PC должны решать гораздо более сложную проблему, чем просто давать возможность поиграть в игры - они должны полностью отслеживать состояние системы в текущий момент времени, например, потому, что любое приложение может полностью поменять текущее состояние всех ресурсов. Они предоставляют костыли для программ, которые работают не правильно а также пытаются улучшить производительность без вмешательства разработчиков со стороны приложения, это довольно раздражающая практика, которая никому из разработчиков API не нравится, но факт остается фактом - здесь побеждает рыночный интерес - пользователи ожидают, что программы, которые работали раньше, продолжат работать и в будущем (и будут делать это плавно). Вы вряд ли заработаете себе друзей, если будете просто кричать на программы "НО ЭТО ЖЕ НЕ ПРАВИЛЬНО!", а потом обижаться и использовать самый медленный код.
Вернемся к конвейеру. Следующая остановка: режим ядра!
Драйвер режима ядра (Kernel mode driver, KMD)
Это та часть, которая работает непосредственно с оборудованием. Экземпляров UMD в системе может одновременно быть несколько, а вот KMD всегда один, и если он упадет, то пиф-паф, умирает зайчик мой - раньше вы наблюдали синий экран смерти, но теперь Windows знает как убить свалившийся драйвер и воскресить его обратно (прогресс!); по крайней мере если это простое падение, а не повреждение памяти ядра, в противном случае все пропало.
KMD разбирается со всеми штуками, которые есть в одном экземпляре. Там одна память GPU, даже не смотря на то, что есть куча программ, ведущих за нее борьбу. Кто-то должен непосредственно выделить (и разметить) физическую память. Аналогично, кто-то должен инициализировать GPU при запуске, выставить видеорежимы от устройств отображения (и получить о них дополнительную информацию), управлять аппаратной поддержкой мышиного курсора (да, есть аппаратная поддержка для курсора, и да, он у вас всего один:), запрограммировать сторожевой таймер, чтобы перезапустить GPU, если он не отвечает какое-то время, реагировать на прерывания и все прочее. Вот чем и занимается KMD.
Там же находится вся эта DRM защита контента, которая обеспечивает защищенный канал между видеоплеером и GPU, так что ни один бесценный раскодированный пиксель видеоизображения не виден для какого-нибудь грязного кода из пользовательского режима, который может сделать ужасные запрещенные вещи вроде дампинга всего этого на диск (ну или чего там). KMD какой-то частью вовлечен и в это дело тоже.
Важнее всего для нас то, что KMD управляет реальным буфером команд. Ну, вы понимаете, тем самым, который непосредственно скармливается оборудованию. Командные буферы внутри UMD не представляют реальной картины - по факту они являются лишь произвольными нарезками адресуемой GPU памяти. Что на самом деле с ними происходит, когда UMD завершает их подготовку, отправляет в планировщик, который дожидается, когда данному процессу будет предоставлено время, и потом переправляет команды на KMD? Драйвер режима ядра записывает вызов к буферу команд в основной командный буфер, и в зависимости от того, может ли командный процессор GPU читать из основной памяти или нет, ему может потребоваться транслировать его в видеопамять через DMA . Основной командный буфер - это обычно довольно маленький кольцевой буфер, единственные вещи, которые когда либо туда пишутся - это системные команды/команды инициализации и вызовы к "настоящим", "мясным" командным буферам.
Но пока это лишь буфер в памяти. Его размещение известно видеоадаптеру - обычно существует указатель только для чтения, который показывает, на какой команде основного буфера находится GPU прямо сейчас и указатель для записи, указывающий на сколько далеко продвинулся в процессе записи своих команд KMD. Это аппаратные регистры и они спроецированы в память - KMD обновляет их периодически (обычно, всякий раз, когда он предоставляет новый кусок работы).
Шина
Но, разумеется, запись не ведется сразу же прямо в видеокарту, поскольку данным нужно сперва пройти по шине - сейчас это обычно PCI-Express. Обращения по DMA проходят тоже по шине. Все это не занимает много времени, но это еще один участок пути в нашем путешествии. Пока мы наконец не попадем в...
Командный процессор!
Это интерфейс GPU - та часть, которая принимает команды, записанные KMD. С этого места я продолжу в следующей статье - этот пост и так уже достаточно велик:)
Небольшое отступление: OpenGL
OpenGL очень похож на то, что я описал выше, за исключением того, что граница между API и UMD там не выражена столь ярко. В отличие от D3D, компиляция шейдера (GLSL) не обрабатывается на стороне API вообще, всем этим занимается драйвер. Неприятным побочным эффектом этого является то, что существует столько же интерфейсов GLSL, сколько существует производителей оборудования, все они делают примерно одно и то же, но со своими багами и особенностями. Не прикольно. Еще это значит, что драйвера вынуждены выполнять все оптимизации самостоятельно, когда они наконец добираются до шейдеров - в том числе и дорогие оптимизации тоже. Формат байт-кода D3D - это действительно чистое решение проблемы - есть всего один компилятор (так что нет никаких немножко несовместимых диалектов от разных производителей), и это позволяет вам выполнить больше анализа потока данных, чем вы могли бы себе позволить в противном случае.
Опущения и упрощения
Это был всего лишь краткий обзор, существую горы нюансов, о которых я умолчал. Например, существует не один планировщик, есть несколько реализаций (между которыми драйвер может выбирать). Есть целая история о том, как именно выполняется синхронизация между центральным процессором и GPU, и о ней я вообще до сих пор не рассказал, и т.д. И, возможно, я что-то упустил. Если так - скажите мне об этом. Но сейчас - до свидания и мы надеемся увидеться с вами в следующей статье.


Часовой пояс GMT +4, время: 18:55.

Powered by vBulletin® Version 3.8.0
Copyright ©2000 - 2018, Jelsoft Enterprises Ltd.