И снова приветствую всех любителей олдскула, демосцены и олдскульной демосцены. Это вторая часть разбора DOS-демки "Remoded" и, соответственно, эффектов, основанных на аппаратных особенностях видеоадаптера VGA. Сегодня мы продолжим разбираться с аппаратным скроллингом, узнаем, что такое "xorfill" и как его правильно готовить, как эмулировать 12 тысяч цветов в 256-цветном режиме, а также познакомимся с другими технологиями прошлого столетия.

В предыдущей части мы разобрались: что такое mode-x и зачем он был нужен, как работает аппаратный скроллинг и сплиттинг, а заодно выяснили: зачем писать демки под DOS в 2026-м году. Если вы все пропустили, уточняю: речь идет о ретро-кодинге под DOS, VGA-совместимую видеокарту (256 цветов, и никаких SVGA/VESA) и процессор уровня 486/66 мГц. Мы разбираем эффекты из демки "Remoded", которую я и @Manwe_SandS выставляли в этом году на демопати Revision'2026.

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

Ну, не хотите как хотите. Тогда врубаем ретровэйв и погнали!

Вертикальный скроллинг (Greetings part)

После части с собакой-зумером в демке идет скроллер с приветствиями демогруппам (древняя традиция). Чем технически интересна эта часть? С одной стороны все банально: аппаратный скроллинг и палитровая анимация. Благодаря этим двум вещам мы не перерисовываем экран целиком, а лишь добавляем несколько пикселей (дорисовывая буквы и одну строку фона) каждый кадр. Плавная прокрутка происходит за счет смещения стартового адреса в регистре видеокарты (с какой позиции видеокарта будет читать свою память отрисовывая экран), а эффект параллакса (разная скорость движения текста и двух "слоев" фона) и анимация шестеренок реализованы палитровой анимацией (подробнее о ней мы говорили в предыдущей статье).

Для наглядности, как обычно, я сделал пару гифок:

Что мы увидим без real-time модификации палитры (default VGA palette)
Что мы увидим без real-time модификации палитры (default VGA palette)
Что мы видим с модифицируемой палитрой
Что мы видим с модифицируемой палитрой

Видите, как славно получается? Пока мы плавно, волной, закрашиваем экран, мы не дергаем Start Address, но создаем эффект скроллинга палитрой. Как только начинается аппаратный скроллинг видеостраницы, мы начинаем сдвигать палитру в обратную сторону, чтобы компенсировать скролл изображения вверх и сделать движение фона чуть медленнее, чем движется текст. На гифках это выглядит грубо, ведь ради экономии они имеют 10 FPS. В реальности сцена работает с номинальной скоростью рефреша (60 или 70 FPS, в зависимости от выбранного видеорежима), и скроллинг получается идеально плавным (особенно на реальном железе). И цимес в том, что такой эффект будет работать безупречно даже на 8086-м процессоре, ведь нагрузка на CPU и объем данных гоняемых по шине довольно скромные. Да, при этом мы расходуем 128 цветов на анимацию фона и еще 48 цветов на шестеренки внутри букв. Такова цена трюка.

А сейчас сказка заканчивается и начинается суровая реальность. Следующий кусок сказа о скроллинге будет посвящен проблемам, засадам и подставам от производителей железа, и о том, как демосценеры героически их преодолевали (или героически забивали на них). Если вам не слишком интересны эти подробности - смело переходите к следующей главе.

Вдумчивый читатель может спросить: ну хорошо, мы сдвигаем стартовый адрес на длину строки каждый кадр, благодаря чему получаем вертикальный скроллинг (не перерисовывая кадр), но что случится, когда выводимый на экран "фрэйм" упрется нижней границей в конец видеопамяти? А исторически стандарт VGA-видеоадаптера ограничен 256 килобайтами. И что собственно будет дальше? При благоприятном стечении обстоятельств произойдет "заворачивание" (wrapping): видеокарта будет читать свою память с начала. Для наглядности я попытался изобразить этот процесс:

Ради упрощения представим, что ширина строки является степенью двойки
Ради упрощения представим, что ширина строки является степенью двойки

Так делает большинство VGA-совместимых видеокарт. И на этом можно было бы и успокоиться. Но дело в том, что IBM не гарантирует "враппинг" в спецификациях VGA. Так что некоторые производители пошли по своему особому пути. Время шло, стали появляться видеокарты с 512 кб видеопамяти и более (для SVGA-режимов). Одними из первых отличились инженеры Tseng Labs. Их видеокарты были безусловно инновационны и полны разных фич, но местами эти фичи ломали обратную совместимость. В частности видеокарты Tseng не делают "враппинга" на 256кб, а продолжают читать видеопамять дальше, которая замаплена на сегмент 0xB000. Это чудесно, но ведь другие видеокарты в VGA-режиме так не делали (по крайней мере без спросу). При этом никакого переключателя инноваторы из Tseng Labs не предусмотрели. Что в итоге? Если мы по старой привычке надеялись на "враппинг" и скроллили кадр как обычно, то получали примерно такую херабору:

Вместо черной области, как вариант, будет просто мусор
Вместо черной области, как вариант, будет просто мусор

Находчивый читатель скажет: ну и делов-то - детектим Tseng и подстраиваемся под него! Но проблема еще в том, что так себя ведут не только видеокарты Tseng, но и некоторые модели других вендоров. Например S3 Virge (86С325 и 86С988). У некоторых "враппинг" слетает после включения SVGA-режима, и до перезагрузки не возвращается. Да, спецификации VGA, как я уже говорил, не обязывают его поддерживать, это лишь правило хорошего тона. Но в итоге это вызывает проблемы не только в демках, но и в некоторых играх, которые эксплуатируют аппаратный скроллинг. Джону Кармаку даже пришлось сделать специальную переключалку "SVGA compatibility" в Commander Keen начиная с 4-й версии.
Детектить - есть ли wrapping, или нет - теоретически можно, но делал ли это кто-то - мне не известно.

Какой же выход из ситуации? Если установленное разрешение позволяет уместить в видеопамяти хотя бы две экранные страницы, мы можем делать так:
- Впереди (внизу) фрэйма рисуем строку, которая должна появиться в следующем кадре,
- После сдвига видимой области рисуем (или копируем) такую же строку в области видеопамяти выше фрейма,
- и так повторяем, пока не происходит сдвиг на высоту одного экрана.
В этот момент у нас получается две идентичные по содержанию видео-страницы, так что мы просто сбрасываем Start Address в ноль, и начинаем все заново. На экране получается зацикленный скроллинг без необходимости во враппинге и с незначительными накладными расходами:

Представьте, будто в верхней и нижней части фрейма у нас принтеры :)
Представьте, будто в верхней и нижней части фрейма у нас принтеры :)

Да, мы могли бы просто перерисовывать всю верхнюю (невидимую) часть за раз (или скопировать с нижней), перед тем как прыгнуть на нее. Но это сильно затратнее, чем строить ее построчно. А если мы не успеваем сделать это до VBLANK'a, то это вызовет запинания каждые 60/70 кадров, а это отвратительно!

Ну и что же? Казалось бы, проблема решена. Но это пока мы работаем с разрешениями, которые позволяют уместить два полных экрана в видеопамяти. Да, даже для 320х400 все отлично укладывается. Но мы же демосценеры, или кто? Мы не ищем легких путей! В демке "Remoded" предлагается на выбор в том числе и разрешения 320х480, 360х400 и 360х480. И если представить как при этом выглядит видеопамять, то получится что-то вроде:

Пиксели в таких разрешениях не квадратные, а сильно сплюснутые
Пиксели в таких разрешениях не квадратные, а сильно сплюснутые

Как видите, у нас не хватает места, чтобы выстроить копию экрана дабы незаметно на нее переключиться. Да, если мы полагаемся на враппинг, то проблем никаких - скроллим дорисовывая экран снизу, все завернется само собой, будто мы рисуем на закольцованной пленке.

Но что мы можем сделать, если враппинга нет? Проскролить столько, сколько можно, не залазя нижней границей за 256Кб, и формируя сверху картинку строчка за строчкой. Сбросить Start Address в ноль. Потом дождаться конца рефреша экрана и скопировать недостающий кусок снизу на верх, надеясь, что мы успеем это сделать, пока сканлайн не добежал до этой части. Так я сначала и сделал. И на моем 486-м с картой Cirrus Logic все работало без проблем. И возгордился я, но тут же был низвергнут на землю аки змий Но радость была временной. Оказалось, что на некоторых видеокартах, даже более современных, этот "фокус" дает мигание в нижней части экрана. То есть этот кусок не успевает полностью скопироваться до того, как видяшка начнет отрисовывать эту часть видеопамяти.

Что ж, дедлайн был уже на носу, и выход был очевиден. По умолчанию считаем, что у нас порядочная видяшка с враппингом, а для случаев с Tseng'ом и подобными - опционально оставляем вышеописанный механизм. К счастью на Tseng оно как раз работает без косяков, ничего не мигает, а у организаторов пати для проверки и каптуринга DOS-демок была машина с S3 Trio 64, на которой и с "враппингом" все в порядке. В общем никаких проблем. Да и ведь даже Джон Кармак так делал, так что и мне не стыдно. Но чувство, что можно сделать что-то ещё, чтобы было универсально, красиво и по-умному, не покидало меня.

И, как в той байке про "Hello, world!", был еще один программист... В данном случае artёmka (aka wbcbz7/sibCrew), который сказал: "экранируй через Line compare". Я сначала не понял, а потом как понял!

Что такое Line compare - мы подробно разбирали в предыдущей статье, но напомню: это такой регистр видеокарты, с помощью которого можно указать - на какой строке экрана сбросить счетчик и начать чтение видеопамяти с нулевой позиции, продолжая заполнять экран. Благодаря чему VGA имеет такую фичу как "сплиттинг" (о чем мы тоже говорили в прошлой части).

А теперь - что мы делаем? На старте мы устанавливаем "Line compare" на позицию равную высоте экрана (кол-во строк в текущем разрешении, 400 или 480), в таком положении это не оказывает никакого влияния (счетчик и так и сяк сбросится перед началом отрисовки следующего кадра). Когда же нижняя граница отображаемого фрейма упирается в последнюю строку видеопамяти, мы начинаем поднимать Line compare, синхронно с увеличением Start Address. После строки, на которую у нас сейчас указывает значение Line compare, видеокарта будет рисовать на экран начиная с нулевого адреса видеопамяти. То есть мы как бы эмулируем "враппинг", вытягивая начало видеопамяти снизу экрана.
Когда Line compare добирается до нулевой строки - мы сбрасываем Start Address, а Line compare снова устанавливаем за последней строкой (как на старте), и далее все повторяется.
Единственно, чтобы картинка правильно затайлилась (без отдельных танцев), нам придется установить логическую длину строки в 512 (через CRTC-регистр Offset), но это не проблема.
Схематически все выглядит примерно как на гифке объясняющей случай с "враппингом".

Вот теперь всё работает и на ровных, и на кривых видимокартах... И без ручного выбора. Славтебегоспаде, переходим к следующей части.

Заливка методом Xorfill (Glenz scroller)

Вслед за "гритсами" идет часть с релаксовой музыкой и цветастым текстовым скроллером. Можно подумать, что у меня проблемы с фантазией. Опять какой-то скроллер с буквами. А всё дело в скрепах и традиционных ценностях. В древности демки состояли из текстовых скроллеров (на разный манер) чуть более чем полностью, в том числе и на PC. На Commodore-64 и сейчас чтут этот обычай. Так что идея была в создании вайба старой школы, ведь демка делалась для Oldskool compo. Ну и чего греха таить, я тоже люблю скроллеры, если они плавные и красивые.

В данной части весь контент рисуется с помощью алгоритма "Xorfill" (XOR fill, XOR filling, называйте как хотите). Очень простой, очень олдскульный и очень полезный способ закраски произвольных контуров. Особенно актуален на Amiga, где его можно реализовать на блиттере (этакий шейдер-процессор на минималках). Но и на старых PC, если речь идет о демках, xorfill очень даже использовался (например, в той же Second Reality). Суть метода: рисуем в буфере контуры (границы) фигуры (одной, или нескольких), а потом проходимся по буферу вот таким макаром:

for (x=0; x < screen_width; x++)
{
    color = edge_buffer[x][0]; 
    screen[x][0] = color;

    for (y = 1; y < screen_height; y++) 
    {
        color = color XOR edge_buffer[x][y];
        screen[x][y] = color;
    }
}

Проход можно делать и горизонтально. Но вертикально удобнее тем, что можно "красить" сразу по 4 пикселя, используя 32-битные инструкции, что ощутимо быстрее.

Для наглядности здесь отображаются границы, на практике мы их не видим (они в отдельном буфере)
Для наглядности здесь отображаются границы, на практике мы их не видим (они в отдельном буфере)

Плюшки данного метода:
- Буфер контуров может находиться в обычной памяти, а "заливка" выполняться сразу в видеопамять (напомню, что чтение из видеопамяти очень медленное, но здесь мы туда только записываем).
- Когда мы хотим изобразить что-то вроде полупрозрачных объектов, наложенных друг на друга - мы можем отрисовать это в один проход и не используя экранный видеобуфер в ОЗУ. Удобно для рендера glenz-многогогранников, так любимых демосценерами.
- Даже если у нас не возможности использовать бэкбуффер, мы можем смело рисовать в видеопамять и у нас не будет миганий, а обновление кадра будет выглядеть сносно, даже если FPS не очень высокий и кадр не успевает обновиться за номинальный рефреш развертки.

Но и, как вы, возможно, догадываетесь, ограничений, с которыми придется мириться, тоже хватает. Но для демосценера ограничения не проблема, а вызов :)

Выше я визуализировал процесс "заливки" для обычного VGA-режима, с линейной организацией видеопамяти. Но если вы читали первую статью, то помните, что мы используем mode-x. А это значит планарный режим. Заново объяснять - что это за чертовщина и зачем попу гармонь - я, пожалуй, не стану, просто прочитайте это в первой части.

Ну а что же тогда в mode-x? Ничего особенного. Просто каждый план мы закрашиваем отдельно. Получается этакий проход "расческой" со сдвигом:

Да, конечно, чтобы рутина xorfill'a была максимально быстрой, нам нужно, чтобы и edge-буфер по структуре повторял парадигму планов. Так же, дабы кэш процессора работал эффективно, я придумал такую структуру буфера контуров, чтобы при вертикальном проходе чтение происходило последовательно. В итоге непосредственно процедура на ассемблере выглядит очень просто:

Скрытый текст
procedure xorfill_modex(source, output:word); assembler;
  asm
  push ds
  mov es, output
  mov ds, source
  xor di, di
  xor si, si
  xor ebx, ebx
  mov dx, 80/4
  @x:
    mov eax, ds:[si]
    mov cx, 200           //сканируем колонками, высота 200 пикселей
    @y:
        xor eax, ds:[si]
        mov es:[di], eax //записываем 4 байта в видеопамять, или бэкбуфер)
        mov ds:[si], ebx //заодно чистим буфер контуров
        add si, 4
        add di, 80
        dec cx
    jnz @y
    sub di, 80*200-4
    dec dx
  jnz @x
  pop ds
end;

Ну, с xorfill-ом разобрались. Но быстро сказка сказывается, да не скоро демка пишется.
Когда сцена уже была готова, буквы были нужного размера, бабочек, рыбок и котеек было в необходимом количестве, оказалось, что по скорости у меня фактически нет никакого запаса. То есть да, сцена давала 70 фпс на моем 486/66Мгц, но уже на пределе, и только при установке муз.плеера в 22 кГц mixing rate (для Sound Blaster). Стоило выбрать 44 кГц, как начинались дропы кадров. Скроллинг переставал быть идеально плавным. А кому нужен не плавный скроллинг? Никому не нужен!

Что-то упрощать во вред визуалу, который я уже для себя зафиксировал как удовлетворительный, я не хотел. Я так обычно не делаю. Это не спортивно. Код был уже заоптимизирован по десятому кругу. Что ж, нужна была идея. И желательно такая, чтобы не пришлось переписывать все заново.

Да... Тут бы стоило сказать, что на текущий момент в этой части у меня уже была реализована тройная буферизация с использованием аппаратного переключения видеостраниц. Одно из преимуществ modex-режима. Может это и звучит сложно, но по факту это работает так же как и скроллинг, который мы разобрали выше. Просто здесь мы скролим не на одну строчку, а на целый экран, тем самым выбирая - какую область видеопамяти сейчас отображать. Это удобно, поскольку нам не нужно ни расходовать обычную память (ОЗУ) для бэкбуфера, ни копировать бэкбуфер в видеопамять (что для 486-го еще вполне значимая экономия процессорного времени).

Сейчас будет лекция для колхозников немножко скучной информации, возможно вы это знаете и без меня, а может и не хотите знать, в этом случае можете пролистать.

Схема работы моей тройной буферизации. У нас три видеостраницы. И возможные статусы:
- display (отображается),
- ready (ждет показа),
- drawing (рендерится),
- free (свободна для рендера)

Так же у нас есть прерывание, которое происходит в момент, когда начинается обратный ход луча развертки (VBLANK). Стоит пояснить, что сама видеокарта такого прерывания нам не предоставляет (нет, что-то такое вроде было в эру папоротниковых, и включалось перемычкой на самой карте, но впоследствии было отвергнуто эволюцией). Так что программистам приходилось программировать системный таймер на частоту вертикальной развертки, танцами с бубенцами синхронизировать его с VBLANK'ом и собственно вешать свой обработчик прерываний на IRQ 0. К счастью трекерный плеер MIDAS (который я использую) делает это сам и предоставляет нам калбэк.

В общем, теперь вы знаете, что помимо рендер-цикла у нас есть функция, которая вызывается всегда, когда видеокарта отрисовала кадр на экран, вне зависимости от работы основного цикла. Получается стабильный вызов 60 или 70 раз в секунду (в зависимости от видеорежима).

Так вот. В основном цикле мы ищем страницу со статусом "free" и начинаем в нее рисовать, присвоив статус "drawing", а после отрисовки кадра мы ждем, пока из конвейера пропадет страница со статусом "ready", присваиваем "ready" только что отрисованной, и сразу же возвращаемся к работе (и пусть вас не смущает, что мы здесь чего-то "ждем", но об этом позже).

Обработчик сидящий на прерывании постоянно проверяет - есть ли страница со статусом "ready", и если да, то переключает адрес видеокарты (Start Address) на неё, устанавливает статус "display", а ту страницу, что отображалась до нее, переводит во "free".

Для тех, кто любит блок-схемы я нарисовал такое (я, правда, не очень хорошо рисую блок-схемы):

Скрытый текст

Данная схема немного отличается от классической, поскольку обычно при тройной буферизации не ждут пока в очереди не останется страниц в статусе Ready. Но я могу объяснить. Давайте разберемся, зачем нам вообще тройная буферизация? В традиционных для DOS-демок и игр схемах либо вообще не использовали бэкбуфера и рисовали сразу в видимую область видеопамяти (я так делаю в предыдущих частях демки Remoded). Либо использовалась двойная буферизация - пока показываем один буфер, рисуем в другой. В основном это нужно, чтобы мы не видели процесс построения кадра, особенно если требуется очищение, многопроходный рендер, когда какие-то детали рисуются поверх других. Перед тем, как сделать готовый отрисованный кадр видимым нам нужно дождаться состояния START VBLANK (вертикальный интервал гашения), когда видеокарта уже отрисовала на монитор текущий кадр, но еще не начала выводить следующий. Значит после рендера мы входим в цикл и читаем порты состояния видеокарты, пока не словим состояние начала retrace ("луч" развертки возвращается к началу экрана). В этот небольшой тайминг нам и надо либо успеть скопировать софтварный (из ОЗУ) буфер в видеопамять, либо переключить Start Address, в случае с аппаратной сменой видеостраниц.

Это может решать две задачи. Мы ограничиваем FPS частотой экрана, чтоб рендер не молотил зазря, если компьютер достаточно быстрый, чтобы рендерить больше одного кадра за время рефреша экрана. А также позволяет избежать "screen tearing'a", когда на экране одновременно виден кусок нового кадра поверх старого.
Но взамен мы получаем в рендер-цикле залипуху, во время которой мы ничего полезного не делаем, только ждем. Если кадр успевает отрендериться за 1 / "частота кадров монитора" секунды, то все хорошо, но если рендер хоть чуточку опаздывает, то мы неизбежно прилетаем в состояние "ждем начала retrace", и FPS уполовинивается. То есть: не успели впрыгнуть в вагон - значит ждем следующего. Вместо, скажем, 59 кадр/сек мы сразу же получаем 30. Это обидно, нечестно!

Так вот. Когда у нас есть еще и 3-й буфер (отображаемую видеостраницу мы здесь тоже считаем буфером, чтобы не путаться), а заодно и вышеописанное прерывание, то мы можем немножко развязать рендер и видео-подсистему. Вместо того чтобы после окончания рендера кадра ожидать VBLANK'a, мы помечаем отрисованный буфер как готовый к показу, а сами сразу же начинаем рендер в следующий свободный буфер. Обработчик прерываний работает асинхронно, так что он сменит кадр в нужное время, пока наша программа занята обработкой следующего кадра, а не пинает груши. И, что важнее, теперь если наш рендер не успевает за вертикальной разверткой, он не упирается в цикл ожидания следующего вагона. Поэтому FPS не режется в два раза, как вышеописанной схеме.

Зачем у меня после рендера вставлено ожидание ухода из очереди страницы со статусом Ready? Обычно при тройной буферизации не ждут, а продолжают рендер в первый же освободившийся буфер (страницу). Дальше, в случае когда рендер опережает вертикальную развертку, есть два варианта, либо создавать очередь, либо из отрендеренных буферов выбирать для отображения самый свежий (тогда дропнется предыдущий кадр). Но у нас ведь не игра, а демка, и анимация здесь все равно привязана к рефрешу и не может (и не должна) обгонять его. Так что, если рендер происходит быстро - мы фактически ждем VBLANK, как и в схеме двойной буферизации. А если комп тормозит, то ожидания не происходит, так как к концу рендера никакой страницы в очереди не будет и мы сразу пойдем рендерить следующий кадр. Короче, получается гибрид triple и double buffering ;) Такое вот "ноу-хау", как говорили при Михаил Сергеевиче.

Ну да ладно. Скучные лекции закончены. Дальше будет больше экшена и цветных анимированных картинок!

Возвращаемся к нашему барану. Несмотря на весь этот аналоговнетный высокотех, до конца проблему я не решил и скроллер все еще мог показывать рыбов c дропами кадров, при настройках на максималках. Это не было проблемой для показа на пати (орги предоставляли для DOS-демок 486DX4/120Mhz), но перфекциониста внутри меня это корёжило. Ведь все прочие части демки были устроены так, что медленный компьютер практически не нарушал плавности анимации, а лишь добавлял этакий "motion blur", а скроллер с greets'ами вообще имел огромный запас по скорости. И ведь обязательно найдется добрый человек, который попробует запустить демку на 486/33Мгц, с ISA-картой, на которой играл еще Дмитрий Лозинский (а потом еще и выложит каптур на ютуб).

И вот, протупив над кодом очередной вечер, я уже лег спать. Но вместо нормального сна в голову лезли сорцы, которые я мысленно редактировал, пытался компилировать и запускать, но тут же вспохватывался и просыпался, осознавая, что так это не сработает. Мне это надоело и я всё же сел за комп (на этот раз в реальности). И, изменив буквально пару строк, внезапно запилил "адаптивную детализацию". И самое замечательное, что благодаря modex-режиму это получается как бы "из коробки" и "на халяву". Сейчас объясню.

Из предыдущего урока по modex'у вы должны помнить, что записывая байт по одному и тому же адресу мы можем одновременно нарисовать в видеопамять и один пиксель, и два, и три, и четыре. В какие из четырёх планов записывать - регулирует маска в соответствующем регистре видеокарты. Если маска равна 1, то запись разрешена для первого плана (мы считаем их с нуля, так что это "Plane 0"). Для записи во второй - 2, для записи в третий - 4, в четвертый - 8. Если вы посмотрите на бинарное представление этих чисел, то сразу поймете логику. Но никто не мешает нам назначить маске значение 15 (1111 в бинарном представлении), и тогда байт запишется во все четыре плана, а это четыре пикселя подряд. Логика справедлива и для любых других комбинаций первых четырех бит. Например 1001 - запись в Plane 0 и в Plane 3, а два плана посрединке останутся нетронутыми.

Демо "Crystal Dream II"                                            Игра "LHX: Attack Chopper"
Демо "Crystal Dream II" Игра "LHX: Attack Chopper"

Эта фича была весьма популярна, когда графоний в играх (и демках) представлял из себя полигоны закрашенные одним цветом (flat shading). Ведь в планарном режиме вы могли за одну запись бахнуть не только 4 пикселя, но и 8, или даже 16 (32-битная запись, если речь о 386-м и выше). Возможно, вы удивитесь, но это эксплуатировалось даже в Doom. Кармак заюзал эту фичу для настройки разрешения рендера: если Graphics detail: low, то рендер логически был 160 пикселей в ширину, а удваивались пиксели за счет чередования маски 0011/1100...

Так, минуточку... значит это не я придумал, а Кармак? Выходит зря статью писал. До свиданья.

Ну уж нет. На самом деле у меня даже немножко интереснее (естественно на мой взгляд).
Так, и что я сделал? Как вы должны помнить из описания xorfill'a в режиме modex'a у нас была этакая лапша из планов: сначала рисуем Plane 0, потом Plane 1, Plane 2, Plane 3. В четыре прохода заполняя весь экран. Пока не отрисованы все 4 плана на экране имеются необновленные колонки.

Я изменил маски планов таким образом: 15, 14, 12, 8. В бинарном виде (1111, 1110, 1100, 1000). И этапы закраски стали выглядеть вот так:

Видите, что происходит? Сначала заполняется грубо, по всем 4 планам, а каждый следующий проход добавляет детализации. И благодаря modex-режиму мы не увеличили количество записей, мы гоняем по шине столько же данных, как и раньше. То, что вместо одного пикселя видеокарта рисует 4 (или 3, или 2), на скорость никак не влияет, это "аппаратные дела" самой видеокарты.

И вот теперь-то главный трюк: после каждого такого прохода я проверяю - есть ли в очереди страница со статусом Ready? Если нет (а значит она ушла на экран), то срочно бросаем рендер и отправляем в очередь то, что успели отрисовать. Получается, что вместо вероятного дропа кадра мы показываем кадр с меньшей детализацией (разрешением).

Может показаться, что выглядит это не очень. Но на самом деле, если компьютер не радикально тормознее, чем нужно, а лишь процентов на 10-15%, то визуально вы даже не заметите подвоха. Если мы пропускаем последний проход, то кадр не так уж и сильно страдает, особенно учитывая стилистику сцены (пикселизация видна лишь на краях объектов).
И это прекрасно избавляет от "импульсных" тормозов, когда производительность рендера вдруг упала на какие-то доли секунды (причины: музыкальный плеер, работающий параллельно, потребляет ресурсы CPU не равномерно; увеличившееся число объектов на экране и бог знает что еще), механизм прекрасно сглаживает ситуацию, не допуская пропусков кадров. Ну, и даже если запустить это на достаточно тормозном компе... Я считаю - лучше пикселизация (не путать с шакализацией), чем не гладкая анимация. Особенно в скроллере.

Ротозумер 12-ти тысяч цветов (Clownzoomer)

Наконец мы дошли до следующей части. И писанины здесь будет гораздо меньше (наверное). Если вы читали мою статью про "ротозумеры", то скорее всего помните, что последние годы на демосцене проходит этакий челлендж среди "амижников": кто сделает 4096-цветный ротозумер с наибольшим количеством пикселей (колонок) по горизонтали. Это какие-то сугубо "амижные" приколы, мы их здесь разбирать не будем. Но потихоньку этот локальный мем вовлек и другие платформы. Главная идея - сделать не просто ротозумер, а с помощью какой-либо техники воспроизвести большое количество цветов, большее, чем позволяет платформа в штатном режиме. В ход идут и уловки, создающие лишь иллюзию многообразия цветов. Именно этот челендж я и поддержал в демке "Remoded", сделав целых два ротозумера в разной технике имитации "многоцветности" обходясь возможностями VGA. Первый ротозумер мы разбирали в предыдущей статье, и там была совершенно иная песня.

Здесь я решил поэкспериментировать с эмуляцией цвета за счет быстрого переключения двух страниц. Идея не оригинальна, у спектрумистов это называется "Gigascreen". Суть проста: если, например, компьютер не умеет отображать желтый цвет (привет БК-0010), то на одной картинке нужные участки красим в зеленый, на другой картинке эти же места красим в красный, если потом эти изображения чередовать с большой скоростью, то мы увидим что-то вроде желтого цвета (стремно мигающего желтого цвета. Да эмуляторы обычно убирают мерцание и все выглядит идеально, но это читерство.) Таким макаром, как вы понимаете, можно получить и другие оттенки.

Воизбежание приступов эпилепсии скорость чередования символическая
Воизбежание приступов эпилепсии скорость чередования символическая

Говорят, что на PC существовали "вьюверы" картинок, эксплуатирующие этот метод, чтобы отображать truecolor-картинки на компьютере с видеокартой, которая поддерживала только режимы с 256 цветами. Сам не видел. Так что мне давно хотелось проверить это на практике.

Здесь стоит заметить, что в дикой природе демосцены встречается еще один метод эмуляции "высокоцветности", похожий на то, что мы делали в предыдущих частях, описанных в первой статье. Но более хитроумный. В нем ничего не мигает, но картинка делится на два канала: сине-зеленый и красный, а выводится в виде "шахматки". То есть синий и зеленый уже сложены в одном из пикселей, и к ним остается добавить красный оттенок. Получается, что логический пиксель состоит всего из двух реальных, а не из трех, что конечно же во всех смыслах удобнее. Эмулируемый итоговый цвет здесь получается за счет близости пикселей, как и в любом dither-эффекте.

64кб-интро Jest by Camora (1997), в режиме эмуляции hicolor
64кб-интро Jest by Camora (1997), в режиме эмуляции hicolor

Я же решил совместить оба этих трюка. И, когда попробовал это на статичной картинке, даже немного удивился. Если без "шахматного" паттерна мерцание еще было заметно, то с "шахматкой" картинка была практически идеальной (при дотошном разглядывании можно увидеть лишь нечто похожее на шум кинопленки). Никакого мерцания неприятного для глаз, ни на реальном железе, ни в эмуляторах (при правильной настройке). LCD-монитор, CRT... без разницы. Полагаю, на ЭЛТ, или старом ЖК помогает еще и инерционность экрана. Я не был уверен, будет ли это так же хорошо при движущейся картинке, но опасения были напрасны, в динамике было все также прилично. Картинка на реальном железе выглядит почти так же, как и на ютубе, только менее замыленой.

Как выяснилось впоследствии, то же самое, но несколькими годами ранее, мой коллега и соавтор демок @Manwe_SandS уже делал, но на БК-0010 и подробно описал свой опыт.
("...на рассвете, когда дописаны были последние строки, я вспомнил, что этот стих уже написал А. Пушкин. Такой удар со стороны классика! " (с) Остап Бендер).

И так, исходная картинка у нас в True color. Делаем две копии. Из одной удаляем красный канал, в другой только красный и оставляем. Эти операции я проделал в обычном графическом редакторе (я использую GIMP).

После чего сине-зеленую картинку конвертируем в 192-цветный indexed color, а красную в 63-цветный (оставляя 1 цвет для белых букв в текст-скроллере). Далее нам нужно чтобы сине-зеленый спектр находился в цветах 0..191, а красный в 192..254, но это уже проще сделать программно. VGA-палитра получается адаптированной к существующим оттенкам, и будет статичной.
Остается лишь рисовать кадр "шахматкой", четные пиксели из текстуры 1, нечетные из текстуры 2, в следующей строке наоборот, и так далее. И одновременно, в другую часть видеопамяти (страница 2), рисуем кадр, где все наоборот. Ну вы поняли. А потом бешено переключаем страницы. То есть вешаем переключение на наш обработчик прерывания (которое засинхронено на начало VBLANK), и тогда это мигание страницами будет происходить со скоростью вертикальной развертки (60 или 70 раз в секунду), не зависимо от скорости рендера. Даже если ротозумер затупливает, это не повлияет на наш "стробоскоп".

В реальности смена страниц конечно же быстрее и мерцаний не видно
В реальности смена страниц конечно же быстрее и мерцаний не видно

Что же мы имеем в итоге? 192 сине-зеленых цветов * 63 оттенка красного = 12096 эмитируемых, но довольно убедительных цветов.

Остальное - дело техники: написать на ассемблере быстрый код ротозумера для планарного режима, который отрисовывает два кадра одновременно и с "шахматным" паттерном, настраивается под разный aspect ratio (под 4 разных разрешения). Протестировать на реальном ПК с 486DX2/66. Забраковать. Повторять цикл до наступления полного удовлетворения.

Деформация битмапа (Credits part)

Ну и завершающая часть с "кредитсами" (именами авторов). Внезапно не скроллер и не ротозумер. Но это все равно довольно распространенный 2D-эффект, встречающийся в демо разных эпох. Его механизм достаточно прост, но раз уж я подробно описывал все предыдущие эффекты, было бы нелогично обойтись тут лишь общим описанием. Так что будем разбираться.

Сразу стоит уточнить, что здесь я использовал тот же формат fake-mode'a, что и в первых трех частях демки, когда 1 логический пиксель составлен из трех реальных, каждый из которых отвечает за один из RGB-каналов, а рисуется это все вот такой "мозаикой":

Это основное отличие от любых других DOS-демок (и так сказать, выпендрёж), которое также вносит самую значимую долю сложности в код и его оптимизацию. Но все это, как водится, интересно лишь самому кодеру. Так что эти детали мы опустим.

Суть эффекта в деформации текстуры по сетке. Когда-то я уже описывал эффект с деформацией битмапа, но там мы использовали статичную UV-карту, где для каждого экранного пикселя указывался адрес текстурного пикселя (текселя). Здесь же принцип другой.

Для начала нам понадобится "рутина" текстурирования квада (четырехугольника). Все очень похоже на обычное аффинное текстурирование, только упрощенное. Кроме того, что мы текстурируем не треугольник (как если бы это была 3D-графика), так еще и наш квад имеет фиксированный размер, никак не поворачивается, не искажается, являясь просто 2D-ячейкой на экране. Изменяются в нашем случае только текстурные координаты (обычно их обозначают буквами UV, вместо XY). Мы вычисляем "дельты", а затем рисуя на экране квад, пиксель за пикселем (построчно, или по столбцам), интерполируем координаты чтения пикселя из текстуры. Пример кода (для простоты на Си и на флоатах):

Скрытый текст
// Размеры квада
const int W = 64;
const int H = 64;

//Дельты для вычесления краев области чтения текстуры
float duLeft  = (lb.u - lt.u) / (H - 1);
float dvLeft  = (lb.v - lt.v) / (H - 1);
float duRight = (rb.u - rt.u) / (H - 1);
float dvRight = (rb.v - rt.v) / (H - 1);

float uL = lt.u;
float vL = lt.v;
float uR = rt.u;
float vR = rt.v;

for (int y = 0; y < H; y++)
{
    // дельты внутреннего цикла отрисовки строки
    float du = (uR - uL) / (W - 1);
    float dv = (vR - vL) / (W - 1);

    float u = uL;
    float v = vL;

    for (int x = 0; x < W; x++)
    {
        screen[y][x] = texture[(int)v][(int)u];

        u += du;
        v += dv;
    }

    uL += duLeft;
    vL += dvLeft;
    uR += duRight;
    vR += dvRight;
}
       То что получается в кваде                       Область читаемая из текстуры
То что получается в кваде Область читаемая из текстуры

Потом нам остается только построить сетку из таких квадов, и колбасить UV-координаты по какой-то функции:

Способ колбасить уже целиком зависит от вашей фантазии и эстетического чутья.
Профит же такого метода отрисовки эффекта деформации, я думаю, очевиден: вместо вычисления сложной функции для каждого пикселя, мы вычисляем ее только для узлов сетки, внутреннее пространство заполняется с помощью интерполяции между значениями в узлах, что гораздо быстрее. Минусы тоже очевидны, но это уже вопросы дизайна и компромиссов.

Вот пожалуй и все, друзья. Надеюсь вам удалось погрузиться в олдскульный вайб и почувствовать тот самый demo spirit "девяностых". Кто-то вспомнил молодость, у кого-то даже рассосался камень в почке, кто-то обрел покой и умиротворение, а кто-то упал на пол и забился в конвульсиях... Это нормально. Мои статьи иногда оказывают подобные эффекты. В основном все заканчивается хорошо.
Если вы нашли в статье откровенную глупость - прошу списать это на ступор мозговины и возрастное расслоение мозжечка (у автора). Если же у вас есть вопросы и пожелания - не стесняйтесь, пишите студия Останкино, передача "Сельский час" в комментариях.

Полезные ссылки:

Демосцена в России:
https://retroscene.org/
https://www.demoscene.ru/
https://chaosconstructions.ru/ - фестиваль Chaos Constructions (Питер)
http://www.dihalt.org.ru/ - демопати DiHalt (Н.Новгород)
https://multimatograf.ru/ - фестиваль «Мультиматограф» (Вологда)

Демосценерские ресурсы (международные):
https://www.pouet.net/ - новые демо-релизы и основная тусовка западной сцены
https://www.demoparty.net/ (календарь мировых демопати)
https://demozoo.org/ - новости (жиденькие) и каталог всех релизов мировой сцены https://www.youtube.com/@psenough/videos - еженедельные обзоры жизни демосцены

Старый добрый DEMO.DESIGN FAQ (материалы из FidoNet и прочий стаф из 90-х):
https://www.enlight.ru/demo/faq/
https://www.enlight.ru/faq3d/content.htm

Комментарии (1)


  1. Manwe_SandS
    18.06.2026 14:10

    Блок-схема гениальная