Создание 1k/4k intro для Linux, часть 1

Источник: habrahabr
w23

"на русской сцене мы удивляем друг друга тем, что вообще что-то делаем", © manwe
(из статуса SCRIMERS на demoscene.ru/forum/)

Пятиминутка мета: в этом тексте вам, котятки, предстоит прочитать о том, как потратить свое время совершенно неэффективно с точки зрения отношения размера полученного продукта к потраченным времени и усилиям.
Предположим, что мы хотим сделать что-нибудь эдакое, например, интру размером до 4кб, но мы нищеброды, и у нас нет денег на виндовс и видеокарту с шейдерами, поддерживающими ветвления. Или мы просто не хотим брать стандартный набор из apack/crinkler/sonant/4klang/боже-что-там-еще-есть, делать очередную "смотрите все! я тоже умею рэймарчинг дистанс филдс!" и теряться среди десятков-сотен таких же. Ну или же мы просто любим выпендриваться как попало в надежде, что девочки на нас наконец-то обратят внимание.

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

Что для этого мы можем задействовать? Прежде всего, у нас есть X11 и OpenGL. Как их можно проинициализировать наименьшей кровью:

  1. Напрмую через Xlib, GLX
    +: гаранитированно есть в системе;
    -: 11 вызовов только для того, чтобы поднять GL-контекст
  2. glut
    +: 4-5 функций для GL-контекста;
    -: есть далеко не везде
  3. SDL
    +: 2 вызова для контекста, де-факто стандарт, есть практически везде;
    -: может где-то вдруг не быть, или в каком-нибудь будущем потенциально поползти совместимость с новыми версиями
  4. забить на библиотеки и делать все руками
    +: можно выкинуть динамическую линковку и получить, по слухам (viznut?), около 300 байт для инициализации GL-контекста
    -: можно себе всё сломать, пока разберёшься

Последний вариант, несомненно, самый тру, но он слишком сложен для меня пока. Поэтому будем делать предпоследний.

Давайте расчехлим gcc и попробуем почувствовать всё то, с чем нам предстоит иметь дело. Начнем со скелета нашей интры - инициализируем OpenGL-контекст, укажем viewport и запустим простенький eventloop, завершающий приложение при нажатии любой клавиши:

#include <SDL.h> #define W 1280 #define H 720 #define FULLSCREEN 0//SDL_FULLSCREEN int main(int argc, char* argv) { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(W, H, 32, SDL_OPENGL / FULLSCREEN); glViewport(0, 0, W, H); SDL_Event e; for(;;) { SDL_PollEvent(&e); if (e.type == SDL_KEYDOWN) break; // что-нибудь нарисуем здесь потом SDL_GL_SwapBuffers(); } SDL_Quit(); return 0; }

(Внимательный читатель здесь может обнаружить следующее:

  • оптимизм, затмевающий солнце - полное отсутствие проверок на ошибки
  • отсутствие проверки на e.type == SDL_QUIT (обработка закрытия окна пользователем), что будет слегка нервировать любителей закрывать приложения кликом на крестик, а не нажатием произвольной клавиши

Отвечу: то ли еще будет!)

Скомпилируем его:

(Примечание1: для пользователей бинарных дистрибутивов: вам потребуются компилятор (gcc), заголовочные файлы для OpenGL (обычно лежат в mesa) и версия SDL для разработчиков (libsdl-dev, или что-то около того).)
(Примечание2: флаг -m32 нужен только для обладателей 64-битных дистрибутивов)

cc -m32 -o intro intro.c `pkg-config --libs --cflags sdl` -lGL

И ужаснемся тому, что такая простенькая программа занимает почти 7кб!

Чешем репу, понимаем, что нам на фиг не сдался crt и прочие буржуйские излишества, поэтому вырезаем их.
Нужно:

  • Вместо main() объявить фнукцию _start() без аргументов и возвращаемого значения (ее можно переименовать, но зачем?).
  • Вместо простого выхода из этой функции сделать системный вызов выхода из приложения (eax=1, ebx=exit_code).

#include <SDL.h> #define W 1280 #define H 720 #define FULLSCREEN 0//SDL_FULLSCREEN void _start() { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(W, H, 32, SDL_OPENGL / FULLSCREEN); glViewport(0, 0, W, H); SDL_Event e; for(;;) { SDL_PollEvent(&e); if (e.type == SDL_KEYDOWN) break; // что-нибудь нарисуем здесь потом SDL_GL_SwapBuffers(); } SDL_Quit(); asm ( \ "xor %eax,%eax\n" \ "inc %eax\n" \ "int $0x80\n" \ ); }
Кроме этого, надо указать gcc параметр -nostartfiles.
И заодно сразу стрипнем бинарник:
cc -m32 -o intro intro.c `pkg-config --libs --cflags sdl` -lGL -Os -s -nostartfiles

Получаем ~4.7 килобайта, что на ~30% лучше, но все еще очень много.
В этой программе полезного кода ещё кот наплакал, и практически все место занимают различные elf-заголовки. Для выпиливания бесполезностей из заголовков существует утилита sstrip из набора elfkickers (http://www.muppetlabs.com/~breadbox/software/elfkickers.html):
sstrip intro
Это дает нам ещё примерно 600 байт и опасно приближает размер бинарника к 4кб. И это он ещё ничего полезного-то не делает.

В очередной раз расстраиваемся, смотрим в хекс-редакторе на наш бинарник и обнаруживаем нули. Много их. А кто лучше всех сражается с нулями и прочими одинаковостями?
Правильно:
cat intro / 7z a dummy -tGZip -mx=9 -si -so > intro.gz
(Примечание1-очевидность: нужно поставить пакет p7zip. Почему p7zip и такая сложность? Потому что: (а) gzip и прочие сжимают на несколько процентов хуже, (б) надо убрать gz-заголовок с лишними метаданными, вроде имени файла)
(Примечание2: почему вообще gzip, а не, скажем, bzip2? потому что эмпирически p7zip -tGZip дает лучший результат из всего того, что я перепробовал, и распаковщики чего есть более-менее везде)
intro.gz занимает уже около 650 байт! Осталось только сделать его запускаемым. Создадим файл unpack_header со следующим содержанием:
T=/tmp/i;tail -n+2 $0/zcat>$T;chmod +x $T;$T;rm $T;exit
и прилепим этот заголовок к нашему архиву:
cat unpack_header intro.gz > intro.sh
Сделаем файл запускаемым и посмотрим на то, что у нас получилось:
chmod +x intro.sh ./intro.sh
Убедимся в том, что эти 700 с копейками байт действительно создают пустое окно.
То, что мы сейчас сделали, называется gzip-дроппингом, и корни его уходят в популярный в свое время (до эры crinkler и ребят) метод cab-dropping, который делал ровно то же самое, но под виндой.

Давайте посмотрим, а много ли можно захерачить в оставшиеся 300 байт. Традиционно делаем следующее: выводим на весь экран glRect, который отрисовывается специальными шейдерами вида color = f(x,y):
#include <SDL.h> #include <GL/gl.h> #define W 1280 #define H 720 #define FULLSCREEN 0//SDL_FULLSCREEN char* shader_vtx[] = { "varying vec4 p;" "void main(){gl_Position=p=gl_Vertex;}" }; char* shader_frg[] = { "varying vec4 p;" "void main(){" "gl_FragColor=p;" "}" }; void shader(char** src, int type, int p) { int s = glCreateShader(type); glShaderSource(s, 1, src, 0); glCompileShader(s); glAttachShader(p, s); } void _start() { SDL_Init(SDL_INIT_VIDEO); SDL_SetVideoMode(W, H, 32, SDL_OPENGL / FULLSCREEN); glViewport(0, 0, W, H); int p = glCreateProgram(); shader(shader_vtx, GL_VERTEX_SHADER, p); shader(shader_frg, GL_FRAGMENT_SHADER, p); glLinkProgram(p); glUseProgram(p); SDL_Event e; for(;;) { SDL_PollEvent(&e); if (e.type == SDL_KEYDOWN) break; glRecti(-1,-1,1,1); SDL_GL_SwapBuffers(); } SDL_Quit(); asm ( \ "xor %eax,%eax\n" \ "inc %eax\n" \ "int $0x80\n" \ ); }
Код довольно прозрачный, кроме, пожалуй, момента с varying vec4 p, которая служит для протаскивания нормализованных экранных координат из вершинного шейдера во фрагментный, и делает нас независимыми от физических размеров окна.

Итак, попробуем собрать:
cc -m32 -o intro intro.c `pkg-config --libs --cflags sdl` -lGL -Os -s -nostartfiles && \ sstrip intro && \ cat intro / 7z a dummy -tGZip -mx=9 -si -so > intro.gz && \ cat unpack_header intro.gz > intro.sh && chmod +x intro.sh && \ wc -c intro.sh && \ ./intro.sh

Обнаруживаем, что такой мелочью мы уже хапнули лишних 50 байт, и вылезли за дозволенный один килобайт. "Ты у меня еще попляшешь, попляшешь!", думаем мы и достаем из-за пазухи последний трюк: ручная загрузка всех необходимых функций из динамических библиотек:
#include <dlfcn.h> #include <SDL.h> #include <GL/gl.h> #define W 1280 #define H 720 #define FULLSCREEN 0//SDL_FULLSCREEN const char* shader_vtx[] = { "varying vec4 p;" "void main(){gl_Position=p=gl_Vertex;}" }; const char* shader_frg[] = { "varying vec4 p;" "void main(){" "gl_FragColor=p;" "}" }; const char dl_nm[] = "libSDL-1.2.so.0\0" "SDL_Init\0" "SDL_SetVideoMode\0" "SDL_PollEvent\0" "SDL_GL_SwapBuffers\0" "SDL_Quit\0" "\0" "libGL.so.1\0" "glViewport\0" "glCreateProgram\0" "glLinkProgram\0" "glUseProgram\0" "glCreateShader\0" "glShaderSource\0" "glCompileShader\0" "glAttachShader\0" "glRecti\0\0\0"; // удобство: #define _SDL_Init ((void(*)(int))dl_ptr[0]) #define _SDL_SetVideoMode ((void(*)(int,int,int,int))dl_ptr[1]) #define _SDL_PollEvent ((void(*)(void*))dl_ptr[2]) #define _SDL_GL_SwapBuffers ((void(*)())dl_ptr[3]) #define _SDL_Quit ((void(*)())dl_ptr[4]) #define _glViewport ((void(*)(int,int,int,int))dl_ptr[5]) #define _glCreateProgram ((int(*)())dl_ptr[6]) #define _glLinkProgram ((void(*)(int))dl_ptr[7]) #define _glUseProgram ((void(*)(int))dl_ptr[8]) #define _glCreateShader ((int(*)(int))dl_ptr[9]) #define _glShaderSource ((void(*)(int,int,const char**,int))dl_ptr[10]) #define _glCompileShader ((void(*)(int))dl_ptr[11]) #define _glAttachShader ((void(*)(int,int))dl_ptr[12]) #define _glRecti ((void(*)(int,int,int,int))dl_ptr[13]) void* dl_ptr[14]; // функция квази-ручной загрузки динамических библиотек void dl() { const char* pn = dl_nm; void** pp = dl_ptr; for(;;) // для всех библиотек { void* f = dlopen(pn, RTLD_LAZY); // откроем for(;;) // для всех ее precious функций { while(*(pn++) != 0); // пропускаем все байты до следующего за нулем байта if (*pn == 0) break; // если и он ноль, то это конец текущей so'шки *pp++ = dlsym(f, pn); } // закончили с текущей библиотекой if (*++pn == 0) break; // если за нулем-разделителем тоже ноль, то все, это конец } } void shader(const char** src, int type, int p) { int s = _glCreateShader(type); _glShaderSource(s, 1, src, 0); _glCompileShader(s); _glAttachShader(p, s); } void _start() { dl(); _SDL_Init(SDL_INIT_VIDEO); _SDL_SetVideoMode(W, H, 32, SDL_OPENGL / FULLSCREEN); _glViewport(0, 0, W, H); int p = _glCreateProgram(); shader(shader_vtx, GL_VERTEX_SHADER, p); shader(shader_frg, GL_FRAGMENT_SHADER, p); _glLinkProgram(p); _glUseProgram(p); SDL_Event e; for(;;) { _SDL_PollEvent(&e); if (e.type == SDL_KEYDOWN) break; _glRecti(-1,-1,1,1); _SDL_GL_SwapBuffers(); } _SDL_Quit(); asm ( \ "xor %eax,%eax\n" \ "inc %eax\n" \ "int $0x80\n" \ ); }
Ух! Наконец-то становится более-менее мясисто и запутанно!
Что здесь происходит: мы храним нужные нам библиотеки и функции из них одной строкой через нули-разделители, вместо того, чтобы все эти данные хранить в весьма рыхлых и плохо пакующихся структурах внутри elf-заголовка. Функции из строки загружаются в просто массив указателей, из которого они уже в нужный момент вызываются через макросы. Стоит отметить, что конкретные типы параметров в разумных пределах не имеют значения - они все равно будут выровнены на 4 байта в стеке. А возвращаемое значение так и вообще можно игнорировать, если оно не нужно - все равно вызывающая функция, по соглашениям, не рассчитывает на то, что eax будет сохранен. И тем более не надо проверять на ошибки - на ошибки проверяют только слюнтяи и тряпки, которым никто не даёт.
В команде сборки разденем уж бинарник совсем до гола, оставив ему зависимости только от ld-linux.so (интерпретатор, позволяющий динамическую линковку вообще), и libld.so, в которой лежат функции dlopen и dlsym:
cc -Wall -m32 -c intro.c `pkg-config --cflags sdl` -Os -nostartfiles && \ ld -melf_i386 -dynamic-linker /lib32/ld-linux.so.2 -ldl intro.o -o intro && \ sstrip intro && \ cat intro / 7z a dummy -tGZip -mx=9 -si -so > intro.gz && \ cat unpack_header intro.gz > intro.sh && chmod +x intro.sh && \ wc -c intro.sh && \ ./intro.sh
899 байт! Ещё целых сто с жирком байт можно забить Творчеством™!

Что же туда можно уместить? Например, старинный эффект туннеля, от которого всех уже тошнит (да-да, тот самый из скриншота наверху).
Для начала нам нужно протащить время в шейдеры. Мы не будем делать это как ботаны через glUniform, а поступим по-простому, как дворовые пацаны:
float t = _SDL_GetTicks() / 1000. + 1.; _glRectf(-t, -t, t, t);
Для того, чтобы время не терялось в интерполяции, нужна небольшая помощь со стороны вершинного шейдера:
const char* shader_vtx[] = { "varying vec4 p;" "void main(){gl_Position=p=gl_Vertex;p.z=length(p.xy);}" };
Всё, теперь в компоненте p.z у нас лежит текущее время. Осталось только прифигачить его к туннелю.
Туннель делается просто - у нас есть угол a=atan(p.x,p.y) и перспектива по-деревенски: z=1./length(p.xy), остается только сгенерировать какую-нибудь текстуру color=f(a,z,t). В сами координаты время, конечно же, тоже можно подмешать. Да и вообще все можно делать, например, бросить читать этот бред нафиг и пойти гулять - там же такая офигенская ясная погода, большие чистые сугробы и сосновый бор в двух шагах от дома!

В общем, методом научно-итерационного тыка получаем такое:
#include <dlfcn.h> #include <SDL.h> #include <GL/gl.h> #define W 1280 #define H 720 #define FULLSCREEN 0//SDL_FULLSCREEN const char* shader_vtx[] = { "varying vec4 p;" "void main(){gl_Position=p=gl_Vertex;p.z=length(p.xy);}" }; const char* shader_frg[] = { "varying vec4 p;" "void main(){" "float " "z=1./length(p.xy)," "a=atan(p.x,p.y)+sin(p.z+z);" "gl_FragColor=" "2.*abs(.2*sin(p.z*3.+z*3.)+sin(p.z+a*4.)*p.xyxx*sin(vec4(z,a,a,a)))+(z-1.)*.1;" "}" }; const char dl_nm[] = "libSDL-1.2.so.0\0" "SDL_Init\0" "SDL_SetVideoMode\0" "SDL_PollEvent\0" "SDL_GL_SwapBuffers\0" "SDL_GetTicks\0" "SDL_Quit\0" "\0" "libGL.so.1\0" "glViewport\0" "glCreateProgram\0" "glLinkProgram\0" "glUseProgram\0" "glCreateShader\0" "glShaderSource\0" "glCompileShader\0" "glAttachShader\0" "glRectf\0\0\0"; // удобство: #define _SDL_Init ((void(*)(int))dl_ptr[0]) #define _SDL_SetVideoMode ((void(*)(int,int,int,int))dl_ptr[1]) #define _SDL_PollEvent ((void(*)(void*))dl_ptr[2]) #define _SDL_GL_SwapBuffers ((void(*)())dl_ptr[3]) #define _SDL_GetTicks ((unsigned(*)())dl_ptr[4]) #define _SDL_Quit ((void(*)())dl_ptr[5]) #define _glViewport ((void(*)(int,int,int,int))dl_ptr[6]) #define _glCreateProgram ((int(*)())dl_ptr[7]) #define _glLinkProgram ((void(*)(int))dl_ptr[8]) #define _glUseProgram ((void(*)(int))dl_ptr[9]) #define _glCreateShader ((int(*)(int))dl_ptr[10]) #define _glShaderSource ((void(*)(int,int,const char**,int))dl_ptr[11]) #define _glCompileShader ((void(*)(int))dl_ptr[12]) #define _glAttachShader ((void(*)(int,int))dl_ptr[13]) #define _glRectf ((void(*)(float,float,float,float))dl_ptr[14]) static void* dl_ptr[15]; // функция квази-ручной загрузки динамических библиотек static void dl() __attribute__((always_inline)); static void dl() { const char* pn = dl_nm; void** pp = dl_ptr; for(;;) // для всех библиотек { void* f = dlopen(pn, RTLD_LAZY); // откроем for(;;) // для всех ее precious функций { while(*(pn++) != 0); // пропускаем все байты до следующего за нулем байта if (*pn == 0) break; // если и он ноль, то это конец текущей so'шки *pp++ = dlsym(f, pn); } // закончили с текущей библиотекой if (*++pn == 0) break; // если за нулем-разделителем тоже ноль, то все, это конец } } static void shader(const char** src, int type, int p) __attribute__((always_inline)); static void shader(const char** src, int type, int p) { int s = _glCreateShader(type); _glShaderSource(s, 1, src, 0); _glCompileShader(s); _glAttachShader(p, s); } void _start() { dl(); _SDL_Init(SDL_INIT_VIDEO); _SDL_SetVideoMode(W, H, 32, SDL_OPENGL / FULLSCREEN); _glViewport(0, 0, W, H); int p = _glCreateProgram(); shader(shader_vtx, GL_VERTEX_SHADER, p); shader(shader_frg, GL_FRAGMENT_SHADER, p); _glLinkProgram(p); _glUseProgram(p); SDL_Event e; for(;;) { _SDL_PollEvent(&e); if (e.type == SDL_KEYDOWN) break; float t = _SDL_GetTicks() / 400. + 1.; _glRectf(-t, -t, t, t); _SDL_GL_SwapBuffers(); } _SDL_Quit(); asm ( \ "xor %eax,%eax\n" \ "inc %eax\n" \ "int $0x80\n" \ ); }

Эта няшка собирается моим gcc-4.5.3 в ровно 1024 запакованных байта.

Окей, думаем мы, вряд ли здесь можно сделать что-нибудь ещё лучше и сложнее. Почти довольные собой лезем смотреть на то, что делают другие ребята в данной категории…

И впадаем в уныние.

Ничего не остаётся, придётся лезть в ассемблер.
Но об этом как-нибудь в следующий раз.


Страница сайта http://185.71.96.61
Оригинал находится по адресу http://185.71.96.61/home.asp?artId=34980