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

:

«на русской сцене мы удивляем друг друга тем, что вообще что-то делаем», © 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 запакованных байта.

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

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

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