---== linux demomaking ==---
В этой статье я расскажу о том как написать движок для демки под linux.
Стоит заметить, что на это сподвигла меня дока madcr'a из x25zine#2.
Я расскажу только основы, конкретно как выводить картинку на экран,
крутить музыку и т.д.
Начнём как водится с главного, тобишь с графы. Первое что стоит
рассказать, так это то, как выводить на экран изображение без всяких Х.
В линухах (и наверное не только в них) есть такой интерфейс для работы
с видеокартой как Frame Buffer. Точнее даже не совсем интерфейс, скорее
девайс /dev/fbX. X на большинстве машин == 0, если конечно на твоей
машине не стоит более одной видео-карты. Для начала, чтобы работать
с frame_buffer'ом необходимо загрузить линух собственно в графическом
режиме, с поддержкой fb. Скорее всего так уже и есть (если в консоли у
тебя разрешение 800x600, 1024x768 или больше то всё ок). Если нет -
ковыряй конфиг lilo.
Допустим, у нас всё готово. Теперь о том как работать с /dev/fb: это
виртуальный деавйс символьного типа, в котором хранится текущее
``содержимое'' экрана. Вот небольшой пример:
$ cat /dev/fb0 > scrshot
Таким образом мы сохраняем текущее содержимое экрана ака скриншот в
файл scrshot. Соответсвенно, если писать в этот девайс, мы можем менять
текущую картинку, проще говоря писать на дисплее. Теперь ещё одна
проблема: а как узнать текущее разрешение и глубину цвета ``на лету'' ?
Вот для этого мы можем воспользоваться ioctl()-ом. Советую заглянуть в
файл /usr/include/linux/fb.h. Там описаны нужные нам структуры:
struct fb_var_screeninfo {
__u32 xres; /* горизонтальная развёртка экрана */
__u32 yres; /* вертикальная развёртка экрана */
__u32 xres_virtual; /* аналогично, но уже виртуальная развёртка */
__u32 yres_virtual;
__u32 xoffset; /* offset from virtual to visible */
__u32 yoffset; /* resolution */
__u32 bits_per_pixel; /* бит на пиксел, ака color depth */
__u32 grayscale; /* если не 0, цвета переводятся в градации серого */
struct fb_bitfield red; /* bitfield in fb mem if true color, */
struct fb_bitfield green; /* else only length is significant */
struct fb_bitfield blue;
struct fb_bitfield transp; /* прозрачность */
__u32 nonstd; /* != 0 Non standard pixel format */
__u32 activate; /* see FB_ACTIVATE_* */
__u32 height; /* высота картинки в мм */
__u32 width; /* ширина картинка в мм */
__u32 accel_flags; /* acceleration flags (hints) */
/* Timing: All values in pixclocks, except pixclock (of course) */
__u32 pixclock; /* pixel clock in ps (pico seconds) */
__u32 left_margin; /* time from sync to picture */
__u32 right_margin; /* time from picture to sync */
__u32 upper_margin; /* time from sync to picture */
__u32 lower_margin;
__u32 hsync_len; /* length of horizontal sync */
__u32 vsync_len; /* length of vertical sync */
__u32 sync; /* see FB_SYNC_* */
__u32 vmode; /* see FB_VMODE_* */
__u32 reserved[6]; /* Reserved for future compatibility */
};
struct fb_fix_screeninfo {
char id[16]; /* identification string eg "TT Builtin" */
unsigned long smem_start; /* Start of frame buffer mem */
/* (physical address) */
__u32 smem_len; /* Length of frame buffer mem */
__u32 type; /* see FB_TYPE_* */
__u32 type_aux; /* Interleave for interleaved Planes */
__u32 visual; /* see FB_VISUAL_* */
__u16 xpanstep; /* zero if no hardware panning */
__u16 ypanstep; /* zero if no hardware panning */
__u16 ywrapstep; /* zero if no hardware ywrap */
__u32 line_length; /* length of a line in bytes */
unsigned long mmio_start; /* Start of Memory Mapped I/O */
/* (physical address) */
__u32 mmio_len; /* Length of Memory Mapped I/O */
__u32 accel; /* Type of acceleration available */
__u16 reserved[3]; /* Reserved for future compatibility */
};
На самом деле самое ценное в структуре fb_fix_screeninfo это значение
line_length, хотя конечно его можно расчитать по простой формуле:
line_len = fb_var_struct.xres * fb_var_struct.bits_per_pixel / 8;
Вот небольшой пример того, как получить значения этих структур:
#includzz
int main(){
int fd;
struct fb_var_screeninfo inf0;
strcut fb_fix_screeninfo inf1;
fd = open("/dev/fb0", O_RDWR);
ioctl(fd, FBIOGET_VSCREENINFO, &inf0);
ioctl(fd, FBIOGET_FSCREENINFO, &inf1);
/* теперь мы можем ковырять полученные значения */
printf("screen resolution: %d x %d (%d bits)\n", inf0.xres, inf0.yres,
inf0.bits_per_pixel);
/* выставляем новые значения */
inf0.xoffset = 0;
inf0.yoffset = 0;
ioctl(fd, FBIOPUT_VSCREENINFO, &inf0);
close(fd);
return;
}
Тестим:
$ gcc -o test test.c
$ ./test
screen resolution: 1024 x 768 (16 bits)
$
Отлично, мы можем узнать всю необходимую инфу о настройках fb,а это как
раз то что нужно. Конечно можно теперь маллочить спейс объёмом
xres * yres * bits_per_pixel / 8, прорисовывать там каждый кадр и
выводить это дело на экран. Но есть техника получше. В unix существует
такой системный вызов, как mmap(). Для его использования в сях нужно
подключить файл <sys/mman.h>.Теперь объявляем, скажем войдовый, pointer
(назовём его scr_ptr). С помошью mmap() мы можем отобразить файл
/dev/fb0 в памяти и уже работать с ним как с локальным массивом, адрес
которого после вызова ядро поместит в наш указатель scr_ptr. mmap() сам
будет обновлять файл по мере изменения его образа в памяти, и наоборот.
Вот небольшой пример:
scr_ptr = mmap(0, scr_size, PROT_READ | PROT_WRITE, MAP_SHARED,
dev_fb0_descriptor, 0);
Теперь scr_ptr указывает на область памяти,в которой хранится наш кадр.
Изменяя его мы увидим как mmap() синхронизирует кадры. Пример:
memset(scr_ptr, 0, scr_size);
После выполнения данной функции весь экран зальёт белым цветом.Ещё один
простой пример:
memset(scr_ptr + 2048*3, 0xff, 2048);
Четвёртый ряд пикселей с верху станет чёрным. Ну и так далее. Рулясь с
оффсетами мы можем гулять по всему массиву. Скажем нам нужно записать в
точку с координатами (13,37) значение 0xc0 (началом координат считается
левый верхний угол экрана):
memset(scr_ptr + 2048*38 + 13, 0xc0, 2);
Хорошо, мы умеем рисовать на экране. Кстати, возможно кого-то смущает
откуда взялось 2048? Это значение, количество байт на строку, не трудно
подсчитать, зная количество бит на пиксел и разрешение, или взять из
структуры fb_fix_screeninfo.
Естественно, было бы глупо вырисовывать здоровые цветные картинки таким
допотопным способом (по-умному он называется векторное построение). Для
этого стоит считывать инфу из файлов и затем, преобразовав её, писать в
кадр. Тут есть некоторые трудности - линуксовый Vesafb не даёт нам
менять глубину цвета без перезагрузки системы, так что стоит для
удобства всю графику хранить в 16-ти битном формате (конкртенее, в unix
отлично отрисовывается палитра в формате "WinNT 16bits (565)", для
работы с которым в win9x нужно слить dll-ки [1]) или ``на лету''
конвертировать в него, что ужасно громоздко. С 16-битным кодированием
снова получаем гимор - 16 бит это 2 байта на пиксел,так что, к примеру,
хранить картинку размером 1024x768 очень громоздко. Можно конечно юзать
разные алгоритмы сжатия, но имхо проще всю демку зажать скажем в
.tar.bz, чем линковать i.e. zlib и распаковывать все ресурсы наружу
поотдельности. Скажем, 16-ти битовая растровая картинка размером ~800кб
после сжатия уменьшается до ~150 kb, что весьма неплохо. Ещё одна
интересная техника умеьшения размера картинки - использовать zoom перед
выводом на экран. Например, имага хранится в 512x384, а при выводе на
экран мы дублируем каждый второй пиксел по горизонтали и каждую вторую
строку по вертикали. Вообще-то, качество будет дерьмовое (по
горизонтали особенно), я лично использую только вертикальный зум, т.к.
при нём кадр смотрится вполне сносно. Зато размер файла можно уменьшить
в целых четыре раза!! Кстати, если исходная ``маленькая'' картинка не
изобилирует кучей мелких фрагментов, можно спокойно зумить её и по
горизонтали.
Ещё пара советов на последок о графике:
- при создании рисунка, который хочешь затолкать в демку, ставь его
длину и ширину кратными 8, т.к. это сэкономит много нервов и времени
при обработке (zoom/resize/crop) и выводе такой имаги.
- растровые картинки (.bmp) если кто не читал доку из прошлого номера
записываются в обратном порядке. Тоесть, файл надо читать с конца
блоками размером, равным длине экранной строки в байтах. Пример:
lseek(fb_device, -2048, SEEK_END); // читаем 1-ю строку
read(fb_device, scr_ptr, 2048);
lseek(fb_device, -2048*2, SEEK_END); // читаем 2-ю строку
read(fb_device, scr_ptr, 2048);
...
Теперь поговорим о звуке. В линухе мы можем прямо в консоли запустить
играть файл формата .au, а последние лет пять и PCM'овые .wav:
$ cat sound.au > /dev/audio
$ cat sound.wav > /dev/dsp
Рекомендуется использовать формат .au, т.к. он старее. Вообще, судя по
инклудам /dev/audio как и данный формат перекочевал со спарков. Юзать
mp3 очень гиморно, к тому же .au вполне неплохо держит качество звука,
а размер и вообще не может не радовать.
Но не всё так просто - как правило при выключении/ребуте системы все
звуковые настройки микшера звуковой карты (если он есть) ``слетают''.
Так что для совместимости стоит написать функцию, ``включающую'' звук.
Для этого нам понадобится глянуть в файл <linux/soundcard.h>:
/*********************************************
* IOCTL commands for /dev/mixer
*/
/*
* Mixer devices
*
* There can be up to 20 different analog mixer channels. The
* SOUND_MIXER_NRDEVICES gives the currently supported maximum.
* The SOUND_MIXER_READ_DEVMASK returns a bitmask which tells
* the devices supported by the particular mixer.
*/
#define SOUND_MIXER_NRDEVICES 25
#define SOUND_MIXER_VOLUME 0
#define SOUND_MIXER_BASS 1
#define SOUND_MIXER_TREBLE 2
#define SOUND_MIXER_SYNTH 3
#define SOUND_MIXER_PCM 4
#define SOUND_MIXER_SPEAKER 5
#define SOUND_MIXER_LINE 6
#define SOUND_MIXER_MIC 7
#define SOUND_MIXER_CD 8
#define SOUND_MIXER_IMIX 9 /* Recording monitor */
#define SOUND_MIXER_ALTPCM 10
#define SOUND_MIXER_RECLEV 11 /* Recording level */
#define SOUND_MIXER_IGAIN 12 /* Input gain */
#define SOUND_MIXER_OGAIN 13 /* Output gain */
/*
* The AD1848 codec and compatibles have three line level inputs
* (line, aux1 and aux2). Since each card manufacturer have assigned
* different meanings to these inputs, it's inpractical to assign
* specific meanings (line, cd, synth etc.) to them.
*/
#define SOUND_MIXER_LINE1 14 /* Input source 1 (aux1) */
#define SOUND_MIXER_LINE2 15 /* Input source 2 (aux2) */
#define SOUND_MIXER_LINE3 16 /* Input source 3 (line) */
#define SOUND_MIXER_DIGITAL1 17 /* Digital (input) 1 */
#define SOUND_MIXER_DIGITAL2 18 /* Digital (input) 2 */
#define SOUND_MIXER_DIGITAL3 19 /* Digital (input) 3 */
#define SOUND_MIXER_PHONEIN 20 /* Phone input */
#define SOUND_MIXER_PHONEOUT 21 /* Phone output */
#define SOUND_MIXER_VIDEO 22 /* Video/TV (audio) in */
#define SOUND_MIXER_RADIO 23 /* Radio in */
#define SOUND_MIXER_MONITOR 24 /* Monitor (usually mic) volume */
И немного макросов:
#define MIXER_READ(dev) _SIOR('M', dev, int)
#define SOUND_MIXER_READ_VOLUME MIXER_READ(SOUND_MIXER_VOLUME)
#define SOUND_MIXER_READ_BASS MIXER_READ(SOUND_MIXER_BASS)
#define SOUND_MIXER_READ_TREBLE MIXER_READ(SOUND_MIXER_TREBLE)
#define SOUND_MIXER_READ_SYNTH MIXER_READ(SOUND_MIXER_SYNTH)
#define SOUND_MIXER_READ_PCM MIXER_READ(SOUND_MIXER_PCM)
#define SOUND_MIXER_READ_SPEAKER MIXER_READ(SOUND_MIXER_SPEAKER)
#define SOUND_MIXER_READ_LINE MIXER_READ(SOUND_MIXER_LINE)
#define SOUND_MIXER_READ_MIC MIXER_READ(SOUND_MIXER_MIC)
#define SOUND_MIXER_READ_CD MIXER_READ(SOUND_MIXER_CD)
#define SOUND_MIXER_READ_IMIX MIXER_READ(SOUND_MIXER_IMIX)
#define SOUND_MIXER_READ_ALTPCM MIXER_READ(SOUND_MIXER_ALTPCM)
#define SOUND_MIXER_READ_RECLEV MIXER_READ(SOUND_MIXER_RECLEV)
#define SOUND_MIXER_READ_IGAIN MIXER_READ(SOUND_MIXER_IGAIN)
#define SOUND_MIXER_READ_OGAIN MIXER_READ(SOUND_MIXER_OGAIN)
#define SOUND_MIXER_READ_LINE1 MIXER_READ(SOUND_MIXER_LINE1)
#define SOUND_MIXER_READ_LINE2 MIXER_READ(SOUND_MIXER_LINE2)
#define SOUND_MIXER_READ_LINE3 MIXER_READ(SOUND_MIXER_LINE3)
/* Obsolete macros */
#define SOUND_MIXER_READ_MUTE MIXER_READ(SOUND_MIXER_MUTE)
#define SOUND_MIXER_READ_ENHANCE MIXER_READ(SOUND_MIXER_ENHANCE)
#define SOUND_MIXER_READ_LOUD MIXER_READ(SOUND_MIXER_LOUD)
#define SOUND_MIXER_READ_RECSRC MIXER_READ(SOUND_MIXER_RECSRC)
#define SOUND_MIXER_READ_DEVMASK MIXER_READ(SOUND_MIXER_DEVMASK)
#define SOUND_MIXER_READ_RECMASK MIXER_READ(SOUND_MIXER_RECMASK)
#define SOUND_MIXER_READ_STEREODEVS MIXER_READ(SOUND_MIXER_STEREODEVS)
#define SOUND_MIXER_READ_CAPS MIXER_READ(SOUND_MIXER_CAPS)
#define MIXER_WRITE(dev) _SIOWR('M', dev, int)
#define SOUND_MIXER_WRITE_VOLUME MIXER_WRITE(SOUND_MIXER_VOLUME)
#define SOUND_MIXER_WRITE_BASS MIXER_WRITE(SOUND_MIXER_BASS)
#define SOUND_MIXER_WRITE_TREBLE MIXER_WRITE(SOUND_MIXER_TREBLE)
#define SOUND_MIXER_WRITE_SYNTH MIXER_WRITE(SOUND_MIXER_SYNTH)
#define SOUND_MIXER_WRITE_PCM MIXER_WRITE(SOUND_MIXER_PCM)
#define SOUND_MIXER_WRITE_SPEAKER MIXER_WRITE(SOUND_MIXER_SPEAKER)
#define SOUND_MIXER_WRITE_LINE MIXER_WRITE(SOUND_MIXER_LINE)
#define SOUND_MIXER_WRITE_MIC MIXER_WRITE(SOUND_MIXER_MIC)
#define SOUND_MIXER_WRITE_CD MIXER_WRITE(SOUND_MIXER_CD)
#define SOUND_MIXER_WRITE_IMIX MIXER_WRITE(SOUND_MIXER_IMIX)
#define SOUND_MIXER_WRITE_ALTPCM MIXER_WRITE(SOUND_MIXER_ALTPCM)
#define SOUND_MIXER_WRITE_RECLEV MIXER_WRITE(SOUND_MIXER_RECLEV)
#define SOUND_MIXER_WRITE_IGAIN MIXER_WRITE(SOUND_MIXER_IGAIN)
#define SOUND_MIXER_WRITE_OGAIN MIXER_WRITE(SOUND_MIXER_OGAIN)
#define SOUND_MIXER_WRITE_LINE1 MIXER_WRITE(SOUND_MIXER_LINE1)
#define SOUND_MIXER_WRITE_LINE2 MIXER_WRITE(SOUND_MIXER_LINE2)
#define SOUND_MIXER_WRITE_LINE3 MIXER_WRITE(SOUND_MIXER_LINE3)
/* Obsolete macros */
#define SOUND_MIXER_WRITE_MUTE MIXER_WRITE(SOUND_MIXER_MUTE)
#define SOUND_MIXER_WRITE_ENHANCE MIXER_WRITE(SOUND_MIXER_ENHANCE)
#define SOUND_MIXER_WRITE_LOUD MIXER_WRITE(SOUND_MIXER_LOUD)
#define SOUND_MIXER_WRITE_RECSRC MIXER_WRITE(SOUND_MIXER_RECSRC)
Нас будет интересовать собственно общая громкость и громкость канала
PCM. Вот как мы можем выставить их значения на максимум:
int mixer, volume = 0xffffffff;
mixer = open("/dev/mixer", O_RDWR);
ioctl(mixer, SOUND_MIXER_WRITE_VOLUME, &volume);
ioctl(mixer, SOUND_MIXER_WRITE_PCM, &volume);
close(mixer);
Уровень громкости для каждого из каналов находится в значении volume.
Т.к. мне было дико лениво смотреть формат этой самой громкости я ставлю
максимум на сколько позволяет размер аргумента, тоесть 4 байта ).
На последок немного о работе с клавой. Существует два режима вывода в
консоли - графический и текстовый. Текстовый используется когда мы,
скажем просто работаем в консоли, при этом мы сразу видим то что
вводим, так называемое эхо. А вот в графическом моде эхо отключено. Вот
небольшой пример:
vtty = open("/dev/tty0", O_RDWR); // /dev/tty0 ссылается на текущий tty
ioctl(vtty, KDSETMODE, KD_GRAPHICS); // switch on graph mode
...
ioctl(vtty, KDSETMODE, KD_TEXT); // back to text mode
Как видно, всё довольно просто. Но этого конечно мало, также мы можем
выбрать режим чтения с клавиатуры:
- K_XLATE (дефолтовый), транслирует последовательности нажимаемых клавиш
через keymap и собственно выдаёт символы.
- K_UNICODE делает тоже, что и предидущий, но выдаёт не ASCII, а UTF-8
коды.
- K_RAW, самый илитный режим!!! Вообще никакой трансляции. Ядро выдаёт
не коды сиволов, а коды клавиш. Причём отсылаются они не только при
нажатии, но и при отпускании кнопы. Очень мощная штука, вот небольшой
список кодов клавиш:
key push_code release_code
+------------------------+---------------------+
escape 1 129
f1-f10 59..68 187..196
scroll_lock 70 198
backspace 14 142
Ну и так далее, полные списки можно найти в инклудах (linux/input.h).
Примеры:
ioctl (vtty, KDGKBMODE, &mode); // get current keyboard-mode
ioctl (vtty, KDSKBMODE, K_RAW); // set raw keyboard mode
Вот собственно и всё,что нужно для написания движка демки. А ``поверх''
него можно уже навешать всяких эффектов, etc.
Links:
[1] dll-ки для работы с 16-битными изображениями в win9x:
http://fly.to/mwgfx/ - ищите mwgfxdll.exe
[2] мой пример демки со всеми ресурсами (исходник в инклудах)
http://defaced.w6.ru/demo/