Индустрия графической разработки окончательно повернулась в сторону эксплицитных («явных») API — то есть, таких, которые не содержат в своей реализации скрытых эвристик и не пытаются что-либо оптимизировать, строго следуя инструкциям программиста. Человек, плохо знакомый с принципами работы видеокарт, может удивиться: неужели классические OpenGL или Direct3D не подчиняются вашим инструкциям? Подчиняются, но фишка в том, что абстрактный графический конвейер, реализуемый OpenGL, на стороне пользователя не соответствует тому, что на самом деле происходит на низком уровне. К примеру, когда вы вызываете скромную, на первый взгляд, glEnable, видеодрайвер делает много вещей — сначала проверяет, что переданная константа действительна, и что вызов не происходит в недопустимом режиме, затем обновляет CPU-копию состояния, проверяя, не является ли вызов избыточным. Вместо того чтобы немедленно сообщать видеокарте о необходимости изменения, драйвер помечает состояние как измененное — фактическая аппаратная конфигурация конвейера меняется только при следующем вызове отрисовки (glDraw*). Некоторые изменения состояния — например, переключение шейдеров — приводят к значительным задержкам; кроме того, glEnable может заставить драйвер перекомпилировать внутренние скрытые шейдеры для обработки новой конфигурации, что крайне медленно (почитайте о таком явлении, как комбинаторный взрыв).

Эту проблему, вкупе с потоковой небезопасностью конвейера, новые API (Vulkan, Direct3D 12, Metal) решают путем использования неизменяемых PSO (pipeline state objects). Программист создает нужные ему конвейеры при инициализации приложения, заранее предоставляя всю необходимую конфигурацию — шейдеры, формат выводного буфера, настройки растеризации — которые в дальнейшем уже не меняются от кадра к кадру. Это позволяет драйверу производить валидацию состояния, компиляцию шейдеров в машинный код GPU и прочую тяжелую работу лишь один раз. PSO хранятся в видеопамяти, поэтому переключение с одного состояния на другое — дешевая операция, это всего лишь запись инструкции в командный буфер и никакого скрытого оверхеда. В оптимизированном рендере можно сократить переключения PSO до 15-20 и даже меньше, в зависимости от логики — для современных видеокарт это практически ничто. Вот это и есть главное, на мой взгляд, преимущество Vulkan и его родственников.

В чем же главный недостаток? А в том же самом — в неизменяемости конвейера! Если вы создаете классический Forward, вы уже не можете дать пользователям вашего движка возможность менять шейдер на уровне отдельного объекта внутри прохода. Новый шейдер — новый PSO. Создавать их на лету нельзя, нужно иметь всю информацию заранее и предусмотреть специальные проходы для всех кастомных шейдеров, если они есть. Как бы вы ни любили Forward за его гибкость, для Vulkan намного эффективнее строгий Deferred с его жестко заданными шейдерами и полным отстутствием вариативности состояния в маштабах прохода (либо Clustered Forward, но это уже другая история). Мир эксплицитных API — это мир строгих правил, где исключения обходятся слишком дорого. Если у вас есть талант придумывать хорошие правила и следовать им, то вы выиграете от этого, в противном случае вам будет тяжело и больно.

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *