Введение от меня, как от переводчика

Статья взята отсюда и переведена на русский язык. В статье автор подробно разбирает принцип работы графического пайплайна в GPU , используя простые и понятные иллюстрации.

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

Эта статья является частью серий статей под общим названием "Ад Рендера 2.0". Со списком статей можно ознакомиться здесь.

Перевод делался с рамках самостоятельного изучения GameDev. В рамках разработки собственной игры возникла необходимость разобраться как работает рендеринг, в итоге получилась статья с переводом и много ссылок на уточняющие неочевидные трактовки и описание терминов.

Эта статья и ей подобные исследования сначала публикуются на моем сайте bekhan.org. Узнать первыми о публикациях можно в моих сообществах.

Pipeline in Detail (Пайплайн в Деталях)

Most of the constructive feedback I received about this article was “Nice explanation, but your pipeline is 6 years old!”. I wasn’t sure what that exactly meant until Christoph Kubisch joined my fight in the Render Hell. He is a Developer Technology Engineer working for NVIDIA and whatever question I had, he answered it. And believe me, I had a lot! :)

Большинство конструктивных отзывов, которые я получил по этой статье, были в духе: "Хорошее объяснение, но ваш пайплайн устарел на 6 лет!". Я не был уверен, что это точно значит, пока Кристоф Кубиш не присоединился к моей книге. Он инженер по разработке технологий, работающим на NVIDIA, и на любой вопрос, который у меня был, он отвечал. И поверьте мне, у меня было много вопросов! :)

Our explanation is mostly based on NVIDIA architecture due the fact that Christoph is working for NVIDIA. I hope you don’t misunderstand this as an advertisement. We both just share the passion to explain stuff and I was really happy to have someone who could help me with that technical knowledge.

Be aware that we omitted and simplified some details.

Наше объяснение в основном базируется на архитектуре NVIDIA из-за того, что Кристоф работает на NVIDIA. Я надеюсь, вы не воспринимаете это как рекламу. Мы оба просто разделяем страсть к объяснению вещей, и я был действительно счастлив, что нашел кого-то, кто мог помочь мне с этими техническими знаниями.

Имейте в виду, что мы опустили и упростили некоторые детали

Two major points of the pipeline will be explained below, which weren’t totally wrong explained here in book I, but might not be clear enough:

Два главных момента пайплайна будут объяснены ниже, которые не были полностью неправильно объяснены здесь в книге I, но могли быть недостаточно ясными:

  1. Not everything is done by the “tiny” GPU cores!

  2. There can be several parallel running pipelines!

  1. Не всё делается "маленькими" ядрами GPU!

  2. Может быть несколько параллельно работающих пайплайнов!

Below I’ll go into detail about it. Have fun!

Ниже я подробно расскажу об этом. Приятного чтения!

1. Not everything is done by the “tiny” GPU cores! (Не всё делается "маленькими" ядрами GPU!)

In the pipeline example above it might seem like every pipeline stage is done by the GPU Cores – This is NOT the case! In fact, most of the stuff is not done by them. In the new section in “1. Copy the data into system memory for fast access” you already saw that several components are necessary to just bring the data to a core. So what work do the cores actually do?

В приведенном выше примере пайплайна может показаться, что каждый этап пайплайна выполняется ядрами GPU - это НЕ так! На самом деле большинство задач не выполняется ими. В новом разделе “1. Копирование данных в системную память для быстрого доступа” вы уже видели, что для передачи данных ядру необходимо несколько компонентов. Так что же на самом деле делают ядра?

Let’s observe one:

Давайте рассмотрим одно из ядер:

A core can receive commands and data. Then it executes the command by calculating the data in a floating point unit (FP UNIT) or an integer unit (INT UNIT). So you could say: A core can calculate pixels and vertices (maybe also other calculations like physics but let’s focus on graphic rendering).

Ядро может принимать команды (инструкции) и данные. Затем оно выполняет команду, обрабатывая данные в блоке с плавающей точкой (FP-блок) или в целочисленном блоке (INT-блок). Так что можно сказать: ядро может вычислять пиксели и вершины (возможно, также и другие вычисления, например, физику, но давайте сосредоточимся на графическом рендеринге).

Other important stuff like distribute the render-tasks, calculate tessellation, culling and preparing the fragments for the pixel shader, depth testing and writing pixels into the frame buffer are NOT done by the cores. This work is done by special, not programmable hardware blocks which are also placed in the GPU.

Другие важные задачи, такие как распределение задач рендеринга, вычисление тесселяции, отсечение и подготовка фрагментов для пиксельного шейдера, тестирование глубины и запись пикселей в буфер кадра, НЕ выполняются ядрами. Эта работа выполняется специальными не программируемыми аппаратными блоками, которые также находятся в GPU.

Ok, after we know that now, let’s move on to the next major point I have to clear out:

Хорошо, теперь, когда мы знаем это, давайте перейдем к следующему важному моменту, который мне нужно прояснить:

2. There can be several parallel running pipelines! (Может быть несколько параллельно работающих пайплайнов!)

First, I’ll give you a short example what I mean with that headline. If you’re still thirsty after this, I’ll take you into an even more detailed explanation.

Сначала я приведу вам короткий пример того, что я имею в виду под этим заголовком. Если после этого у вас по-прежнему останется интерес, я предоставлю более детализированное объяснение.

But first, let’s recap:

Но сначала давайте вспомним:

If we would only have one GPU Core, what could we calculate with it?

Если бы у нас было только одно ядро GPU, что бы мы могли вычислить с его помощью?

Correct: Nothing! Because the core needs someone who assigns him some work. This is done by a Streaming Multiprocessor (SM) which can handle a stream of vertices/pixels which belong to one shader. OK, with that SM and one core we could calculate one vertex/pixel at a time:

Верно: Ничего! Потому что ядру нужен кто-то, кто поручит ему какую-то работу. Это делается с помощью потоковых мультипроцессоров (SM), который может обрабатывать поток вершин/пикселей, относящихся к одному шейдеру. Хорошо, с этим SM и одним ядром мы могли бы вычислять одну вершину/пиксель за раз:

Of course, if we increase the amount of cores, there can be more vertices/pixels calculated at the same time. But only if they belong to the same shader!

Конечно, если мы увеличим количество ядер, можно будет вычислять больше вершин/пикселей одновременно. Но только если они относятся к одному и тому же шейдеру!

This was already explained in my first attempt of explaining the pipeline! But now it gets interesting: What if we would add another Streaming Multiprocessor which cares about half of the cores?

Это уже было объяснено в моей первой попытке объяснить пайплайн! Но теперь становится интересно: что, если мы добавим еще один потоковый многопроцессор, который будет заниматься половиной ядер?

Now we can not only calculate vertices/pixels in parallel, we can also take care about 2 shader streams at the same time! This means, we can for example run two different pixel shaders at the same time or run a vertex shader AND a pixel shader at the same time!

Теперь мы можем не только параллельно вычислять вершины/пиксели, но и одновременно обрабатывать 2 потока шейдеров! Это означает, что мы можем, например, одновременно запустить два разных пиксельных шейдера или запустить вершинный шейдер и пиксельный шейдер одновременно!

This rough example shall just give you a glimpse about that several different hardware blocks are involved and they all work in parallel so the pipeline is more flexible than I described in my first attempt.

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

Anyone still thirsty? Then let’s get into more detail!

Есть еще желающие узнать подробности? Тогда давайте углубимся в детали!

3. In-Depth look into the pipeline stages (Подробный взгляд на этапы пайплайна)

3.1 Overview (Обзор)

First of all: Why do we need a flexible/parallel pipeline? The reason is, that you can’t foresee, what workload you’ll have. Especially with tessellation it might be, that there are suddenly 100.000 more polygons on the screen than one frame before. Therefore you need a flexible pipeline which handles totally different workloads.

Прежде всего: почему нам нужен гибкий/параллельный пайплайн? Причина в том, что нельзя предсказать, какая у вас будет рабочая нагрузка. Особенно с тесселяцией может случиться так, что вдруг на экране появится на 100 000 полигонов больше, чем за один кадр до этого. Поэтому вам нужен гибкий пайплайн, который справляется с совершенно разными рабочими нагрузками.

DON’T WORRY! (НЕ БОЙТЕСЬ!)

Please don’t be afraid by looking at the two following images (like I am when i read a Wikipedia article and see formulas between the text)! Yes, this stuff isn’t easy and even complex charts only show what a programmer needs to know and hide a lot of the “real” complexity. I only show it to give you a rough understand how complicated this stuff is. :)

Пожалуйста, не бойтесь, глядя на два следующих изображения (как я, когда читаю статью в Википедии и вижу формулы между текстом)! Да, это не просто, и даже сложные диаграммы показывают только то, что должен знать программист, и скрывают много "реальной" сложности. Я показываю это только чтобы дать вам грубое представление о том, насколько сложной является эта область. :)

The following image shows a GPU. I have no idea about what is what, but it’s kind of beautiful, isn’t it?

Следующее изображение показывает GPU. Я понятия не имею, что есть что, но это выглядит довольно красиво, не правда ли?

And here’s an image from Christoph Kubisch’ Article “Life of a triangle”. It shows parts of the work which is happening on a GPU in structured graphics.

И вот изображение из статьи Кристофа Кубиша "Жизнь треугольника". Оно показывает части работы, которая происходит на GPU в структурированных графиках.

I hope you’ve a rough idea about the complexity now and will realize how much the explanations below are simplified. Let’s now have a detailed look on the whole pipeline.

Я надеюсь, у вас теперь есть общее представление о сложности и вы поймете, насколько объяснения ниже упрощены. Теперь давайте подробно рассмотрим весь пайплайн.

3.2 Application Stage (Этап Приложения)

It starts with the application or a game telling the driver that it wants something rendered.

Все начинается с приложения или игры, сообщающих драйверу, что оно хочет что-то отрендерить.

3.3 Driver Stage (Этап Драйвера)

The driver receives the commands from the application and puts them into a command buffer (this was already explained here in book I). After a while (or when the programmer forces it), the commands are send to the GPU.

Драйвер получает команды от приложения и помещает их в буфер команд (это уже было объяснено здесь в книге I). Спустя некоторое время (или когда программист заставляет это делать), команды отправляются на GPU.

IMPORTANT: The Driver can be a bottle-neck. See Book III “1. Many Draw Calls” for details.

ВАЖНО: Драйвер может стать узким местом. Смотрите Книгу III "1. Много Вызовов Отрисовки" для получения подробной информации.

3.4 Read commands (Чтение Команд)

Now we’re on the graphic card! The Host Interface reads commands to make them available for further use.

Теперь мы на графической карте! Интерфейс хоста читает команды, чтобы сделать их доступными для дальнейшего использования.

3.5 Data Fetch (Передача Данных)

Some of the commands sent to the GPU can contain data or are instructions to copy data. The GPU typically has a dedicated engine to deal with copying data from RAM to VRAM and vice versa. This data could be anything filling vertex buffers, textures or other shader parameter buffers. Our frame would typically start with some camera matrices being sent.

Некоторые команды, отправленные на GPU, могут содержать данные или являются инструкциями по копированию данных. Обычно на GPU есть специализированный движок для копирования данных из RAM в VRAM и обратно. Эти данные могут быть чем угодно, наполняя буферы вершин, текстуры или другие буферы параметров шейдеров. Обычно наш кадр начинается с отправки некоторых матриц камеры.

Important

  1. I symbolize the data as geometries but in fact we’re talking just about vertex-lists (vertex buffer). For a long time the rendering process doesn’t care about the final model. Instead, most of the time, only single vertices/pixels are worked with.

  2. Textures only get copied, if they’re not already in the VRAM on the graphic card!

  3. If vertex buffer are used a lot, they can stay in the VRAM like textures and don’t have to be copied with every draw call

  4. Vertex buffers may stay in the RAM (not VRAM) if they change a lot. Then the GPU can read the data directly from RAM to its Caches.

Важно

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

  2. Текстуры копируются только в том случае, если они еще не находятся в VRAM на графической карте!

  3. Если буферы вершин часто используются, они могут оставаться в VRAM подобно текстурам и не должны копироваться с каждым вызовом рендеринга.

  4. Буферы вершин могут оставаться в RAM (не в VRAM), если они часто меняются. Тогда GPU может считывать данные напрямую из RAM в свои кэши.

Now that all ingredients are ready the Gigathread Engine comes into play, it creates a thread for every vertex/pixel and bundles them into a package. NVIDIA calls such a package: Thread Block. Additional threads may also be created for vertices after tessellation or for geometry shaders (will be explained later). Thread blocks are then distributed to the Streaming Multiprocessors.

Теперь, когда все ингредиенты готовы, в игру вступает Gigathread Engine. Он создает поток для каждой вершины/пикселя и объединяет их в пакет. NVIDIA называет такой пакет: Блок потоков. Дополнительные потоки могут быть созданы также для вершин после тесселяции или для геометрических шейдеров (будет объяснено позже). Блоки потоков затем распределяются по потоковым многопроцессорам.

3.6 Vertex Fetch (Получение Вершин)

A Streaming Multiprocessor is a collection of different hardware units and one of them is the Polymorph Engine. For the sake of simplicity I present them like separate guys. :) The Polymorph Engine gets the needed data and copies it into caches so cores can work faster. Why it is useful to copy data into caches was explained here in book I.

Потоковый многопроцессор представляет собой совокупность различных аппаратных блоков, и одним из них является Polymorph Engine . Для простоты я представляю их как отдельных персонажей. :) Polymorph Engine получает необходимые данные и копирует их в кэши, чтобы ядра могли работать быстрее. Зачем полезно копировать данные в кэши, объясняется здесь в книге I.

3.7 Shader Execution (Выполнение Шейдеров)

The main purpose of the Streaming Multiprocessor is executing program code written by the application developer, also called shaders. There is multiple types of shaders but each kind can run on any of these Streaming Multiprocesors and they all follow the same execution logic.

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

The Streaming Multiprocessor now takes his big Thread Block which he received from the Gigathread Engine and separates it into smaller chunks of work. He splits the Thread Block into heaps of 32 Threads. Such a smaller heap is called: Warp. In total, a Maxwell Streaming Multiprocessor (SMM) can “hold” 64 of such warps. In my example and SMM there are 32 dedicated cores to work on the 32 Threads.

Теперь потоковый многопроцессор берет свой большой блок потоков, который он получил от Gigathread Engine, и разделяет его на меньшие части работы. Он разделяет блок потоков на кучи по 32 потока. Такая меньшая куча называется: Варп. В общей сложности потоковый многопроцессор (в архитектуре) Maxwell (SMM) может "содержать" 64 таких варпов. В моем примере и в SMM есть 32 выделенных ядра для работы с 32 потоками.

One Warp is then taken to be worked on. At this point the hardware should have all necessary data loaded into the registers so that the cores can work with it. We simplify the illustration here a bit: SMM for example has 4 warp schedulers, which each would let one warp work and manage a subset of the SMM’s warps. (исправленная неточность)

Один варп затем берется для работы. На этом этапе аппаратное обеспечение должно загрузить все необходимые данные в регистры, чтобы ядра могли с ними работать. Мы немного упрощаем иллюстрацию здесь: например, SMM имеет 4 планировщика варпов, каждый из которых позволяет одному варпу работать и управлять подмножеством варпов (что "содержатся" в) SMM. (исправленная неточность)

The actual work begins now. The cores itself never see the whole shader-code but only one command of it at the time. They do their work and then the next command is given to them by the Streaming Multiprocessor. All cores execute the same command but on different data (vertices/pixels). It’s not possible that some cores work on command A and some on command B at the same time. This method is called lock-step.

Теперь начинается настоящая работа. Сами ядра никогда не видят весь код шейдера, а только одну команду за раз. Они выполняют свою работу, а затем следующая команда передается им потоковым многопроцессором. Все ядра выполняют одну и ту же команду, но с разными данными (вершины/пиксели). Невозможно, чтобы некоторые ядра работали над командой A, а некоторые над командой B одновременно. Этот метод называется синхронный шаг.

This lock-step method gets important if you have an IF-statement in your shader code which executes either one code block or another.

Метод синхронный шаг становится важным, если у вас в коде шейдера есть инструкция IF, которая выполняет либо один блок кода, либо другой.

This IF-Statement makes some of our cores execute the left code and some the right code. It can’t be done at the same time (like explained above). First one code-side would be executed and some cores would be “sleeping” and then the other side is executed afterwards. Kayvon explains this here at 43:58 “What about conditional execution?”.

Эта инструкция IF заставляет некоторые из наших ядер выполнять левый код, а некоторые - правый. Это невозможно сделать одновременно (как объяснено выше). Сначала будет выполнена одна сторона кода, и некоторые ядра будут "спать", а затем другая сторона будет выполнена позже. Кейвон объясняет это здесь на 43:58 "Что насчет условного выполнения?".

In my example 16 pixels/vertices would be worked on but it’s of course possible to have IF-statements where only ONE pixel/vertex is calculated and the other 31 cores get masked out. The same applies to loops, if only one core has to stay in the loop long, all the others become idle. This phenomenon is also called divergent threads and should be minimized. Ideally all threads in the warp hit the same side of the IF-condition, because then we can entirely jump over the non-active side.

В моем примере было бы обработано 16 пикселей/вершин, но, конечно, возможны инструкции IF, где рассчитывается только ОДИН пиксель/вершина, и остальные 31 ядро маскируются. То же самое относится к циклам: если только одно ядро должно оставаться в цикле долго, все остальные становятся неактивными. Это явление также называется расходящимися потоками и его следует минимизировать. В идеале все потоки в варпе должны попадать на одну и ту же сторону условия IF, потому что тогда мы можем полностью пропустить неактивную сторону.

But why can the SMM hold 64 warps, when the cores can only work on very few at a time?? This is because sometimes you can’t progress because you have to wait for some data. For example we need to calculate the lighting with the normal from our normal map texture . Even if the normal was in the cache it takes a while to access it, and if it wasn’t it can take a good while. Pro’s call this Memory Stall and this is greatly explained by Kayvon. Instead of doing nothing, the current warp will just be exchanged with one, where the necessary data is ready for use:

Но почему SMM может содержать 64 варпа, когда ядра могут работать только с очень небольшим их количеством за раз?? Это потому, что иногда вы не можете продолжить, потому что должны ждать какие-то данные. Например, нам нужно рассчитать освещение с нормалью из нашей текстуры карты нормали. Даже если нормаль была в кэше, доступ к ней занимает некоторое время, и если ее не было, это может занять довольно много времени. Профессионалы называют это задержкой памяти, и это хорошо объяснено Кейвоном. Вместо того, чтобы ничего не делать, текущий варп просто заменяется на тот, где необходимые данные уже готовы к использованию:

The explanation above was a bit simplified. Modern GPU architecture doesn’t only allow a Streaming Multiprocessor working at one Warp at the time. Look for example at this image showing a Streaming Multiprocessor based on Maxwell architecture (SMM): One Streaming Multiprocessor has access to four Warp schedulers, each controlling 32 Cores. This makes him able to work on four Warps completely parallel. The book keeping for the work state of multiple threads is kept independently and is reflected by how many Threads the SM can hold in parallel, as noted on top.

Вышеописанное объяснение было немного упрощено. Современная архитектура GPU не только позволяет потоковому многопроцессору работать с одним варпом за раз. Посмотрите, например, на эту картинку, показывающую потоковый многопроцессор на основе архитектуры Maxwell (SMM): Один потоковый многопроцессор имеет доступ к четырем планировщикам варпов, каждый из которых управляет 32 ядрами. Это позволяет ему работать с четырьмя варпами полностью параллельно. Учет рабочего состояния нескольких потоков ведется независимо и отражается тем, сколько потоков SM может содержать параллельно, как отмечено выше.

There is more than just waiting for memory that allows us to switch active scheduling as Guohui Wang noted:

Есть ещё нечто большее, чем просто ожидание памяти, что позволяет нам переключать активное планирование, как отметил Guohui Wang:

Since more than 4 Warps can run in parallel in Maxwell. Each warp scheduler can perform dual-issuing instruction in one clock clock to a warp, in this sense, every clock cycle we have up to 4 warps get their new instructions to work on (suppose at least 4 warps are ready by far). However, we also have instruction-level parallelism. That means when 4 warps are executing instructions (typically with 10-20 cycle latency), the next batch of 4 warps can accept new instructions in the next clock cycle. Therefore, it is possible to have more than 4 warps running in parallel if the resources are available. Actually, in one of the CUDA optimization video in GTC2013, more than 30 active warps are recommended to keep the pipeline fully occupied in a general case. You may want to revise the wording here to indicate that there will be multiple (more than 4) warps running in parallel, to avoid any potential confusion that people may think 4 scheduler could only hold 4 parallel warps.
– Guohui Wang

Поскольку в архитектуре Maxwell могут параллельно работать более четырех варпов, каждый планировщик варпов может выполнить двойную выдачу инструкций за один тактовый цикл для варпа. Исходя из этого, на каждом тактовом цикле до четырех варпов могут получать новые инструкции для работы (при условии, что как минимум четыре варпа готовы). Однако здесь также присутствует параллелизм на уровне инструкций. Это означает, что в то время как четыре варпа выполняют инструкции (обычно с задержкой в 10-20 циклов), следующая группа из четырех варпов может принимать новые инструкции в следующем тактовом цикле. Таким образом, при наличии доступных ресурсов, возможно, что будет работать более четырех варпов параллельно. На самом деле, в одном из видео по оптимизации CUDA на конференции GTC 2013 рекомендуется, чтобы для полной загрузки конвейера в общем случае было активно более 30 варпов. Возможно, стоит пересмотреть формулировку, чтобы указать на наличие множественных (более четырех) параллельных варпов и избежать возможного недопонимания, что четыре планировщика могут обслуживать только четыре параллельно работающих варпа.
– Guohui Wang

But what are our threads actually working on? For example a Vertex Shader!

Но над чем на самом деле работают наши потоки? Например, над вершинным шейдером!

3.8 Vertex Shader (Вершинный Шейдер)

The vertex shader takes care of one vertex and modifies it how the programmer wants. Unlike normal software (e.g. Mail-Program) where you run one instance of the program and hand over a lot data which get taken care of (e.g. handling all the Mails), you run one instance of a vertex shader program for every vertex which then runs in in one thread managed by the Streaming Multiprocessor.

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

Our vertex shader transforms vertices or its parameters (pos, color, uv) like you want:

Наш вершинный шейдер преобразует вершины или их параметры (позиция, цвет, uv) так, как вы хотите:

Some stages are only executed when Tessellation is used. Click the link below, if you want to see what happens when Tessellation gets into the game.

Некоторые этапы выполняются только при использовании тесселяции. Нажмите на ссылку ниже, если хотите увидеть, что происходит, когда тесселяция включена в игру.

Click here for Tessellation (Нажмите здесь для тесселяции)

3.13 Primitive Assembly (Сборка Примитивов)

Towards the end of the geometry pipeline we gather the vertices that assemble our primitive: a triangle, line or point. The vertices either came from the vertex shader, or if tessellation was active from the domain shader.

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

What mode (triangle, line or point) we are in was defined in the application for this drawcall. Normally we would just pass the primitive for final processing and rasterzation, but there is an optional stage that makes use of this information, the Geometry Shader.

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

3.14 Geometry Shader (Геометрический Шейдер)

The Geometry Shader works on the final primitives. Similar to the Hull Shader it gets the primitive’s vertices as input. It can modify those vertices and even generate a few new ones. It can also change the actual primitive mode. For example turn a point into two triangles, or the three visible sides of a cube.

Геометрический шейдер работает с окончательными примитивами. Подобно шейдеру оболочки, он получает вершины примитива на вход. Он может изменять эти вершины и даже создавать несколько новых. Также он может изменить текущий режим примитива. Например, превратить точку в два треугольника или три видимые стороны куба.

However geometry shader is not really good for creating lots of new vertices or triangles, tessellation is best left to the tessellation shaders. The purpose of the geometry shader is rather special given it’s the last stage prior preparing the primitive for rasterization. For example geometry shader plays a key-role in current voxelization techniques.

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

Here you can find good examples how to program & use geometry shaders. And this is a great overview about the openGL pipeline.

Здесь вы можете найти хорошие примеры программирования и использования геометрических шейдеров. А это отличный обзор конвейера OpenGL.

3.15 Viewport Transform & Clipping (Преобразование Вида и Отсечение)

Until here the programmers seem to use a quadratic space for all the operations (i guess it’s easier/faster that way) but now this needs to be fit to the actual resolution of your monitor (or window where the game is rendered into). More info about this you can find here in the “Viewport Transform / Screen Mapping” Section.

До этого момента программисты, кажется, использовали квадратное пространство для всех операций (я предполагаю, что так проще/быстрее), но теперь это нужно приспособить к фактическому разрешению вашего монитора (или окна, в котором отображается игра). Больше информации об этом можно найти здесь в разделе "Viewport Transform / Screen Mapping".

Also triangles get cut if they overlap a certain security-border (Guard Band) of the scene (this is called: Guard Band Clipping and you find more infos here and here). The clipping is done because the Rastizer can only deal with triangles within the area it’s working on:

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

3.16 Triangles Journey (Путешествие Треугольников)

This is not really a separate step in the pipeline but I found it very interesting so that it gets its own section.

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

At this point we know the exact position, shading, etc. of several vertices which form triangles. These triangles need to be “painted” and before we can do that, someone has to find out which pixels of the screen are covered by the triangles. This is done by so called rasterizers. It’s important to know, that there are several available AND several of them can work at the same triangle if its big enough. Else it would mean that only one rasterizer is working when you have big triangles on screen and the others would have some vacation.

На данном этапе мы точно знаем позиции, затенение и другие характеристики нескольких вершин, которые образуют треугольники. Эти треугольники нужно «закрасить», и для этого кто-то должен определить, какие пиксели экрана закрыты треугольниками. Это делают так называемые растеризаторы. Важно знать, что (растеризаторов) доступно несколько и что несколько из них могут работать над одним и тем же треугольником, если он достаточно большой. В противном случае это означало бы, что при наличии крупных треугольников на экране работает только один растеризатор, а остальные могли бы отдыхать.

Therefore every rasterizer has the responsibility for certain parts of the screen. And if a triangle belongs to such a responsibility area (the bounding box of the triangle is taken to measure this), it is send to this rasterizer so that he can work on it.

Поэтому каждый растеризатор несет ответственность за определенные части экрана. И если треугольник относится к такой зоне ответственности (для измерения этого берется bounding box треугольника), он отправляется этому растеризатору, чтобы тот мог поработать над ним.

3.17 Rasterizing (Растеризация)

The rasterizer receives the triangles he’s responsible for and first does a quick check if the triangle even faces forward. If not, it’s thrown away (backface culling). If the triangle is “valid”, the rasterizer creates pre-pixels/fragments by calculating the edges which connect the vertices (edge setup) and so seeing which pixels quads (2×2 pixels) belong to the triangle.

Растеризатор получает треугольники, за которые он отвечает, и сначала быстро проверяет, направлен ли треугольник вперед. Если нет, он отбрасывается (отсечение задних граней). Если треугольник "правильный", растеризатор создает предварительные пиксели/фрагменты, вычисляя грани, соединяющие вершины (настройка граней), и таким образом видит, какие пиксельные квадраты (2×2 пикселя) принадлежат треугольнику.

If you’re really into rasterizing and micro-triangles, you definitely should check out this presentation. And this article gives a good overview about it.

Если вам действительно интересна растеризация и микротреугольники, вам определенно стоит ознакомиться с этой презентацией. А эта статья дает хороший обзор по этой теме.

That the raster engine always works with 2×2 pixels which can make the render process inefficient. See Book III for details.

Тот факт, что растерный движок всегда работает с квадратами 2×2 пикселя, может сделать процесс рендеринга неэффективным. См. Книгу III для деталей.

After the pre-pixels/fragments are created, there’s a check if they would be even visible (or hidden by already rendered stuff):

После создания предварительных пикселей/фрагментов проводится проверка, будут ли они вообще видимыми (или скрытыми уже отрендеренными объектами):

Pixels produced by the rasterizer are sent to the Z-cull unit. The Z-cull unit takes a pixel tile and compares the depth of pixels in the tile with existing pixels in the framebuffer. Pixel tiles that lie entirely behind framebuffer pixels are culled from the pipeline, eliminating the need for further pixel shading work.
NVIDIA GF100 Whitepaper

Пиксели, созданные растеризатором, отправляются в блок Z-отсечения. Блок Z-отсечения берет пиксельный квадрат и сравнивает глубину пикселей в квадрате с существующими пикселями в кадровом буфере. Пиксельные квадраты, которые полностью находятся за пикселями кадрового буфера, отсекаются от конвейера, устраняя необходимость дальнейшей работы по оттенению пикселей.
NVIDIA GF100 Whitepaper

3.18 Pixel Shader

After the pre-pixels/fragments are generated they can be “filled”. For every pre-pixel/fragment a new thread is generated and again distributed to all the available cores (like it was done with all the vertices).

После создания предварительных пикселей/фрагментов они могут быть "заполнены". Для каждого предварительного пикселя/фрагмента создается новый поток и снова распределяется по всем доступным ядрам (как это было сделано со всеми вершинами).

“Again we batch up 32 pixel threads, or better say 8 times 2×2 pixel quads, which is the smallest unit we will always work with in pixel shaders.”

"Снова мы группируем 32 потока пикселей, или лучше сказать, 8 раз по 2×2 пиксельных квадрата, которые являются наименьшей единицей, с которой мы всегда будем работать в пиксельных шейдерах."

When the cores are done with their work, they write the results into the registers from where they are taken and put into the caches for the last step: Raster Output (ROP).

Когда ядра закончат свою работу, они записывают результаты в регистры, откуда они берутся и помещаются в кэши для последнего этапа: Растровый вывод (ROP).

3.19 Raster Output (Растровый вывод)

The final step is done by the so called “Raster Output” Units which move the final pixel data (just got from the pixel shader) from L2 cache into the framebuffer which lays around in the VRAM. The GF100 as an example has 48 such ROPs and I interpret the dataflow (from L2 cache to VRAM) based on that they are placed really near to each other:

Заключительный этап выполняется так называемыми блоками "Растрового вывода", которые перемещают окончательные данные пикселей (только что полученные от пиксельного шейдера) из кэша L2 в буфер кадра, который находится в VRAM. Например, GF100 имеет 48 таких ROP, и я интерпретирую поток данных (от кэша L2 к VRAM) на основе того, что они размещены очень близко друг к другу:

“[…] L2 cache, and ROP group are closely coupled […]”
NVIDIA GF 100 Whitepaper

“[…] кэш L2 и группа ROP тесно связаны […]”
NVIDIA GF 100 Whitepaper

Besides of just moving pixel data, the ROPs also take care of pixel blending, coverage information for anti aliasing and “atomic operations”.

Помимо простого перемещения данных пикселей, ROP (в множественном числе) также заботятся о пиксельном смешивании, информации о покрытии для антиалиасинга и атомарных операций.

What a ride, it took a long time to bring all the information together so I hope you found this book useful.

Какое путешествие, потребовалось много времени, чтобы собрать всю информацию, поэтому я надеюсь, что эта книга была вам полезна.

The End.

Конец.

Комментарии (1)


  1. gdt
    06.11.2023 09:01
    +4

    Интересный подход, облегчаете задачу критикам. Однако, оригинал и перевод ничем визуально не отличаются, это усложняет чтение. Было бы круто оригинал выделить визуально как-то (например, курсивом) или убрать под спойлер. То же самое касается выделения слов, сильно пестрит - я уверен есть способ, который меньше бросается в глаза.

    И все же интересно, как правильно - мультипроцессор или многопроцессор? У вас встречаются оба термина. А перейдя по ссылке в начале статьи увидел "Драйвер уровень" вместо "Уровень драйвера". Думаю в целом с переводами есть еще над чем поработать.