making of "blash" - код и железо
(осторожно, большие фотографии и обилие
Так как я обещал продолжение, то оно уже тут :) Предыдущие записи можно найти по тегу «blash».
… Итак, общая идея демы выработана, теперь нужно ее реализовать. С платформой я уже определился, теперь требуется выбрать язык, на котором и будет написан код. Выбор был, честно скажу, небогат:
- от Бейсиков на PC меня уже тошнило — QuickBasic слишком медлителен (однако даже на нем делают демы :), а порт FreeBasic под DOS, мягко говоря, неидеален, более того, обладает целым рядом глюков.
- Паскаль. Я делал некоторые наброски на Turbo Pascal 7.0, в частности, генерил таблички для ldi (Processing? какой Processing?), но в плане скорости голый Паскаль был далек от совершенства, заставляя писать огромное количество кода на inline-ассемблере, да еще и без толковой поддержки инструкций IA-32 (читай i386 и выше), да еще либо в реальном режиме с пресловутыми 640 килобайтами, либо с косячным DPMI-сервером… Ну уж неееееееееееет :). Добавим сюда и проблемы со звуковыми системами (плееров .XM-файлов под TP7 я в природе не видел, а без музыки дема — не дема)
FreePascal тоже был отметен, но по другой причине — проблемы компиляции и отладки исполняемых файлов, да и размер не прельщал. - Ассемблер. Хардкор-вариант, на который я просто не был морально готов.
Оставался последний вариант — С. Watcom C.
Прежде всего, Watcom я полюбил за великолепную оптимизацию выходного кода (зачастую вставки на ассемблере по скорости проигрывали написанному на Си коду, что меня первое время серьезно удивляло), отличную поддержку защищенного режима и обилие внешних сторонних библиотек. Я нашел Open Watcom 1.9, настроил среду и начал писать.
DOS-расширитель — конечно же PMODE/W! Скорость, самостоятельность (не нужен отдельный EXE-шник, в отличие от DOS/4GW) и отсутствие лишних вопросов (это относится к DOS32) — главные его достоинства, да и памяти теперь можно использовать хоть все четыре гигабайта (которых у меня нет ;)
Ах да, я чуть описать платформы, на которых все писалось и тестировалось:
- Pentium 200 MMX (время от времени разгоняемый до 250 МГц)\ ASUS TXP4 на i430TX \ 128 МБ SDRAM \ Matrox Millennium \ 40 ГБ IDE \ Realtek 8139C \ AWE64 Value \ Windows 98SE — основная кодингмашина (под нее все и писалось)
- Pentium 133 \ LuckyStar P55CE на i430FX (казалось бы, причем тут nyuk … ;) \ 32 МБ EDO \ пачка S3-шек \ пачка всяких звуковух — на этой платформе фиксилась финальная версия
- Celeron 300A \ некая помидорина на i440LX \ 32 МБ SDRAM \ GeForce 2 MX200 \ разные звуковухи — а эта машина была наиболее близкой к компотачке — там стояла P2-333
- … ну и конечно же DOSBox различных версий и сборок
На основной машине в качестве редактора использовался Notepad++, а компьютеры соединялись локальной сетью, что позволяло быстро проверить свеженаписанный код на целевой машине.
Из внешних либ использовался Useless Module Player v1.11 beta by FreddyV\Useless. В принципе, если бы я писал дему сейчас, я бы заюзал Indoor Music System 0.6 by pascal, так как я устал ловить глюки в USMP (например, встроенная система таймеров вызывала дергания и секучки на экране, так что пришлось писать таймер самому), хотя… благодаря понятным исходникам USMP я даже добавил в нее поддержку Covox (для прикола :). Но при этом косяков в проигрывнии XM-ок (а именно в этом формате и написана музыка из демы) не было, особенно после установки багфикса.
Отладка на различных платформах — важная вещь, так как можно отловить различные непредвиденные глюки :)
Здесь лирическое(?) отступление заканчивается и начинается, собственно, разбор blash по косточкам. Поехали!
Начало
В самом начале нужно подключить все необходимые библиотеки:
#include <math.h>
#include <strings.h>
#include <stdlib.h>
#include <conio.h>
#include <stdio.h>
#include "usmplay.h"
#include "rtctimer.h"
Далее идут константы, общие массивы и вспомогательных функции:
#define X_SIZE 320
#define Y_SIZE 200
#define DIST 300
typedef struct {
float x, y, z, d;
} vertex;
typedef struct { float x, y; } vertex2d;
short sintab[65536];
float sintabf[65536], costabf[65536];
unsigned char *screen = 0xA0000;
#define pi 3.141592653589793
#define ee 1.0E-6
#define bb 2 * 1.0E+4
#define bb2 1.0E+6
#define sat(a, l) ((a > l) ? l : a)
#define sqr(a) ((a)*(a))
#define sgn(a) (((a) > 0) ? 1 : -1)
USM *module;
int notimer = 0, noretrace = 0, mode13h = 0, manual = 1, lowres = 0, fakelowres = 0
По процедурам в #define стоит пройтись отдельно. sat(a, l) производит проверку на выход целого положительного числа a на выход из диапазона [0, l], в случае выхода значение ограничивается l. Именно эту продедуру я использую для насыщения палитры\картинки в некоторых частях (в том числе при наложении спрайтов на экран путем сложения, чтобы не переполнить значения)
sqr вычисляет квадрат числа, а sgn — его знак (понадобится потом)
Константы ee и bb — соответственно достаточно малое и достаточно большое число, используются для предотвращения ошибки деления на ноль в некоторых случаях.
Ниже — инициализация таблички синусов:
void initsintab() {
int i, j;
float r;
for (i = 0; i < 65536; i++) {
r = (sin(2 * pi * i / 65536));
sintab[i] = 32767 * r;
sintabf[i] = r;
r = (cos(2 * pi * i / 65536));
costabf[i] = r;
}
}
Далее идут различные аппаратные хаки, в частности, связанные с выводом картинки. В этой части без описания регистров VGA-адаптера не обойтись — вот оно
Ах, да:
void setvidmode(char mode) {
_asm {
mov ah, 0
mov al, mode
int 10h
}
}
320x200 60Гц, или как ускорить дему без лишних хлопот
Вся демка работает в режиме 320x200 256 цветов (стандартный режим VGA 13h), но… с небольшим хаком:void set60hz() {
// again thanks to type one for info ;)
///*
outp (0x3D4, 0x11); outp(0x3D5, (inp(0x3D5) & 0x7F)); // unlock registers
outp (0x3C2, 0xE3); // misc. output
outpw(0x3D4, 0x0B06); // vertical total
outpw(0x3D4, 0x3E07); // overflow
outpw(0x3D4, 0xC310); // vertical start retrace
outpw(0x3D4, 0x8C11); // vertical end retrace
outpw(0x3D4, 0x8F12); // vertical display enable end
outpw(0x3D4, 0x9015); // vertical blank start
outpw(0x3D4, 0x0B16); // vertical blank end
//*/
}
Данная процедура перепрограммирует регистры VGA-адаптера для снижения частоты развертки до 60 Гц, чтобы слегка ускорить дему на слабых машинах. Кроме того, данный режим еще и исправляет некорректное соотношение сторон в стандартном режиме 13h (пикселы вытянуты по вертикали).
Идея данного хака встретилась в текстовике от Type One \ Pulpe ^ TFL-TDV, в котором описана туева хуча различных Super- и не только VGA трюков (в частности, способы
На реальной машине картинка выглядит вот так:
Вообще, этот режим должен быть совместим по развертке со стандартным режимом 640x480 60Гц и работать на любой видеокарте и мониторе, но на всякий случай его можно отключить ключом «70hz»
LowRes-режим
Этот режим был запилен в финальной версии для слабых машин (медленнее P133), чтобы избежать неприятных тормозов и даже зависаний. Суть его проста — некоторые эффекты при его активации рендерятся в режиме 160x200, растянутом до 320x200, что позволяет тратить меньше времени на отрисовку.И вот тут-то и начинаются грабли. Вначале режим был сделан из вышеописанного текстовика таким образом:
// method one - set mode 0xD and turn 256 color mode on hand
// 10% works on matrox, ati and S3 (serious palette corruption)
// 1% works on trident (even more serious corruption)
// i guess that it should work on tseng, but dunno
setvidmode(0xD);
outp (0x3D4, 9); outp(0x3D5, ((inp(0x3D5) & 0x60) | 0x3));
outpw(0x3D4, 0x2813);
// unlock S3 extensions
outpw(0x3C4, 0x0608);
outpw(0x3D4, 0x4838);
// and test for presence of S3 card
outp (0x3D4, 0x30);
// if S3 detected - use S3 method of VCLK/2
if (inp(0x3D5) > 0x80) {
outp(0x3C4, 1); outp(0x3C5, (inp(0x3C5) & 0xF7));
outp(0x3C4, 0x15); outp(0x3C5, (inp(0x3C5) | 0x10));
}
// lock S3 extensions
outpw(0x3C4, 0x0008);
outpw(0x3D4, 0x0038);
outpw(0x3C4, 0x0E04);
outpw(0x3D4, 0x4014);
outpw(0x3D4, 0xA317);
outp (0x3CE, 5); outp(0x3CF, (inp(0x3CF) | 0x40));
inp (0x3DA); outp(0x3C0, 0x30); outp(0x3C0, 0xC1);
inp (0x3DA); outp(0x3C0, 0x34); outp(0x3C0, 0x00);
Суть его ясна из комментария — включаем режим 0Dh (320x200 16 цветов) и вручную переключаем видеокарту в 256-цветный режим. Где-то тут затесался хак для видеокарт S3 — он использует вместо VGA-шного делителя пикселклока на два полудокументированный в даташитах S3-шный, и он даже работает (вполне возможно, что я впервые заставил работать режим 160x200 256 цветов на S3-шках :D, но не буду голословным)
Однако запуск на Matrox и S3 привел вот к такому результату:
Туннель превратился в месиво :)
На других карточках эффект был еще хуже. На NVidia монитор выдал «power saving mode» и отрубился, на ATi Rage 128 система просто зависла. На остальных картах режим просто не включился, выводя черный экран либо просто зависая.
Тогда я решил попробовать второй способ:
// method two - set mode 13h and then load horiz.params from mode Dh
// 100% works on matrox, 70% on S3 (palette corruption)
// does not work on nvidia and crashes on ati (why?)
//setvidmode(0x13);
outp (0x3D4, 0x11); outp(0x3D5, (inp(0x3D5) & 0x7F));
outpw(0x3C4, 0x0901); // FUCK YOU S3! :)
// but...they strikes back!
// unlock S3 extensions
outpw(0x3C4, 0x0608);
outpw(0x3D4, 0x4838);
// and test for presence of S3 card
outp (0x3D4, 0x30);
// if S3 detected - use S3 method of VCLK/2
if (inp(0x3D5) > 0x80) {
outp(0x3C4, 1); outp(0x3C5, (inp(0x3C5) & 0xF7));
outp(0x3C4, 0x15); outp(0x3C5, (inp(0x3C5) | 0x10));
}
// lock S3 extensions
outpw(0x3C4, 0x0008);
outpw(0x3D4, 0x0038);
outpw(0x3D4, 0x2D00);
outpw(0x3D4, 0x2701);
outpw(0x3D4, 0x2802);
outpw(0x3D4, 0x9003);
outpw(0x3D4, 0x2B04);
outpw(0x3D4, 0x8F05);
outpw(0x3D4, 0x1413);
Результаты были уже лучше — на Матроксе и S3 картинка стала выглядеть правильно. Правда, остались кое-какие глюки с S3 (тормозит RAMDAC, из-за чего часть палитры терялась, но пофиксил). На остальных картах режим опять не заработал.
Тогда я решил сделать еще и его эмуляцию — пикселы просто дублировались по горизонтали. Для выбора режима сделал небольшую менюшку:
if (lowres == 1) {
puts("select 160x200 mode: ");
puts("1 - hardware (Matrox\\S3\\Tseng?)");
puts("2 - fake (other cards and S3 also if first one is buggy)");
do {ch = getch();} while (!strchr ("12\x1B", ch));
fakelowres = ((ch == 0x32) ? 1 : 0);
if (ch == 27) {KillAll();}
}
В общем, в lowres-режиме дема вполне смотрибельна и на Pentium 90 (не было возможности проверить на 486, но если вдруг заработает, пишите :)
Timeline, или просто «план»
В процессе работы над демой получился такой план:
- вначале идет небольшая пауза до 32-й строки нулевого паттерна (для синхронизации)
- первый туннель (паттерны с 0 по 4)
- второй туннель (4 — 8)
- первая 3D-фиговина (9 — 0xC)
- четыре twirl'а подряд (0xD — 0x14)
- гритсы с 3D-бубликом (0x15 — 0x1С)
- free-directional туннель (0x1D — 0x25)
- free-directional плоскости (0x16 — 0x2D)
- конец! :)
Сами части просто инклудились отдельными исходниками в основной файл:
#include "parts\\tunnel.c"
#include "parts\\tunnel2.c"
#include "parts\\3dstuff.c"
#include "parts\\twirl.c"
#include "parts\\twirl2.c"
#include "parts\\twirl3.c"
#include "parts\\twirl4.c"
#include "parts\\3dstuff2.c"
#include "parts\\fdtunnel.c"
#include "parts\\fdplanes.c"
Каждая часть торчала наружу двумя процедурами — <название>_init() и <название>_main(). Первая процедура вызывалась во время инициализации, вторая уже во время работы демы.
В общем инициализация выглядела вот так:
for (p = 1; p < argc; p++) {
if (strcmp(strupr(argv[p]), "NOTIMER") == 0) notimer = 1;
if (strcmp(strupr(argv[p]), "NORETRACE") == 0) noretrace = 1;
if (strcmp(strupr(argv[p]), "70HZ") == 0) mode13h = 1;
if (strcmp(strupr(argv[p]), "LOWRES") == 0) lowres = 1;
if (strcmp(strupr(argv[p]), "SETUP") == 0) manual = 0;
}
HardwareInit(_psp);
rtc_initTimer();
if (manual == 0) {USS_Setup();} else {puts("run \"blash setup\" for manual sound setup"); USS_AutoSetup();}
if (Error_Number!=0) { Display_Error(Error_Number); exit(0); }
if (notimer == 1) {puts("timer sync disabled\0");}
if (noretrace == 1) {puts("vertical retrace sync disabled\0");}
if (mode13h == 1) {puts("320x200 70Hz mode used\0");}
cputs("init .");
module = XM_Load(LM_File, 0x020202020, "musik.xm");
if (Error_Number!=0) { puts(".\0"); Display_Error(Error_Number); exit(0); }
cputs(".");
initsintab();
cputs(".");
t1_init();
cputs(".");
t2_init();
cputs(".");
fx1_init();
cputs(".");
w1_init();
cputs(".");
w2_init();
cputs(".");
w3_init();
cputs(".");
w4_init();
cputs(".");
fx2_init();
cputs(".");
fd1_init();
cputs(".");
fd2_init();
puts(".");
puts("get down! ;)\0");
setvidmode(0x13);
if ((lowres == 1) && (fakelowres == 0)) {set160x200();}
if (mode13h == 0) {set60hz();}
USS_SetAmpli(150); // to prevent clipping on sbcovoxpcspeaker
USMP_StartPlay(module);
USMP_SetOrder(0);
if (Error_Number!=0) { setvidmode(3); Display_Error(Error_Number); exit(0); }
… и основной цикл:
while (Row < 32) {}
inp (0x3DA); outp(0x3C0, 0x31); outp(0x3C0, 0xFF);
t1_main();
inp (0x3DA); outp(0x3C0, 0x31); outp(0x3C0, 0);
t2_main();
fx1_main();
if ((lowres == 1) && (fakelowres == 0)) {setvidmode(0x13); if (mode13h == 0) {set60hz();}}
w1_main();
w2_main();
w3_main();
w4_main();
fx2_main();
if ((lowres == 1) && (fakelowres == 0)) {set160x200(); if (mode13h == 0) {set60hz();}}
fd1_main();
fd2_main();
normal = 1;
KillAll();
puts("blash - final - b-state - 2015\0");
Напоследок, нам нужно все за собой подчистить — для этого используется процедура KillAll():
void KillAll() {
USMP_StopPlay();
USMP_FreeModule(module);
setvidmode(3); rtc_freeTimer();
if (Error_Number!=0) { Display_Error(Error_Number); exit(0); }
if (normal == 0) {puts("hey, why you pressed that escape key?!\0"); exit(0);}
}
А разбирать код частей мы начнем с twirl'ов и туннелей уже в следующей статье.
P.S. кстати, в финальной версии демы есть скрытая часть — готовьте отладчики и HEX-редакторы! (осторожно, не рекомендуется к просмотру господину lvd , извини :)
20 комментариев
Да и blash писалась по принципу «делаем прод и по пути делаем побочный стафф, который потом обязательно пригодится» :)
Супер!
У меня больше половины времени — это именно такие дела, бесконечные конверторы, генераторы графики, генераторы кода, компрессоры самопальные, и т.д. и т.п.
Include «FONT\crunch.az8»
Что видишь ты? Для тебя это текст «Hello Speccy», но в реальности кодировка символов стала максимально приближенная к нарисованному шрифту, где автор шрифта не рисовал знака вопроса, и поэтому он был автоматически выкинут. Всё это оптимизировалось в ветке предкомпиляции «FONT\crunch.az8», где анализировалась графика из картинки BMP, которая там же превратилась в бинарные данные, на них создалась табличка(виртуальная для компилятора) неиспользуемых символов, оптимизировались повторения в графике русской буквы «Р» и латинской «P», всё это превратилось в максимально ужатый бинарный кусок который лёг в память. И после исполнения «crunch.az8» любая информация заключённая в кавычки кодируется по таблице. И LD A,«P» в итоге в аккумулятор запишет некий символ после перекодировки. Это я привёл тебе простой пример, самый элементарный. Я молчу о том, что ты можешь откомпилировать свой код и тут же его в эмуляторе подсчитать по тактам, или часть своего кода подсчитать. Или сделать высвечивание пакованных данных, например в OSCOSS есть лица которые высвечиваются. Все они состоят из каких-то простых залитых полигонов, которые у меня высвечиваются какой-то процедурой, которая на каждую часть высвечивания лица стратит некое количество тактов. И тут мне приходит идея, хочу к лицам сделать снизу зеркальное отображение, и продлить границу разделение на бордюре. Вот всё это пропускается через одну команду компилятора, которая при компиляции эффекта подсчитывает сколько тактов занимает высвечивание каждой части лица, делает мне таблицу с конкретными числами в тактах, под каждую картинку-лицо, и я только из фиксированного числа отнимаю сумму тактов из таблички и жду оставшееся время до места продолжения линии на экране и бордюре. Всё, это конец моего кодинга в данном эффекте, график меняет себе пикселы в картинке, музыкант меняет нотки, а у меня жёсткий эффект сделанный из набора простых команд моего асма. И я не знаю как это сделать на sjasmplus? Я честно, клянусь, много раз пытался использовать разные компиляторы. но как правило компиляторы почему-то заботятся о том, что бы LDAB превращалось в LD A,B, но мне это не надо, это не проблема исправить опечатку, а вот когда я не могу дать волю своей фантазии, это для меня очень критично. Так что, Лёша, да, мне пришлось бы заново писать «asam», без него у меня весь пар уходит на гудок. Для меня возможности моего асма это как в 90-ые года писать на TASM 4.0, и потом кропотливо считать-считать-считать-считать-считать-считать…
Вообще, вот такие клёвые детали показывают одну вещь. Я думаю, было бы очень полезно, не только мне, послушать про твою работу над OSCOSS, послушать о принципах сборки, о подходе к эффектам, о подходе к дизайну кода и дизайну вообще. Не факт что люди сделают так же (я всё ещё не хочу писать свой ассемблер!), но вот такие рассказы о способах решения проблем — они очень полезны. Просто мозги прочищаются, волей-неволей начинаешь думать по-другому.
Альтернатива только запилятор. Если не запилятор, то все вот эти бесконечные конверторы и генераторы данных.
1. График нарисовал в удобном редакторе, или поправил один секретный пиксель.
2. Музыкант написал музыку не задаваясь вопросом сколько это будет занимать.
3. Клацнул на ентер.
4. Смотришь уже конечный запакованный результат, готовый к отправке, с лоадером, пакером, шмакером, кодером и другими супер-пупер вещами.
Главное нет вот этого промежуточного вызова утилиты и настроек. Так, что утилиты то же бывают разные.
Похоже так в итоге и напишу я своего «Масона»! (бьюсь лбом об стену)