
ZX-Spectrum был моим первым компьютером еще в те времена, когда я себя не очень хорошо помню. Однако в памяти остались бесконечно долгие экраны загрузки игр с магнитофона и невероятной радости, когда (и если) эта загрузка состоялась. Чуть позже помню первые
Только через несколько лет на кружке по информатике я раскрыл для себя тайный смысл некоторых из этих странных последовательностей символов, которые заставляли компьютер делать те или иные действия, однако к тому времени спектрум уже канул в безвестность уступив место 286-му. С тех пор прошло много лет, но желание вернуться к старичку спектруму и написать для него что-то осмысленное присутствует до сих пор. Всех тех, кому это интересно прошу под кат.
Задача и инструментарий
Первое, с чем следует определиться, — это что писать и на чем. На своей предыдущей работе я занимался созданием графического движка визуализации научных данных, и эта работа приносила мне огромное удовольствие. Однако, по независящим от меня причинам, я вынужден был поменять работу и ушел в мир кровавого энтерпрайза (о чем, кстати, тоже нисколько не жалею). Помня то, насколько мне нравилось работать с графикой, я решил в качестве своей первой программы попробовать написать некоторую сильно упрощенную версию программного 3D графического движка для ZX-Spectrum. Я понимаю, что спектрум далеко не самая быстрая платформа и что он мало приспособлен для 3D графики (хотя есть пример прекрасной игры Elite, которая выглядит как полноценная 3D игра), однако хотелось бы начать эксперименты с интересной для меня тематики.

Касаемо второго вопроса, при просмотре разных роликов на YouTube про спектрум (особенно понравилась рубрика «Дневники разработки» на канале 8-Bit Tea Party) у меня сложилось впечатление, что правильно будет писать на ассемблере, но это для меня практически неизведанная территория, и поскольку это хобби-проект, тратить слишком много времени на изучение еще и нового языка я не хотел.
К счастью, оказалось, что есть прекрасный sdk для спектрума и ему подобных машин — z88dk. Он содержит в себе компилятор языка C и реализацию стандартной библиотеки языка С для zx-spectrum. Его и решено было использовать для разработки.
Начало разработки
Первое, что нужно понять перед разработкой, — это как компилировать программу и как ее запустить на эмуляторе.
Небольшое лирическое отступление: к сожалению, на данный момент у меня нет реальной машины. Как оказалось, спектрум моего детства был безжалостно определен на свалку уже много лет назад. Все что я смог — это по воспоминаниям о внешнем виде понять, что это был российский клон «Символ»

На данный момент в продаже имеется множество современных клонов, (к примеру, ZX-Spectrum Next) использование которых не предполагает дружбу с паяльником, однако цены на эти компьютеры откровенно кусаются (хоть и выглядит тот же Next просто прекрасно). Возможно, в будущем я и куплю себе Next, но на данный момент было решено тестировать код в эмуляторе.
Возвращаясь к компилятору, кратко пройдемся по его опциям, необходимым для сборки программы.
Во-первых, для компиляции используется команда zcc формата
zcc +[target] {options} {files}
В качестве таргета выбираем +zx. Как я понял, этот кросс-компилятор умеет собирать не только под ОС спектрума, но и, например, для ОС CP/M. Главное, чтобы процессор был семейства z80.
Далее в качестве опций необходимо перечислить список библиотек:
-lmzx – используем библиотеку математики для тригонометрических функций;
-lndos – библиотека для возможности отладочного вывода на экран.
Говорим, что хотим создать бинарное приложение, пригодное для запуска на эмуляторе:
-create-app -o zx3dEngine
И перечисляем список файлов. Итоговая команда получается следующая:
zcc +zx -lndos -lmzx -v -create-app -o zx3dEngine main.c engine.c point.c vector.c model3d.c linear_alg.c
В результате компиляции получаем файл zx3dEngine.tap, который прекрасно можно использовать в эмуляторе.
Разработка
Проблем с написанием и запуском Hello World не возникло никаких. Интересные моменты начались с попыток создать объект в динамической памяти.
Оказывается, что перед тем, как работать с кучей, ее нужно проинициализировать. Для этого нужно завести переменную long heap в статической памяти, вызвать mallinit() и, при помощи функции sbrk(), указать, с какого адреса и какой объем памяти выделяется под кучу данной программы.
#include <malloc.h>
long heap;
int main() {
mallinit();
sbrk(30000, 6000);
int * a = (int *) malloc(20 * sizeof(int));
free(a);
}
С рисованием сначала не возникло никаких проблем. Достаточно подключить <graphics.h>, и появляется возможность рисовать графические примитивы.
Однако, как только начинаешь перерисовывать изображение в цикле, возникает мерцание. Понятно, что нужно использовать двойную буфферизацию, но как это сделать изначально было не очевидно.
К счастью, копаясь в примерах к z88dk, я нашел нужные функции, но работают они только в режиме графики с низким разрешением 32х48 (изначально графический режим 256х192). Решил поддержать оба режима и добавил опцию препроцессора LOW_RESOLUTION_MODE.
#define bufferedgfx 1
#include <zxlowgfx.h>
#ifdef ALTLOWGFX
#define ddraw(x,y,x1,y1,c) cdraw(2*(x),y,2*(x1),y1,c);
#else
#define ddraw(x,y,x1,y1,c) cdraw(x,2*(y),x1,2*(y1),c);
#endif
int main() {
cclg(0);
while (1) {
cclgbuffer(0);
ddraw(10, 10, 20, 20, 6);
ccopybuffer();
}
}
Далее пришлось понять, что операции с плавающий точной ужасно медленные. Если есть возможность их избежать, лучше это сделать. И если допущение, что все координаты целочисленные, приходит легко, то с матрицами поворота начинаются проблемы.
К примеру, классическая матрица поворота вокруг оси Y имеет вид:

Как видно, в матрице присутствуют функции косинуса и синуса. Результат выполнения этих функций — вещественные числа. Соответственно, при применении матрицы поворота к точке, придется еще и умножать на вещественное число, а потом округлять полученный результат.
Я не придумал ничего проще, чем умножить полученное значение тригонометрической функции на 1000, округлить и пометить, что при применении матрицы к точке для этого значения нужно не забыть целочисленно разделить полученный результат на 1000. Таким образом удалось уменьшить количество операций с вещественными числами. Обратной стороной этого является потеря точности и искажения, возникающие при применении нескольких матриц к объекту. Уверен, что есть способ оптимизировать гораздо лучше, но я пока не придумал, как это сделать.
Для реализации перспективной проекции я использовал формулу, подсмотренную в книге М. Мозгового «Занимательное программирование», которую очень любил, когда начинал программировать на Delphi.
X1 = HALF_SCREEN_WIDTH + SCREEN_DEPTH * x / z
Y1 = HALF_SCREEN_HEIGHT + SCREEN_DEPTH * y / z
В данной формуле SCREEN_DEPTH — некоторая константа, определяющая, насколько сильно объекты будут уменьшаться при увеличении координаты z.
Полные исходные коды программы можно посмотреть здесь
Итоги
В результате у меня получилось отобразить проволочные графические примитивы в перспективной проекции и повращать их вокруг осей X и Y.

Скорость работы при классической частоте в 3.5Mhz составляет около одного кадра в секунду. Эмулятор позволяет увеличивать частоту процессора, и при 14Mhz все работает довольно шустро.
В режиме низкого разрешения:
Подводя итоги, хотелось бы сказать, что я получил огромное удовольствие, программируя под ZX-Spectrum, и мне хотелось бы продолжить разработку под эту платформу. У меня есть несколько интересных идей, о которых возможно я напишу на хабр.
В заключение хочется поблагодарить создателей ZX-Spectrum-а, z88dk и эмуляторов спектрума, а также мою супругу за редактуру данной статьи. Всем спасибо за внимание!