Речь пойдёт о демке "8088 MPH", победившей в соревновании Revision 2015's Oldskool Demo. Мы, вместе с Trixter, reenigne и Scali сделали это. И я получил возможность не только работать с группой волшебников программирования, но и побить мировые рекорды при изготовлении демок для старого доброго IBM PC, мамы и папы современной платформы x86.
Если у вас под рукой по какой-то причине не оказалось IBM PC XT x86 с CGA-адаптером, вы можете посмотреть демку на ютубе:
Технические подробности содержатся в посте reenigne, а я решил рассказать эту историю в более наглядном виде.
Хитрость с поддержкой большой палитры была придумана мною некоторое время назад, когда я смотрел на код reenigne, который выполнял композитную эмуляцию CGI для DOSBox. Приведя к приличному виду концепцию этого хака, я рассказал об этом reenigne, чтобы тот просмотрел моё решение и потестировал его на настоящем железе. В результате у нас появился рецепт:
— взять два знакомых, хоть и недокументированных, хака. Смешать вместе для получения нужного эффекта
— добавить ещё один хак
— проверить, исправить, повторить
Начало тьюториала написано специально для тех, кто не сильно знаком с CGA.
Старый трюк №1: 16-цветная графика по RGBI
Краткий курс основ CGA: первый графический стандарт для PC. Буфер памяти на 16 Кб, работает при помощи MC6845 CRTC. Видеовывод: NTSC через штекер RCA, или коннектор DE9, выводящий сигнал RGBI (красный, зелёный, голубой, интенсивность). Последний вариант как раз и представляют люди, когда слышат о CGA – цифровой TTL сигнал, каждый компонент может быть включён или выключен, итого 16 цветов.
Стандартные режимы, поддерживаемые в BIOS – высокое разрешение (640x200) в 2 цветах и среднее (320x200) в 4 цветах. Не развернуться: в высоком разрешении только цвет заливки можно менять, а цвет фона – всегда чёрный. В среднем можно менять цвет фона, а три других предопределены фиксированными палитрами.
IBM вскользь упомянула о ещё одном режиме, в 16 цветах и низком разрешении, который «не поддерживается в ROM» – и не дала никакой информации о том, как с ним работать. Но этот ларчик открывался ломом.
Режим низкого разрешения
Это не графический режим, а слегка изменённый текстовый режим из 80 столбцов. Вы поправляете регистры CRTC и получаете 100 строк вместо 25. Получается, что символ занимает прямоугольник 8х2 пикселя – четверть от обычного 8х8. Заполнение экрана волшебным ASCII-символом 0xDE разделяет каждый символ на левые и правые «пиксели», которые соответствуют фону и заливке. Этим двум цветам можно в индивидуальном порядке назначать любое из 16 значений CGA-режима, как и в любом текстовом режиме CGA – главное, не забыть выключить мерцание.
На картинке – клетка 8х8 для символа 0xDE (желтый – заливка, чёрный – фон). В режиме низкого разрешения на экран выводятся только два верхних ряда пикселей (растровые строки), и получается два прямоугольника из 4х2 пикселей рядом.
Вот вам и режим: 160x100 @ 16c. Он использовался по крайней мере с 1983 года, но популярности не снискал. Чтобы он выглядел опрятно, необходимо тратить драгоценные ресурсы CPU, иначе не избежать «снега» на экране.
Метод Macrocom
Вы спросите – раз это текстовый режим, почему бы не использовать весь набор ASCII-символов? Действительно. В середине 80-х эта техника была опробована смельчаками из Macrocom, скомбинировавшими трюк из 100 строчек с ASCII-артом, и получив, что называется, «аццкий ANSI».
На картинке: часть текстового экрана 80х100 символов, использующего случайные ASCII-символы для эмуляции графики высокого разрешения. Выводятся только два верхних ряда растровых строк в каждой строке символов.
Посередине – как бы выглядел экран, если бы вы видели символы целиком.
Справа – шрифт из ROM, подсвечено две верхних растровых строчки каждого символа.
Хитрым образом используя набор символов, можно заставить зрителя думать, что он видит режим 640х200, хотя каждый символ 8х2 и может содержать лишь 2 цвета. И рисовать в таком режиме с нуля – занятие для особых гурманов…
Этот трюк к нашей демке относится лишь опосредованно: мы рассчитывали на композитные дисплеи. Даже если бы композитный вывод CGA не был таким глючным в режими 80 столбцов, его бы всё равно нельзя было вывести на NTSC в такой детализации.
Старый трюк №2: 16 цветов через композитный выход
Цифровые RGB-мониторы в эпоху CGA встречались редко, но CGA также обеспечивал композитный вывод с картинкой, почти совместимой с NTSC. Тут мы пожертвуем разрешением, но зато отыграемся с цветами.
Прямые цвета
На композитном выходе палитра из 16 цветов представлена серией цветовых сигналов, у которых оттенок определяется фазой относительно референсного сигнала (the NTSC color burst). Частота NTSC по цвету (3.579545 MHz) как раз соответствует 160 цветовым циклам на активную строчку растра CGA.
Цвета напрямую создаются железом в качестве цветовых сигналов, поэтому мы зовём их «прямыми цветами». У IBM было две версии CGA, производивших композитное видео по-разному: новые карты содержали дополнительные контуры, которые позволяли привести палитру в наибольшее соответствие с RGBI. Для демки мы использовали старые карточки, поскольку мы тестировали всё именно на них.
Но 16 цветов нам было бы недостаточно, не так ли? Они ещё и уродские к тому же. Но есть способ сделать лучше.
Паразитные цвета
Из-за ограничения пропускной способности NTSC не разделяет полностью цветность и яркость. Любая деталь высокого разрешения (у которой частота больше, чем частота NTSC), смазывается при декодировании. У символов по краям появляются паразитные цвета.
Помните, как мы получили 160 цветовых циклов на активную строку растра CGA? Стандарт CGA даёт или 320 или 640 активных пикселей на строку. Мы можем включать и выключать пиксели в 2 или 4 раза быстрее частоты цветопередачи. Поскольку эти высокочастотные детали не разделяются полностью с информацией о цветах, получается, что оттенок пикселя или переход между пикселями зависит от его позиции внутри периода цветового цикла.
Иногда цветовой цикл NTSC представляют в виде круга – один полный период равен повороту на 360°, и у нас есть 160 полных поворотов на строку растра.
Допустим, у нас режим 640x200, где 4 пикселя помещаются в один цифровой цикл – перемещая один пиксель влево или вправо, мы идём по цветовому колесу на 90°, и, соответственно, меняем оттенок на 90°. В режиме 320х200 мы переходим по колесу на 180°. То есть, манипулировать деталями в режиме высокого разрешения, означает создавать цвета, которые являются паразитными явлениями несовершенной передачи методом NTSC.
На картинке: результат невозможности полного декодирования цвета в виде цветных мазков. Текст в режиме 80 колонок (слева) почти не читаем, в отличие от текста в 40 колонок (справа).
Для уменьшения паразитных эффектов на принимающей стороне часто используются разные фильтры. Но т.к. это технология, а не магия, полное разделение деталей и цвета сделать нельзя, а в качестве побочных эффектов получаются другие артефакты (типа эхо).
Сплошные паразитные цвета
Но все эти паразитные цвета – это всего лишь побочные эффекты неидеальной схемы кодирования. Но, как и любой недостаток, его можно превратить в преимущество. Если посмотреть на эти эффекты, становится понятно: всякий периодический композитный сигнал с той же частотой, что и частота цветопередачи (160 на строку), будет декодирован в виде сплошного цвета.
Наши 16 прямых цветов относятся к этому типу периодических композитных сигналов. Но подождите – если мы поиграем с пикселями в высоком разрешении, мы сможем получить свои собственные периодические формы волн! Подойдёт любой шаблон из точек, пока он повторяется с нужной частотой. Так мы получим сплошные цвета, не входящие в палитру прямых.
Классический способ – перейти в режим BIOS №6, 640x200 с 2 цветами, белый на чёрном, и взвести бит цветовой синхронизации (color-burst). В этом разрешении 4 пикселя влезут в период цветового обновления, и для 1 бита на пиксель получится 16 шаблонов – т.е., 16 цветов.
Практически так Стив Возняк сделал цветопередачу на Apple ][. Вообще, на старой CGA-карте 16 цветов точно совпадают с 16 цветами режима низкого разрешения у Apple. Короче говоря, сами пиксели – белые, передача цвета осуществляется лишь их взаимным расположением.
Но это ещё не всё! CGA, несмотря ни на что, имеет перед Apple преимущество. Наши пиксели, задающие шаблоны, не обязаны быть белыми. В режиме 620х200 мы можем задать заливке любой из 16 цветов (фон всегда чёрный). Назначая тем же шаблонам пикселей отличный от белого цвет, мы получаем для него палитру с новым набором цветов. Конечно, использовать за раз можно только один набор из 16 цветов, но по крайней мере, можно выбирать, какой именно.
А ещё у нас есть режим 320x200 с палитрой из 4 прямых цветов. И только один из них, №0 (фоновый), можно менять свободно. Для остальных можно менять интенсивность, но мы можем получить только зелёный/красный/жёлтый или голубой/пурпурный/белый. Чтобы получить недокументированную палитру голубой/красный/белый, надо отключить бит цифровой синхронизации, из-за чего композитная картинка будет чёрно-белой.
Поскольку в этом режиме пиксели в два раза жирнее, в цикл обновления цвета их можно втиснуть только по два – но для 2 битов на пиксель общее количество паразитных цветов по-прежнему будет 16. Возможные комбинации палитр и выбираемый цвет фона дают нам несколько наборов из 16-цветовых палитр.
Тут кое-что необходимо прояснить. Т.к. у нас есть 160 цветовых циклов на строчку, многие считают, что графические режимы CGA, доступные по композитному сигналу, это режимы 160х200 – но это не совсем так. Эффективное цветовое разрешение, действительно, 160х200, и невозможно получить более мелкое разрешение, используя паразитные цвета. Но, как мы видели, в NTSC решётка пикселей и решётка цветов – не одно и то же. Поэтому горизонтальное разрешение – вопрос открытый, он зависит от семплинга и фильтрации сигнала, и меняется в зависимости от того, какие формы цветового сигнала вы используете.
IBM не документировала эти трюки с артефактами, но концепция была использована даже в самых старых играх – например, Microsoft Decathlon и Flight Simulator от 1982 года. И ограничение в 16 цветов всегда оставалось.
Однако если вы всё ещё читаете текст, вы догадываетесь, что это ограничение можно преодолеть, и даже можете догадаться – как именно.
256 цветов
Наш выбор 16 паразитных цветов зависит от палитры и настроек регистра цвета. Значит, необходимо менять регистры у конкретных строк растра и получать более 16 цветов на экране. Не так ли?
На CGA такое уже делалось, и эту технику можно довести до 256 цветов – но в демке мы этот подход не использовали. Я наткнулся на нашу технику по чистой случайности, ведомый любопытством.
Вспомните, как шаблон из цветов и точек нужной длины (четыре пикселя в 640х200 или два в 320х200) дают сплошной цвет на композитном дисплее. Когда я тестировал эмуляцию композита на DOSBox, я помнил об этом. И в это же время я экспериментировал с графическим хаком «аццкий ANSI».
Вернёмся к одной из частей ROM-шрифта в режиме 80 колонок, у которого подсвечены 2 верхние линии растра:
Присмотритесь, например, к данному символу:
В режиме 80х100 выводятся только две верхних линии растра. Видите эти 25% изображения символа? Две точки цвета заливки и две точки фона, продублированные по горизонтали. Мы находимся в режиме высокого разрешения и отображаются 80 колонок, поэтому на каждый символ приходится два цветовых цикла – которые соответствуют этим двум похожим половинкам. И две верхние линии растра идентичны.
Именно такой повторяющийся шаблон даёт нам сплошной паразитный цвет в NTSC. Эта та же самая форма волн, которая доступна в режиме 320x200. Только сейчас она доступна в текстовом режиме, где мы можем свободно назначать и цвет заливки, и цвет фона, их имеющихся 16 цветов.
То есть, у нас уже имеется 256 вариантов – т.е. возможность выводить более 16 цветов в CGA без моргания, размытия и прочих эффектов.
Возможные комбинации:
512 цветов
Но это ещё не всё! Двигаясь в этом направлении, я начал изучать шрифт из ROM в поисках других полезных шаблонов. Некоторые символы дают нам те же формы волн, что и U – это H, V, Y и ¥. Но только одна с другой подходящей последовательностью бит именно там, где надо — 0x13, или ‼.
Верхние две линии растра U дают битовую маску 11001100 для заливки и фона. '‼' даёт 01100110 – сдвиг вправо, или сдвиг фазы на 90°. Это дополняет U и создаёт хорошую полную палитру, потому, что мы получаем все цвета, которые может предложить форма волны "…1100…": переходя от 'U' к '‼', мы сдвигаем фазу на 90° (0110); 180° и 270° получаются, когда мы меняем местами цвета заливки и фона у 'U' и '‼' соответственно – то же, что для '0011' и '1001'.
И вот мы получили ещё больше – 512 цветов! Конечно, реально их меньше, встречаются дубли и очень мало отличающиеся оттенки. Но вроде бы это предел – других подходящих символов в шрифте нет. В CGA есть альтернативный «тонкий» шрифт 8х8, но во-первых, для этого необходимости ковыряться в карточке, а во-вторых, полезных битовых шаблонов там нет.
С гордостью поделившись своей методикой увеличения количества цветов в 32 раза с reenigne, я не ожидал, что у него родится поистине дьявольский план по удвоению доступного количества цветов…
1024 цвета
А дальше идёт чёрная магия, все лавры которой следует отнести на счёт reenigne
Обратите внимание на два этих символа, у которых есть интересная первая линия растра. К сожалению, вторая линия отличается от первой, и это испортит наш эффект сплошных цветов. Это ASCII коды 0xB0 и 0xB1. Как было бы здорово иметь возможность отрезать вторую, неподходящую линию растра… И такая возможность есть.
Подробности можно прочесть в посте reenigne, но коротко говоря, идея в следующем: начиная каждый новый кадр CRTC с началом новой линии растра и играясь с начальным адресом, возможно расположить наши символы так, чтобы первая линия растра каждого символа была повторена дважды.
И теперь мы можем использовать эти два символа, которые дают нам ещё две палитры из 256 цветов:
Конечно, такая возня даёт большую нагрузку на несчастный проц 4.77MHz 8088, поэтому кроме статичных картинок тут мало что можно сделать. Вариант с 512 цветами и использованием ASCII 0x55 и 0x13 гораздо быстрее – установил, и забыл.
Есть ещё одна проблема с композитными дисплеями – хардварный баг, приводящий к неправильной вертикальной синхронизации и пропускам цветовой снихронизации. Есть способы компенсации – но 100% надёжных для любого монитора нет. Потребовалось много времени на тестирование и калибровку.
Ну и в конце мы получили 1К цветов на 1981 IBM CGA в эффективном разрешении 80х100 «пухлых пикселей». Пухлые они и в памяти, и визуально. Конечно, 80х100 – слишком маленькое поле для рисования. Но ограничения – часть интересной задачи, которая всегда присутствует в демках. Оставьте себе свои мониторы разрешением в 4К – 0.008 мегапикселей должно хватить всем!
На картинке: сверху – результат на откалиброванном NTSC-дисплее. Внизу – вариант с выводом на RGBI. Отрисовано в фотошопе вручную и преобразовано в формат 80х100.