понедельник, 24 января 2011 г.

Видеокерпич

Это Gainward GTX 580 Phantom. Страшно? И мне тоже...

Фюрер

Со Стиви беда, товарищи. Похоже, рак его всё-таки доканает... Так и помрёт посреди своей империи, своих миллиардов и посреди всех этих ебучих гаджетов, на которых он эти миллиарды заработал. С Александром Македонским всё так и случилось, кстати (только его отравили). Лишний повод задуматься, насколько легко всё превращается в прах в этом мире...

суббота, 22 января 2011 г.

MD2 loader update

Осталось перенести на Direct3D 11.

воскресенье, 16 января 2011 г.

С++: отделение интерфейса от реализации

Концепция инкапсуляции основана на разделении того, как выглядит объект (интерфейс), и того, как он в действительности работает (реализация). Но известно, что в С++ определение класса включает в себя описание как методов, так и типов данных и переменных-членов класса. Описание членов класса в файле заголовков (.h) приводит к необходимости подключать описания и определения дополнительных типов, что увеличивает объём компилируемого кода и как следствие, компиляция замедляется. Кроме того, при каждом изменении внутренних деталей определения происходит перекомпиляция всех зависимых файлов, даже если интерфейс остался неизменным.

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

1) Интерфейс представляется в виде абстрактного класса, где все методы чисто виртуальные. Реализация представлена производным классом, который содержит реализацию всех виртуальных методов и необходимые переменные-члены класса. Конструктором объекта выступает фабрика, возвращающая указатель на абстрактный интерфейс.

2) Идиома Pimpl. Интерфейс хранит указатель на непрозрачный (opaque) для клиента тип данных. Реализация типа данных и методов интерфейса размещается в .cpp файле, при этом методы класса-интерфейса просто вызывают соответствующие методы класса-реализации. Возможен и другой вариант: opaque тип данных содержит только члены-переменные класса, с которыми работают методы интерфейса: вместо обращения this->field добавляется один уровень косвенности: this->pimpl->field.

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

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

Для преодоления этой проблемы на вооружение была взята следующая идея:

Cделать различным определение класса для разных единиц трансляции: это возможно, т. к. в С и C++ единица трансляции компилируется автономно и независимо от остальных.

В данном случае роль дескриптора (opaque handle) играет некий базовый класс, от которого наследуется класс-интерфейс реализации. Представим себе некий класс CFoo, реализация которого должна быть скрыта, а клиенту предоставляется лишь интерфейс. Определим класс как производный от класса-дескриптора HFoo:

Declaration of CFoo
(для просмотра я рекомендую тему Slush and Poppies)

Напишем код клиента, который работает с интерфейсом класса CFoo:

Client

Мы определили дескриптор HFoo как пустую структуру, после чего стало возможным правильное определение CFoo: подключаем его определение и компилируем код. Всё нормально.

В файле реализации мы делаем следующий трюк: определяем HFoo так, как того требует реализация (например, нужны две целочисленные переменные), после этого подключаем определение:

Implementation of CFoo

Теперь класс CFoo имеет разные определения в файле реализации и в коде клиента, но это никак не мешает компиляции и сборке программы. Но работать программа не будет, при её запуске можно легко получить heap corruption или AV: почему? Дело в том, что для клиента CFoo имеет, как и положено, размер 1 байт, поскольку не содержит каких-либо членов класса. Поэтому CRT честно выделит 1 байт для экземпляра класса и вернёт указатель на него. Реализация же попытается обратиться как минимум к 2 * sizeof(int) байтам памяти, что вызовет ошибку. Решить проблему можно двумя способами: создать фабрику или перегрузить операторы new и delete для CFoo. Главное, чтобы память выделялась в файле реализации, где CFoo определяется как объект размером 8 байт. Мне больше нравится второй подход:

Extended definition of CFoo
Extended implementation of CFoo

Этот код компилируется и выполняется без ошибок как в MSC, так и в GCC (единственная сложность - неочевидное требование подключения cstddef для operator new(),  в противном случае получаем ошибку declaration of 'operator new' as non-function).

Достоинство данного подхода: никакого оверхеда по памяти и скорости выполнения. Недостаток: хак. Завтра я также постараюсь рассмотреть проблемы, возникающие при таком подходе и возможные пути их решения.

До встречи!

Update

Неудобно каждый раз для нового класса прописывать собственный аллокатор, поэтому мы эту работу поручим препроцессору. Более того, неудобно каждый раз определять пустую структуру-дескриптор перед включением .h файла с классом. Назовём эту структуру заголовком (header), и определим её в том же .h файле, где размещается определение соотв. класса, а определение обложим гвардами препроцессора:
#ifndef FOO_DECLARED
DECLARE_HEADER(Foo)
#endif
Макрос FOO_DECLARED описывается в файле реализации после определения "настоящего" дескриптора и перед включением заголовочного файла. Таким образом мы легко контролируем, какое именно определение дескриптора будет видно в файлах проекта.
 
Теперь о вещах более приземлённых.
 
Одним из недостатков подставного определения является то, что отладчик MS Visual Studio не видит переменные-члены класса во время отладки (сразу оговорюсь, что GDB в Xсode видит нормально). Досадно, но это факт: по каким-то причинам отладчик "видит" только пустую структуру-дескриптор без полей - понятно, что отладка такой программы быстро превратится в настоящий кошмар.

Чтобы преодолеть это досадное неудобство, можно в теле метода объявить указатель на дескриптор и присвоить ему this:
const HFoo *_this = this;
Ещё одно требование - HFoo должна размещаться в безымянном пространстве имён, иначе отладчик будет видеть поля структуры с переменным успехом (это зависит, похоже, от порядка сборки, а впрочем, не уверен). Если все условия выполнены, в процессе отладки можно видеть поля класса:

Ну я для GDB в этих триках нет необходимости

Поручим и эту работу по разрастанию кода препроцессору. Соберём все вспомогательные макросы в отдельный файл заголовков:


Вот как теперь выглядят сокращённые определение и реализация CFoo:


Макросы BEGIN_HEADER и END_HEADER я ввёл для упрощения определения дескриптора в безымянном пространстве имён, но, возможно, у кого-то появится решение получше.

Ну вот и всё. Дискасс из велкам :)

суббота, 8 января 2011 г.

MD2 loader

Мне как программисту тяжело создавать 3D-модели, а использовать кубики и шарики уже надоело. Решил выбрать какой-нить подходящий 3D-формат, с поддержкой анимации. Т. к. с MD5 у меня всплывали проблемы с расчётом adjacency, то выбор пал на формат MD2 из игры Quake II: формат прост и можно взять замечательные модельки прямо из игры.



Недостатком формата является то, что в нём используется повершинная анимация. Все "кадры" анимации хранятся в виде набора вершин, и т. к. кадров много и вершины в каждом кадре дублируются, то размер анимации получается немаленьким: в среднем 200-300 кб (до мегабайта). Для экономии памяти вершины хранятся в формате byte, каждый кадр содержит значения scale и translate для восстановления позиций вершин в формате float.

Оригинальный Quake II и всякие тулзы обычно рисуют MD2 прямо из системной памяти, делая масштабирование и интерполяцию кадров на CPU. Это было не тяжело, т. к. модели были самые настоящие low-poly (SM 5.0 и тесселятор никто ещё не юзал!). И хотя нагрузка на процессор тут минимальна, эту задачу лучше всего перенести на GPU.

Вершины всех кадров я разместил в одном вершинном буфере в формате GL_UNSIGNED_BYTE. Этот буфер привязан к вершинным атрибутам 0 и 1, но для нулевого атрибута указывается смещение для текущего кадра, а для первого - смещение для следующего за текущим кадра. Таким образом не надо создавать отдельный вершинный буфер для каждого кадра. Далее в шейдер передаются параметры scale и translate для каждого из двух кадров и вес для интерполяции полученных вершин. На выходе получаем анимированную модель.

среда, 5 января 2011 г.

DirectGL prototype is over

Думаю, что с DirectGL на данном этапе можно закругляться. 

Желаемые цели достигнуты: сторонние демки (включая DX SDK) компилируются, линкуются и запускаются с подставными интерфейсами. Все render states Direct3D 9 максимально близко переведены на OpenGL 2, так что приложения показывают идентичный результат. 

Кстати, в Windows 7 OpenGL 1.1 без драйверов эмулируется через Direct3D (майкрософт тоже написали враппер), так я смог на ноуте без OpenGL драйверов поиграть в Quake III. Тут я подумал: что, если на чистой Windows 7 запустить D3D9-демку через мой DirectGL: вызовы будут направляться OpenGL, который является обёрткой над Direct3D! Но проводить такие эксперименты я не стал, т. к. это слишком жестоко :)

Когда я только начинал наброски враппера, было интересно, получится из этого что-нибудь, или нет: фигли, портануть весь API! Но скилл, как выяснилось, позволяет, и теперь это становится даже скушным, т. к. растеризатор по своей природе примитивен, и сore API - тоже. Можно выделить несколько потенциальных проблем, которые могут возникнуть при портировании (например, экзотические атрибуты в vertex decl, HLSL-шейдеры и эффекты, инстансинг), но их придётся решать уже на конкретной задаче (а Wine решает "на месте"). Пока же можно было бы наращивать какой-нить рутинный функционал вроде загрузки DXT-текстур, работы с мешами, D3DX math (сейчас через dll-прослойку все вызовы перенаправляются в стандартный D3DX) и т. д., но это monkey job, и растить в себе кодера как-то не хочется.

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

вторник, 4 января 2011 г.

Basic HLSL

После нескольких часов отключения функционала Direct3D 10 из DXUT-а и ещё нескольких часов возни по замене stub-овых методов на dummy (т. е. на такие, что ничего реального не делают, но возвращают валидный указатель на объект), мне удалось запустить сэмпл из Direct3D 9 SDK - Basic HLSL.


Один из багов, из-за которого жирдяй DXUT отказывался стартовать, был в методе IDirect3D::GetDeviceCaps. Я реализовал его только частично, но выяснилось, что DXUT проверяет поле PixelShaderVersion структуры D3DCAPS9, которое я не заполнил. После исправления бага всё пошло как по-маслу. Теперь почти весь DXUT 9 реально работает с ложными интерфейсами Direct3D. Я отключил код, который загружает D3DX Effects и рисует через них интерфейс DXUT-a, и отключил ещё несколько функций загрузки, а в остальном "всё по-честному". В результате рисуется очищенное окно и текст :)

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

воскресенье, 2 января 2011 г.

Новый Год

В лесу родилась ёлочка
В квартире - умерла :(