[Из песочницы] Direct3D vs OpenGL: история противостояния
По сей день в Интернете можно встретить споры о том, какой же графический API лучше: Direct3D или OpenGL? Несмотря на свой религиозный характер, такие словесные баталии приносят полезный результат в виде неплохих исторических обзоров развития аппаратно-ускоренной графики.
Целью данного поста является перевод одного из таких экскурсов в историю, написанного Джейсоном МакКессоном (Jason L. McKesson) в ответ на вопрос «Почему разработчики игр предпочитают Windows». Этот текст вряд ли отвечает на поставленный вопрос, но историю развития и противостояния двух самых популярных графических API он описывает очень красочно и довольно подробно, поэтому в переводе я сохранил авторскую разметку. Текст написан в середине 2011 года и охватывает промежуток времени, начинающийся незадолго до появления Direct3D и до момента написания. Автор оригинального текста является опытным разработчиком игр, активным участником StackOverflow и создателем обширного учебника о современном программировании 3D-графики. Итак, предоставим слово Джейсону.
Предисловие
Прежде чем мы начнём, я хотел бы сказать, что знаю больше об OpenGL, чем о Direct3D. В своей жизни я не написал ни строчки кода на D3D, но писал руководства по OpenGL. Но то, о чём я хочу рассказать — вопрос не предубеждения, а истории.
Рождение конфликта
Однажды, где-то в начале 90-х, Microsoft огляделись вокруг. Они увидели, что SNES и Sega Genesis — это очень здорово, на них можно поиграть во множество экшн-игр и всего такого. И они увидели ДОС. Разработчики писали досовские игры как консольные: близко к железу. Тем не менее, в отличие от консолей, где разработчик знал, какое железо будет у пользователя, дос-разработчики были вынуждены писать под множество конфигураций. А это намного сложнее, чем кажется.
Но у Microsoft была бо́льшая проблема: Windows. Видите ли, Windows хотела полностью владеть железом в отличие от ДОС, которая позволяла разработчикам делать всё что угодно. Владение железом необходимо для взаимодействия между приложениями. Но такое взаимодействие — это в точности то, что ненавидят разработчики игр, потому что оно потребляет драгоценные ресурсы, которые они могли бы использовать для всяких крутых вещей.
Для продвижения разработки игр на Windows, Microsoft нуждалась в однородном API, который был бы низкоуровневым, работал бы на Windows без потерь производительности, и был бы совместим с различным железом. Единый API для графики, звука и устройств ввода.
Так родился DirectX.
3D ускорители появились несколько месяцев спустя. И Microsoft вляпались в неприятности. Дело в том, что DirectDraw, графический компонент DirectX, работал только с 2D графикой: выделял графическую память и делал быстрые битовые операции между различными секторами памяти.
Поэтому Microsoft купили немного стороннего ПО и превратили его в Direct3D версии 3. Его ругали абсолютно все. И было за что: чтение кода на D3D v3 выглядело как расшифровка письменности исчезнувшей древней цивилизации.
Старик Джон Кармак в Id Software посмотрел на это безобразие, сказал «Да пошло оно…», и решил писать с использованием другого API: OpenGL.
Однако другая сторона этой запутанной истории заключалась в том, что Microsoft совместно с SGI работали над реализацией OpenGL для Windows. Идея была в том, чтобы привлечь разработчиков типичных GL-приложений для рабочих станций: САПР, систем моделирования и тому подобных вещей. Игры были последним, о чём они думали. В основном это касалось Windows NT, но Microsoft решили добавить OpenGL и в Windows 95.
Чтобы сманить разработчиков софта для рабочих станций на Windows, Microsoft решили подкупить их доступом к новомодным 3D-ускорителям. Они реализовали протокол для устанавливаемых клиентских драйверов: графическая карта могла заменить программный OpenGL от Microsoft своей аппаратной реализацией. Код автоматически использовал аппаратный OpenGL, если таковой был доступен.
Однако, в те времена потребительские видеокарты не имели поддержки OpenGL. Это не остановило Кармака от портирования Quake на OpenGL на рабочей станции SGI. В readme-файле GLQuake можно прочитать следующее:
Теоретически, glquake запустится на любой реализации OpenGL, которая поддерживает расширение texture objects. Но пока вы не запустите её на очень мощном железе, которое ускоряет всё, что нужно, работать она будет непростительно медленно. Если игре потребуется работать через какие-либо программные эмуляции, её производительность скорее всего не превысит один кадр в секунду.
В настоящее время (март 1997), единственной полностью совместимой с opengl железкой, способной потянуть glquake на приемлемом уровне, является ОЧЕНЬ дорогая видеокарта intergraph realizm. 3dlabs существенно увеличили её производительность, но с имеющимися драйверами она всё ещё недостаточно подходит для игры. Некоторые из драйверов от 3dlabs для плат glint и permedia ещё и крэшат NT при выходе из полноэкранного режима, поэтому я не рекомендую запускать glquake на железе от 3dlabs.
3dfx предоставляет opengl32.dll, которая реализует все нужное для glquake, но это не полная реализация opengl. Другие opengl-приложения скорее всего с ней не заработают, поэтому рассматривайте её в основном как «драйвер для glquake».
Это было рождением miniGL драйверов. В конечном итоге они эволюционировали в полноценные реализации OpenGL, как только железо стало достаточно мощным для аппаратной поддержки этой функциональности. nVidia стала первой, кто предложил полную реализацию OpenGL. Другие поставщики всё ещё медлили, что стало одной из причин перехода разработчиков на Direct3D, поддерживаемый более широким спектром оборудования. В конце концов остались только nVidia и AMD (которая сейчас ATI), и у обеих имелись хорошие реализации OpenGL.
Рассвет OpenGL
Итак, участники определены: Direct3D против OpenGL. Это поистине удивительная история, учитывая то, как плох был D3D v3.
Совет по архитектуре OpenGL (Architectural Review Board, ARB) — это организация, ответственная за поддержание и развитие OpenGL. Они выпускают множество расширений, содержат репозиторий с расширениями, и создают новые версии API. ARB — это комитет, состоящий из большого количества игроков индустрии компьютерной графики и некоторых производителей ОС. Apple и Microsoft в разное время тоже были членами ARB.
3Dfx выходит на сцену со своей Voodoo2. Это первая видеокарта, позволяющая делать мультитекстурирование, которое не было ранее предусмотрено в OpenGL. В то время как 3Dfx была решительно против OpenGL, nVidia, следующий производитель чипа с мультитекстурированием (TNT1), были без ума от OpenGL. Тогда ARB выпустил расширение GL_ARB_multitexture, которое предоставляло доступ к множественным текстурам.
Тем временем появляется Direct3D v5. Теперь D3D действительно стал API, а не какой-то несуразицей. В чём же проблема? В отсутствии мультитекстурирования.
Упс.
Но это не доставляло таких неудобств, какие могло бы доставить, потому что почти никто не использовал множественное текстурирование. Мультитекстурирование почти не вредит производительности, и во многих случаях разница незаметна на фоне многопроходности. И конечно, разработчики игр очень любят чтобы их игры уверенно работали на старом железе, которое не имело поддержку множественных текстур, поэтому многие игры были выпущены без неё.
D3D вздохнул с облегчением.
Время шло, и nVidia выкатили GeForce 256 (не путать с самым первым GeForce GT-250), прекратив борьбу на рынке графических плат на следующие два года. Главным конкурентным преимуществом этой платы была возможность делать преобразования вершин и освещение (transformation & lighting, T&L) аппаратно. Но это ещё не всё: nVidia любила OpenGL настолько, что их T&L-движок фактически и был OpenGL. Почти буквально! Как я понимаю, некоторые из их регистров получали на вход напрямую численные значения переменных типа GLenum.
Выходит Direct3D v6. Наконец то подоспело множественное текстурирование…, но без аппаратного T&L. В OpenGL всегда был T&L-конвейер, хотя до GeForce 256 он был реализован программно. Поэтому для nVidia оказалось довольно просто переделать программную реализацию в аппаратное решение. В D3D аппаратное T&L появилось только к седьмой версии.
Заря эпохи шейдеров, OpenGL во мгле
Затем появилась GeForce 3. В то же самое время произошло много интересных вещей.
В Microsoft решили, что они больше не собираются опаздывать. Поэтому вместо того, чтобы смотреть, что сделает nVidia, и копировать их разработки уже пост-фактум, Microsoft приняли изумительное решение: пойти и поговорить. И они полюбили друг друга, и у них появилась совместная маленькая консоль.
Шумный развод произошёл позже, но это совсем другая история.
Для рынка PC это значило, что GeForce 3 вышла одновременно с D3D v8, и нетрудно видеть, как GeForce 3 повлияла на шейдеры D3D v8. Пиксельные шейдеры Shader Model 1.0 были очень сильно заточены под оборудование nVidia. Не было предпринято ни единой попытки сделать хоть что нибудь для абстрагирования от железа nVidia. Shader Model 1.0 стала тем, для чего предназначена GeForce 3.
Когда ATI ворвалась в гонку производительности видеокарт со своей Radeon 8500, появилась одна проблема. Пиксельный конвейер Radeon 8500 оказался более мощным, чем у nVidia. Поэтому Microsoft выпустила Shader Model 1.1, которая в основном была тем, для чего предназначена 8500.
Это звучит как поражение D3D, но успех и провал — понятия относительные. На самом деле эпический провал поджидал OpenGL.
В nVidia очень любили OpenGL, поэтому после выхода GeForce 3 они выпустили целую пачку расширений для OpenGL. Проприетарных расширений, которые работали только на nVidia. Естественно, когда появилась плата 8500, она не могла использовать ни одно из них.
Итак, на D3D 8 вы могли хотя бы запустить шейдеры SM 1.0. Конечно, чтобы использовать всю крутость 8500, приходилось писать новые шейдеры, но код хотя бы работал.
Чтобы получить любые шейдеры на Radeon 8500 в OpenGL, ATI пришлось разработать несколько расширений для OpenGL. Проприетарных расширений, которые работали только на ATI. В итоге, чтобы разработчики могли заявить, что они прикрутили к своему движку шейдеры, им приходилось писать отдельный код для nVidia и отдельный код для ATI.
Вы возможно спросите: «А где же был комитет ARB, который должен поддерживать OpenGL на плаву?». А были они там, где в конечном итоге оказываются многие комитеты: они сидели и тупили.
Обратите внимание, выше я упомянул ARB_multitexture, потому что это расширение глубоко замешано во всей ситуации. Стороннему наблюдателю казалось, что ARB вообще хочет избежать идеи шейдеров. Они решили, что если они вбухают достаточно конфигурируемости в фиксированный конвейер, то он сравняется по своим возможностям с программируемым шейдерным конвейером.
ARB выпускали расширения одно за другим. Каждое расширение со словами «texture_env» в названии было попыткой залатать этот устаревающий дизайн. Посмотрите на список расширений: таких расширений было выпущено восемь штук, и многие из них были переведены в основную функциональность OpenGL.
В то время Microsoft входила в ARB, и покинула его только к выпуску D3D 9, поэтому возможно, Microsoft саботировали OpenGL некоторым образом. Лично я сомневаюсь в этой теории по двум причинам. Во-первых, они должны были бы заручиться поддержкой других членов Комитета, потому как каждый участник имеет всего один голос. Во-вторых, что более важно, Комитету не нужна была помощью Microsoft чтобы всё запороть, свидетельства чему мы увидим далее.
В итоге ARB, скорее всего под давлением ATI и nVidia (обе являются активными участниками), наконец очнулся и ввёл в стандарт ассемблерные шейдеры.
Хотите ещё более дурацкую историю?
Аппаратное T&L. Это то, что в OpenGL было изначально. Чтобы получить максимально возможную производительность аппаратного T&L, нужно хранить данные вершин на GPU. Все-таки, GPU — это основной потребитель вершинных данных.
В D3D v7 Microsoft ввела концепцию вершинных буферов, которые выделяют куски памяти в GPU и размещают там данные вершин.
Хотите знать когда в OpenGL появилась эквивалентная функциональность? Да, nVidia, как самый большой любитель OpenGL, выпустила своё расширение для хранения массивов вершин на GPU ещё во времена выхода GeForce 256. Но когда же ARB представил такую функциональность?
Два года спустя. Это было после того, как она утвердили вершинные и фрагментные (пиксельные в терминах D3D) шейдеры. Столько времени ушло у ARB чтобы разработать кросс-платформенное решение для хранения вершинных данных в памяти GPU. И это то, что необходимо, чтобы аппаратное T&L достигло максимальной производительности.
Один язык, чтобы убить их всех
Итак, OpenGL был сломан в течение какого-то времени. Отсутствовали кросс-платформенные шейдеры и аппаратно-независимое хранение вершин в GPU, в то время как пользователи D3D наслаждались и тем и другим. Могло ли стать ещё хуже?
Можно сказать, что могло. Встречайте: 3D Labs.
Вы спросите: кто же они? Они — мёртвая компания, которую я считаю истинным убийцей OpenGL. Конечно, общая несостоятельность Комитета сделала OpenGL уязвимым, в то время как он должен был рвать D3D в клочья. Но по-моему, 3D Labs — возможно единственная причина текущего положения OpenGL на рынке. Что они для этого сделали?
Они разработали язык шейдеров для OpenGL.
3D Labs были умирающей компанией. Их дорогостоящие GPU были вытеснены с рынка рабочих станций всё возрастающим давлением nVidia. И в отличие от nVidia, 3D Labs не были представлены на потребительском рынке; победа nVidia означала бы смерть для 3D Labs.
Что в итоге и случилось.
В стремлении оказаться на плаву в мире, которому были не нужны их продукты, 3D Labs заявились на конференцию Game Developer Conference с презентацией того, что они назвали «OpenGL 2.0». Это был OpenGL API, переписанный с нуля. И это имело смысл, ибо в те времена в API OpenGL было полно хлама (который, впрочем, остаётся там и по сей день). Посмотрите хотя бы на то, насколько эзотерически сделаны загрузка и биндинг текстур.
Частью их предложения был язык шейдеров. Да, именно он. Тем не менее, в отличие от имеющихся кросс-платформенных расширений, их язык шейдеров был «высокоуровневым» (C — это высокий уровень для языка шейдеров).
В это же время в Microsoft работали над своим собственным языком шейдеров. Который они, включив всё своё коллективное воображение, назвали… Высокоуровневым Языком Шейдеров (HLSL). Но их подход к языку был фундаментально иным.
Наибольшей проблемой языка от 3D Labs было то, что он был встраиваемым. Microsoft полностью самостоятельно определяла свой язык. Они выпустили компилятор, который генерировал ассемблерный код для шейдеров SM 2.0 (или выше), который, в свою очередь, можно было скармливать D3D. Во времена D3D v9, HLSL никогда не касался D3D напрямую. Он был хорошей, но необязательной абстракцией. У разработчика всегда была возможность взять выхлоп компилятора и подправить его для максимальной производительности.
В языке от 3D Labs ничего этого не было. Вы отдаёте драйверу C-подобный язык, и он создаёт шейдер. На этом всё. Никакого ассемблерного шейдера, ничего, что можно скормить чему-то ещё. Только объект OpenGL, представляющий шейдер.
Для пользователей OpenGL это означало, что они становились подвержены капризам разработчиков OpenGL, которые только научились компилировать ассемблероподобные языки. В компиляторах новорождённого языка шейдеров OpenGL (GLSL) свирепствовали баги. Что ещё хуже, если вам удавалось заставить шейдер корректно компилироваться на различных платформах (что уже само по себе было большим достижением), то он всё ещё был подвержен оптимизаторам тех времён, которые были не так уж оптимальны, как могли бы быть.
Это было большим, но не единственным недостатком GLSL. Далеко не единственным.
В D3D, как и в старых ассемблерных языках OpenGL, можно было смешивать и всячески комбинировать вершинные и фрагментные шейдеры. Можно было использовать любой вершинный шейдер с любым совместимым фрагментным шейдером, если они взаимодействовали через одинаковый интерфейс. Более того, допускалась даже некоторая несовместимость: например, вершинный шейдер мог подать на выход значение, которое не использовалось фрагментным шейдером.
В GLSL ничего такого не было. Вершинный и фрагментный шейдер сплавлялись воедино, образовывая нечто, названное компанией 3D Labs «программным объектом». Поэтому, для совместного использования нескольких вершинных и фрагментных шейдеров в различных комбинациях, приходилось создавать несколько программных объектов. Это стало причиной второй по величине проблемы.
3D Labs думали, что они самые умные. Они взяли C/C++ за основу для модели компиляции GLSL. Это когда вы берёте один c-файл и и компилируете его в объектный файл, а затем берёте несколько объектных файлов и компонуете их в программу. Именно так компилируется GLSL: сначала вы компилируйте вершинный или фрагментный шейдер в шейдерный объект, затем помещаете эти объекты в программный объект и компонуете их воедино чтобы наконец сформировать программу.
В теории это позволяло появиться таким крутым вещам, как «библиотечные» шейдеры, которые содержат код, вызываемый основным шейдером. На практике это приводило к тому, что шейдеры компилировались дважды: один раз на стадии компиляции и второй раз на стадии компоновки. В частности, этим славился компилятор от nVidia. Он не генерировал какой-либо промежуточный объектный код; он компилировал вначале, выбрасывал полученный результат и компилировал заново на стадии компоновки.
Таким образом, чтобы приделать вершинный шейдер к двум разным фрагментным шейдерам, приходилось компилировать намного больше, чем в D3D. Особенно с учётом того, что вся компиляция производится оффлайново, а не перед непосредственным исполнением программы.
У GLSL были и другие проблемы. Возможно, было бы неправильным сваливать всю вину на 3D Labs, ибо в конечном итоге ARB утвердили и включили в OpenGL язык шейдеров (но ничего больше из предложений 3DLabs). Однако, изначальная идея всё равно была за 3D Labs.
И теперь самое печальное: 3D Labs были правы (в основном). GLSL не векторный язык, каким в то время был HLSL. Так случилось потому, что железо 3D Labs было скалярным (как современное железо от nVidia), и они были полностью правы в выборе направления, которому позднее последовали многие производители оборудования.
Они были правы и с выбором модели компиляции для «высокоуровневого» языка. Даже D3D в итоге к этому пришёл.
Проблема в том, что 3D Labs были правы в неправильное время. И в попытках попасть в будущее преждевременно, в попытках быть готовыми к будущему, они отставили в сторону настоящее. Это выглядит как T&L-функциональность в OpenGL, которая была в нём всегда. За исключением того, что T&L-конвейер OpenGL был полезным и до появления аппаратного T&L, а GLSL был обузой до того как остальной мир догнал его.
GLSL — это хороший язык сейчас. Но что было в то время? Он был ужасен. И OpenGL пострадал от этого.
На подходе к апофеозу
Я поддерживаю ту точку зрения, что 3D Labs нанесли OpenGL смертельный удар, но последний гвоздь в крышку гроба забил сам ARB.
Возможно вы слышали эту историю. Во времена OpenGL 2.1, у OpenGL были большие проблемы. Он тащил за собой огромный груз совместимости. API больше не был простым в использовании. Одну вещь можно было сделать пятью разными способами и непонятно какой из них быстрее. Можно было «изучить» OpenGL по простым руководствам, но при этом вы не изучали тот OpenGL, который даёт настоящую графическую мощь и производительность.
ARB решили предпринять ещё одну попытку изобрести OpenGL. Это было как «OpenGL 2.0» от 3D Labs, но лучше, потому что за этой попыткой стоял ARB. Они назвали это «Longs Peak».
Что такого плохого в том, чтобы потратить немного времени на улучшение API? Плохо то, что Microsoft оказалась в довольно шатком положении. Это было время перехода на Vista.
В Vista Microsoft решили внести давно напрашивающиеся изменения в графические драйверы. Они заставили драйверы обращаться к ОС за виртуализацией графической памяти и многим другим.
Можно долго спорить о достоинствах такого подхода, и о том, был ли он вообще возможен, но факт остаётся фактом: Microsoft сделала D3D 10 только для ОС Vista и выше. Даже на поддерживающем D3D железе было невозможно запустить D3D приложение без Висты.
Вы возможно помните, что Виста… скажем так, работала не очень хорошо. Итак, у нас была неторопливая ОС, новый API, который работал только на этой ОС, и новое поколение железа, которое нуждалось в этом API и ОС чтобы делать нечто большее, чем просто превосходить предыдущее поколение в производительности.
Тем не менее, разработчики могли использовать функциональность уровня D3D 10 через OpenGL. То есть могли бы, если бы ARB не был занят работой над Long Peaks.
ARB потратили добрые полтора-два года, работая над улучшением API. Ко времени выхода OpenGL 3.0 переход на Висту закончился, Windows 7 была на подходе, и разработчиков игр больше не заботила функциональность уровня D3D 10. В конце концов, оборудование для D3D 10 прекрасно работало с приложениями на D3D 9. С увеличением темпов портирования с ПК на консоли (или с переходом ПК-разработчиков на консольный рынок), разработчикам всё меньше была нужна функциональность D3D 10.
Если бы разработчики получили доступ к этой функциональности даже на Windows XP, развитие OpenGL могло бы получить живительный заряд бодрости. Но ARB упустили эту возможность. А хотите знать что самое ужасное?
ARB не смогли изобрести API с нуля несмотря на трату двух драгоценных лет на попытки сделать это. Поэтому они вернули статус-кво, добавив лишь механизм для объявления функциональности устаревшей.
В итоге ARB не только упустили ключевые возможности, но и не выполнили ту работу, которая привела их к этому упущению. Это был epic fail по всем направлениям.
Такова история о противостоянии OpenGL и Direct3D. История упущенных возможностей, величайшей глупости, умышленного безрассудства и банальных нелепостей.