Это третья статья из четырёх. Полный цикл выглядит так:
OpenGL расширения
Рисуем треугольник
Ссылки на следующую часть добавлю позже —
как только она перестанет существовать только у меня в головепо мере появления материала
Исходный Код
Все исходники третьей главы можно потыкать на GitHub.
Перед тем как мы начнём по-настоящему издеваться над OpenGL, стоит признать неприятный факт: мы тратим непозволительно много времени на бойлерплейт.
Мы подключаем функции, прописываем константы, копируем typedef’ы и в целом занимаемся тем, что хочется автоматизировать уже на второй минуте.
Давайте разбираться, как с этим жить.
Структура заголовков
В случае GLFW3, как и в случае большинства библиотек — всё довольно тривиально: нам достаточно взять оригинальные заголовки в качестве основы и получить оттуда все дефайны вида #define GLFW_XXX YYY.
Это и будут наши константы:
// было // glfw3.h #define GLFW_VERSION_MAJOR 3 // стало // glfw3.php const GLFW_VERSION_MAJOR = 3;
А всё что идёт далее (все typedef и функции) — это уже часть API, которое мы можем вставить копипастой в “ресурсную” секцию PHP-библиотеки почти 1 в 1.
// было // glfw3.h GLFWAPI void glfwMakeContextCurrent(GLFWwindow* window); // стало // glfw3.php __halt_compiler(); // ... void glfwMakeContextCurrent(GLFWwindow* window);
Но может быть ещё что-то есть?
Устройство функций
Любая функция в FFI — это одновременно и метод:
$context->method();
Но одновременно и указатель на неё (анонимка):
$function = $context->method; $function();
Объявив функцию в GLFW3 и вызывая её как метод мы можем так же и получить и её саму, присвоив переменной:
// app.php // Мы вызывали вот так: $glfw->glfwMakeContextCurrent($window); // Но можем сделать и вот так: $glfwMakeContextCurrent = $glfw->glfwMakeContextCurrent; $glfwMakeContextCurrent($window);
Любая функция API может быть превращена в анонимную. А для удобства, у нас же процедурщина (добро пожаловать обратно в 2007ой, а говорят что не вернёмся…), можно сделать и заглушки для функций.
Для автокомплита. Да и чтоб писать код дальше хотя бы было не больно.
Например:
// src/glfw3.php function glfwMakeContextCurrent(mixed ...$args): void { static $glfwMakeContextCurrent = $glfw->glfwMakeContextCurrent; $glfwMakeContextCurrent(...$args); }
Сами адреса функций меняться не должны, поэтому такой костыль через static не вызовет проблем.
Более того, кажется, подобный финт ушами не должен сильно сказываться и на производительности, т.к. мы просто заменяем все вызовы метода у прокси (класс FFI) на такие же вызовы уже заранее известного замыкания.
Признаться, я протестировал несколько вариантов того, как можно добавить типизацию и автокомплит для этого FFI-прокси, который возвращается из
cdef. Остановился на варианте с обычными функциями: для удобства использования (это же туториал, а не промышленное решение), и по причине скорости работы результата.
Но есть проблема: откуда в функциях брать $glfw?
Можно, конечно сделать так же, как в своё время была сделана Си-шная реализация, передавая контекст в качестве первого аргумента, типа:
// src/glfw3.php // vvv vvvvv - вот сюда function glfwMakeContextCurrent(\FFI $glfw, mixed ...$args): void { static $glfwMakeContextCurrent = $glfw->glfwMakeContextCurrent; // ..
Или как староверы, сделав global $glfw глобальным… Ну, мол, гулять так гулять, классика 2000ых.
Но я предлагаю вариант с константой, чтоб было и не сильно паршиво, и при этом быстро и эффективно. Примерно на 5 битриксов из 10 по шкале “кто так вообще пишет, #&^@$(@”.
// src/glfw3.php define('GLFW_INSTANCE', FFI::cdef( ... )); // ... function glfwInit(mixed ...$args): mixed { static $function = GLFW_INSTANCE->glfwInit; return $function(...$args); }
Если реализовать подобный подход, то весь код нашего основного app.php:
Во-первых, значительно сократится.
Во-вторых, нам больше не придётся трогать код в других файлах.
В итоге, получим что-то вроде:
// app.php require __DIR__ . '/src/glfw3.php'; if (!glfwInit()) { exit(-1); } glfwWindowHint(GLFW_SAMPLES, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // ...etc $window = glfwCreateWindow(640, 480, 'Hello World', null, null);
Поздравляю, мы реализовали расширение GLFW, но на голом PHP и самостоятельно!
Можно теперь смело копировать код из примеров расширения и оно будет работать идентично нашему написанному. Ну почти. С полной поддержкой всех функций и всех констант за исключением OpenGL…
Теперь OpenGL
Помните, я когда-то упоминал о том, что для работы с OpenGL не обязательно тащить opengl32.dll или его аналоги? Настало время понять, почему.
Если кратко, то все функции OpenGL доступны через специальный загрузчик, через одну функцию xxxGetProcAddress:
// Получаем адрес функции через вот эту вот XXX-функцию $glClearAddr = $ffi->xxxGetProcAddress('glClear'); // Говорим что этот адрес - это функция с нужными аргументами (через cast) // В данном случае, ничего не получает и ничего не возвращает $glClear = $ffi->cast('void (*)(void)', $glClearAddr); // Вызываем $glClear();
То есть логика работы OpenGL (и Vulkan, кстати, тоже) очень подозрительно похожа на ту, реализацией которой мы занимались выше.
Но откуда же брать вот эти xxxGetProcAddress?
Загрузка функций OpenGL
Тут начинается тот самый пресловутый платформенный зоопарк:
Windows -> WGL и функция
wglGetProcAddressLinux -> GLX и функция
glXGetProcAddressARBAndroid -> EGL и функция
eglGetProcAddressmacOS + iOS -> Гарри Поттер и хз что там… Metal?
…
Зачем так сделано?
Как вы можете догадываться — ваша видеокарта (хардварно имеется ввиду) может поддерживать только определённый набор функций OpenGL (например, версию, 3.3), а всё остальное просто недоступно. Таким образом, для загрузки какой-либо функции можно проверить её на доступность, а уже потом загрузить, выбросить ошибку или ещё что. Причём функция может предоставляться именно драйверами и эмулироваться на уровне софта, если на уровне железа такое недоступно.
Тоже самое и с расширениями. OpenGL — это расширяемый API. Каждый вендор (производитель т.е.) может добавлять туда свой функционал, например поддержку пресловутых “лучей”, однако реализовать не как часть спецификации OpenGL, а через суффикс, например NV для NVidia, AMD для AMD. После стабилизации API и принятия набора функций — суффикс (или префикс) убирается и/или заменяется на ARB (Architecture Review Board).
Таким образом, этот загрузчик (часть WGL/GLX/EGL/etc) позволяет динамически менять набор доступных функций в OpenGL API, в зависимости от видеокарты, драйвера, его версии и прочих внешних параметров (например конфигурации, когда мы сообщали о том, что “нам не нужно старое API”). А сами функции могут располагаться в различных частях системы (например в тех же драйверах) и браться уже оттуда.
Получаем некую реализацию расширяемого factory для функций: “Загрузчик, дай мне функцию glClear, не важно откуда ты её возьмёшь”.
И тут приходит GLFW
Как же замечательно, что у нас есть GLFW! Не взирая на то, какая у нас ОС и где располагается само API загрузки — GLFW сам выберет и найдёт нужное.
Библиотека сама выбирает загрузчик и предоставляет целую ОДНУ кроссплатформенную функцию glfwGetProcAddress вместо этого разношёрстного зоопарка.
Теперь нам не нужен отдельный src/opengl.php, а всё необходимое из OpenGL можно впихуть в тот же самый src/glfw3.php.
// src/glfw3.php function glClear(mixed ...$args): void { static $function = GLFW_INSTANCE->cast( 'void (*)(GLbitfield mask);', GLFW_INSTANCE->glfwGetProcAddress('glClear'), ); $function(...$args); }
Но можно и лучше!
Если посмотреть на glcorearb.h, можно обнаружить, что для этой функции (glClear) уже определён тип PFNGLCLEARPROC.
typedef void ( * PFNGLCLEARPROC) (GLbitfield mask); // Формат: "PFN" + NAME (в верхнем регистре) + "PROC"
И тогда код упрощается:
// src/glfw3.php function glClear(mixed ...$args): void { static $function = GLFW_INSTANCE->cast( 'PFNGLCLEARPROC', // не надо гадать какая там сигнатура GLFW_INSTANCE->glfwGetProcAddress('glClear'), ); $function(...$args); }
Ну и да, этот код можно весь сгенерировать из исходников!
Генерация GLFW + OpenGL бриджей
Для этого можно воспользоваться, например:
Препроцессором
ffi/preprocessor, который позволит взять оригинальные заголовочные файлы на C и как “собрать” (т.е. “спрепроцессить”) их под формат PHP, так и получить информацию о том что было сделано (например, какие дефайны были определены).Генератором автокомплита
ffi/ide-helper-generator, который уже позволит сгенерировать нужные заглушки для функций с корректными типами и именами аргументов.
Лично я воспользоваться просто препроцессором (первым пунктом), а аргументы оставил как mixed, просто чтобы код кодгена был покороче и эти 200 строк были относительно читаемыми. Опять некий баланс между удобством и качеством.
Да, вот эти 20 тысяч строк кода полностью сгенерированы из исходников GLFW и OpenGL.
Для GLFW выбрана версия 3.3, вместо 3.4. Так как актуальные Linux (вроде Ubuntu 24.04) поставляется именно с ней. Зачем вам лишний гемор с ручной пересборкой под последнюю 3.4?
В любом случае, теперь мы больше не будем возвращаться к этому файлу src/glfw3.php для того, чтобы туда что-то добавить или определить константу (надеюсь). А все константы и функции OpenGL так же поставляются вместе с этим файлом.
…ах да, старый src/opengl.php — уже не актуален, можно его удалить. Код теперь полностью кроссплатформенный и переносимый, и без отдельной загрузки OpenGL.
Итоги
Повторили +90% (смею надеяться) всех возможностей ext-glfw, сделав полностью совместимый API и избавились от необходимости руками каждый раз добавлять в наш src/glfw3.php что-либо. Можно было, конечно, и в самом начале это сделать, но тогда не совсем понятно было бы и как оно работает, и что за WGL/EGL/GLX/прочХ API.
Теперь нам предстоит нарисовать что-то интересное, используя и буферы, и шейдеры, и прочую мутатень.
Все исходники третьей главы можно потыкать на GitHub.
