Здравствуйте, меня зовут bitl, и я люблю демки. Особенно олдскульные. Люблю изучать принципы демо-эффектов - тех, что поражали и восхищали в 90-х годах и геймера, и заправского программиста. Пытаться их воспроизвести, используя аутентичное "железо", сделать также или даже лучше. Иногда это выливается во что-то осязаемое, иногда нет...
Как раз сейчас на очереди новый релиз, и казалось бы уже самое сложное позади, осталось всего ничего... И вот в такие моменты приходит она... Прокрастинация, творческий тупик, приступы пинания гектокотилей - в общем экзистенциальный кризис.
И чтобы как-то расшевелить себя, решил написать очередную статью про демокодинг. Хуже ведь не будет?
Я уже затрагивал тему олдскульных демо-эффектов и описывал их принципы в статье с разбором демки Demoded. Но там я не углублялся в разбор кода, ограничиваясь только объяснением концепции эффектов. Теперь же я решил попробовать разобрать один демоэффект, но с самого начала и до конца (насколько это возможно).
Напомню, речь идет о ретро-кодинге и разборе передовых дедовских технологий из тех времен, когда PC называли "IBM-совместимым", видеокарты не умели ничего майнить, а на процессорах не было не то что вентиляторов, но даже радиаторов. Никаких OpenGL/DirectX, только DOS, софтварный рендер и панк-рок (в s3m-формате).

В олдскульных демо-эффектах есть одна замечательная вещь. За ними почти всегда стоит какой-то фокус. Глядя на демо первой половины 90-х, программист начинавший с "Пентиумов" может сказать: "Хм, мило, но это все несложно, я мог бы сделать также". Но это может быть поспешным суждением. Как только вы попытаетесь кодить графику для 386-го (пусть даже для топового, с 40 МГц и хорошей быстрой ISA-видеокартой) у вас возникнет вопрос - какого черта, почему все так медленно? Должно быть есть какая-то магия, или как у этих ребят все летает? Полноэкранные эффекты, плавный скроллинг, при том, что у вас просто заливка экрана одним цветом не вывозит и 30 FPS?
Фокус может заключаться в дизайне эффекта, в концепции алгоритма, в ловком использовании какой-то аппаратной особенности платформы, в кодерском трюке, во всем сразу. А когда мы видим, что один и тот же эффект (с небольшими модификациями) встречается в десятках (а то и сотнях демок), невольно приходит мысль - это "ж-ж-ж" неспроста: вероятно эффект построен на какой-то фиче, которая и позволяет ему быть таким, какой он есть и работать быстро. Этакий антропный принцип демомэйкинга.
Но довольно философии. Давайте ближе к делу. Я давно думал - а какой бы несложный эффект можно было разобрать для статьи? Для начала, видимо, это должен быть эффект, который я сам понимаю. А еще это должен быть узнаваемый эффект. Но тут на один сценерский форум пришел какой-то парень с вопросом про оптимизацию эффекта "ротозумер" (rotozoomer). Ему, конечно, объяснили: чо как. В общих чертах. А я определился с темой статьи. Действительно, почему бы не "ротозумер"? Это один из тех эффектов, который при кажущейся простоте имеет массу нюансов реализации и пространство для трюков.
Немного истории (что, опять?). Классический "ротозумер" - вращающаяся (и, обычно, масштабируемая) затайленная текстура, в DOS-демках впервые появилась, скорее всего, в 1993-м году. По крайней мере я не нашел более ранних примеров. В двух демках одновременно и на одной и той же демопати Assembly'93, первопроходцы PC-демосцены Future Crew и амижники The Silents представили народу полноэкранный и плавный "ротозумер" в DOS-демках Second Reality и Optic Nerve. Но в итоге "Вторая реальность" стала хитом и нетленкой на все времена, так что говоря про "ротозумеры" обычно приводят в пример эффект с этой мордой:

Господа Амижники, прошу не шуметь! Перестаньте кидать стулья в докладчика! Да, на Амиге и Atari ST "ротозумеры" появились раньше. Как минимум уже в 1992-м году в демках группы Sanity (World Of Commodore и Rotozoom), а также в Desert Dream от Kefrens (которая вышла в начале 1993 года, и на полгода раньше ПЦ-шной Second Reality).
В том же 1993 году, но в декабре, данный эффект продемонстрировали демки legend от Impact Studios и Assembler Instinct от Gollum.

А потом понеслось. Ротозумеры стали одним из тех эффектов, которые делала каждая собака. Уважающие себя собаки демокодеры пытались привнести в него какие-то новшества, прокачать эффект каким-либо образом. Например, поиграться палитровой анимацией, добавить искажений, совместить с другим классическим эффектом.
Другие просто вставляли в свою интру/демку этот эффект и свою картинку, вероятно даже не разбираясь с кодом (исходники с незатейливой реализацией часто встречались на развалах BBS-ок и в фидошных кодерских эхах).
Ну а мы попробуем все же разобраться. Причем я предлагаю не разбирать уже готовый код, а как бы изобрести этот эффект с нуля. Представьте, что на дворе эдак 93-й год, а вы простой студент, увлекающийся программированием, а вовсе не инженер из Silicon Graphics. Возможно вы даже уже видели такие вращающиеся картинки, и решаете запилить что-то похожее самостоятельно. Мы будем использовать Turbo Pascal. Ну, во-первых, это аутентично, во-вторых мне так удобнее :) Но не спешите переключать канал. Код будет очень простым и понятным, даже если вы, прости Господи, питонщик.
Давайте сразу условимся, что мы будем использовать real-mode, VGA-режим 13h (320x200, 1 байт на пиксель), текстуру 256х256 (почему это удобнее всего - поймете позже). Будем использовать стандартный DOSBox с циклами установленными в 20 000. Это примерно как 486DX2 66 MHz. Очень жирно для 93-го, да. Ну, допустим, что вы мажорный скандинавский студент.
Первым делом мы попробуем просто отрисовать текстуру 256х256 на экран в режиме 320х200. Мы не будем рассматривать код загрузки текстуры в память, и прочую ерунду. Просто считаем, что текстура у нас уже в своем отдельном сегменте, а графический режим инициализирован. Пишем:
for y:=0 to 199 do for x:=0 to 319 do begin offset := byte(x) + byte(y) * 256; color := mem[texture:offset]; setpixel(x,y, color); end; end;
Здесь надеюсь все понятно? Setpixel() - некая процедура, рисующая точку на экране. Пока так, чтобы было максимально понятно. X, Y - приводим к типу byte, чтобы текстура правильно зациклилась.
Что ж, как нам поворачивать текстуру? Когда я был маленький (и бегал в валенках, с кудрявой головой) и пытался программировать графон на Бейсике, то думал, что поворачивать (или как-то модифицировать) нужно координаты точки (пикселя), которую мы рисуем. И да, в других ситуациях так и есть. Но не в случае с растровыми эффектами. Здесь мы должны мыслить как шейдер. Точки мы рисуем подряд, проходя весь экран строку за строкой. А поворачиваем мы точку (координаты точки), которую собираемся прочитать из текстуры.
Ну формулы поворота точки в 2D мы знаем (но почему-то не из уроков алгебры, а из FIDO-шной эхи COMP.GRAPHICS.ALGORITHMS):
XR = cos(angle)*X + sin (angle)*Y;
YR = sin (angle)*X - cos(angle)*y;
Для начала пусть все расчеты будут на float-ах (real или single, если по-паскалевски), и приводить к целочисленным значениям мы будем уже на последнем этапе вычисления смещения в текстуре (функцией округления "round").
Angle - угол поворота в радианах,
Scale - коэффициент зума.
centerX, centerY - определяют центр вращения. Без них центром вращения растра будет верхний левый угол экрана.
Не будем сильно уж тупить и сразу вынесем вычисление sin и cos из цикла:
scale:=1; centerX:=160; centerY:=100; Tcos := cos(angle)*scale; Tsin := sin(angle)*scale; for y:=0 to 199 do for x:=0 to 319 do begin xr := Tcos*(x-centerX) + Tsin*(y-centerY); yr := Tsin*(x-centerX) - Tcos*(y-centerY); offset:=byte(round(xr)) + byte(round(yr))*256; color:=mem[texture:offset]; setpixel(x,y, color); end; end;
Я постоянно называю эти переменные Tcos и Tsin, не знаю, почему я так делаю...
Ладно. Запускаем. Все работает. Вот только скорость рендера - 2 кадра в секунду.

Ну ничего, мы ведь только начали. Теперь нам нужно уменьшить количество вычислений в циклах (особенно во вложенном). Помним, что умножение и деление - очень дорогие операции, они нам не бро, а сложения, вычитания, а также всякие битовые операции - бро (но и их - чем меньше, тем лучше).
Первым делом мы понимаем, что части с умножениями на Y мы можем вывести из цикла X (рисующего строку) наверх:
for y:=0 to 199 do begin ysin := Tsin*(y-centerY); ycos := Tcos*(y-centerY); for x:=0 to 319 do begin xr := Tcos*(x-centerX) + ysin; yr := Tsin*(x-centerX) - ycos; offset:=byte(round(xr)) + byte(round(yr))*256; color:=mem[texture:offset]; setpixel(x,y, color); end; end;
Уже лучше: 3 кадра/сек.

Замечаем, что Tcos*(x-centerX) в данном случае это просто линейное приращение. Чтобы было яснее представим что мы вращаем вокруг нулевой точки координат, тогда выражение выглядит как Tcos*x. А "х" ведь растет линейно. Так что мы можем просто делать приращение на Tcos каждый цикл. Как и Tsin для YR. Строго говоря, нормальный программист назвал бы это "дельтами". Что получается:
for y:=0 to 199 do begin ysin := Tsin*(y-centerY); ycos := Tcos*(y-centerY); xr:= Tcos*(0-centerX) + ysin; { начальные значения для строки } yr:= Tsin*(0-centerX) - ycos; { нули оставлю для наглядности } for x:=0 to 319 do begin xr := xr + Tcos; yr := yr + Tsin; offset:=byte(round(xr)) + byte(round(yr))*256; color:=mem[texture:offset]; setpixel(x,y, color); end; end;
Давайте не останавливаться и проделаем это и с циклом Y, получаем:
Tcos := cos(angle)*scale; Tsin := sin(angle)*scale; xcos := Tcos*(-centerX); xsin := Tsin*(-centerX); ysin := Tsin*(-centerY); ycos := Tcos*(-centerY); for y:=0 to 199 do begin xr := xcos + ysin; yr := xsin - ycos; ysin := ysin + Tsin; ycos := ycos + Tcos; for x:=0 to 319 do begin xr := xr + Tcos; yr := yr + Tsin; offset:=byte(round(xr)) + byte(round(yr))*256; color:=mem[texture:offset]; setpixel(x,y, color); end; end;
Неплохо. Мы убрали из цикла все умножения, касающиеся вычислений поворота.
Но это дало нам только +1 кадр/секунду. Так мы до китайской Пасхи будем оптимизировать?
Настало время избавиться от флоатов и переписать все на fixed point вычисления. Для этого просто умножим Tcos и Tsin на 256, а дальше будем работать с целыми integer-числами, а непосредственно при вычислении смещения в текстуре - разделим на 256. Такой точности в нашем случае будет вполне достаточно.
Tcos := round(cos(angle)*scale *256); Tsin := round(sin(angle)*scale *256); xcos := Tcos*(-centerX); xsin := Tsin*(-centerX); ysin := Tsin*(-centerY); ycos := Tcos*(-centerY); for y:=0 to 199 do begin xr := xcos + ysin; yr := xsin - ycos; ysin := ysin + Tsin; ycos := ycos + Tcos; for x:=0 to 319 do begin offset:=(xr div 256) + (yr div 256) * 256; color:=mem[texture:offset]; setpixel(x,y, color); xr := xr + Tcos; yr := yr + Tsin; end; end;
Так. Теперь у нас после вычислений Tcos/Tsin - все на integer'ax. Да, если в Турбо Паскале будет включен Run-Time Range checking, то он будет ругаться на выход из допустимого диапазона integer-переменных, но в нашем случае это не страшно. Не будем пока этим забивать себе голову.
Проверим что там с FPS: 7 кадров/сек.

Давайте сделаем наконец все красиво. Деление XR на 256 заменим побитовым сдвигом вправо на 8 (что намного быстрее), вместо "(YR div 256) * 256" достаточно просто обнулить младшие 8 бит с помощью маски и and-операции. Если не понятно - почему, поиграйтесь с кодерским калькулятором, наблюдая за двоичным представлением числа.
и перестанем наконец вызывать setpixel. Учитывая, что организация памяти в VGA-режиме обычная, линейная, а точки мы рисуем последовательно, строка за строкой - мы можем просто последовательно записывать байт за байтом прямо в видеопамять (или в кадровый буфер).
А заодно замечаем, что:
xr := xcos + ysin;
yr := xsin - ycos;
также можно убрать из цикла:
Tcos := round(cos(angle)*scale *256); Tsin := round(sin(angle)*scale *256); xcos := Tcos*(-centerX); xsin := Tsin*(-centerX); ysin := Tsin*(-centerY); ycos := Tcos*(-centerY); xr:= xcos + ysin; yr:= xsin - ycos; n:=0; for y:=0 to 199 do begin xtemp := xr; ytemp := yr; xr := xr + Tsin; yr := yr - Tcos; for x:=0 to 319 do begin offset:=(xtemp shr 8) + (ytemp and $ff00); mem[vga:n]:=mem[texture:offset]; inc(n); xtemp := xtemp + Tcos; ytemp := ytemp + Tsin; end; end;
Все работает эквивалентно прежним вариантам и код выглядит довольно лаконично. Но скорость рендера 10 FPS. В 5 раз лучше, чем первый вариант, но ведь в демках это работает гораздо быстрее?
Что ж, настало время Ассемблера:
push ds mov es, vga mov ds, texture mov si, Tcos xor di, di ; n:=0; mov y, 200 @y: ; for y:=0 to 199 do begin push bp mov ax, xr ; xtemp:=xr; mov dx, yr ; ytemp:=yr; mov bp, Tsin mov cx, 320 @x: ; for x:=0 to 319 do begin mov bl, ah ; offset:=(xtemp shr 8) + (ytemp and $ff00); mov bh, dh mov bl, ds:[bx] ; mem[vga:n]:=mem[texture:offset]; mov es:[di], bl inc di ; inc(n); add ax, si ; xtemp:= xtemp + Tcos; add dx, bp ; ytemp:= ytemp + Tsin; dec cx jnz @x pop bp mov ax, Tsin ;xr := xr+Tsin; add xr, ax ;yr := yr-Tcos; sub yr, si dec y jnz @y pop ds
Здесь я привожу только код рендер-цикла, вычисления вне цикла на скорость влияют почти никак, так что ради борьбы с энтропией...
И так, запускаем! И... 35 кадров/сек! Вот, уже веселее.

Теперь вы понимаете, что без ассемблера кодить графон в те времена было не то чтоб нельзя, но грешновато. Конечно, если использовать Watcom C, мы получим более быстрый код, чем у Turbo Pascal. Мне лень проверять. Я все-таки уверен, что с подобными частными случаями человек все равно справится лучше. Нет, дяденька, Claude Code не справится лучше!
Что мы тут делаем для оптимизации скорости?
Во внутреннем цикле используем только операции с регистрами, никаких обращений к памяти, кроме собственно чтения из сегмента текстуры и записи в видео-сегмент.
Вычисление смещения в текстуре "offset:=(xtemp shr 8) + (ytemp and $ff00)" заменяется двумя MOV'ами, поскольку каждый из четырёх 16-битных регистров данных (AX, BX, CX, DX) представляет собой два 8-битных (xL,xH), и у нас есть отдельный доступ к старшим 8 битам и к младшим. Проще говоря MOV BL, AH это то же самое что и BL = AX >> 8 и BL = AX / 256. Но занимает всего один такт.
Помещая же значение регистра DH (старшие 8 бит регистра DX) в BH, мы получаем значение OFFSET в регистре BX, который по счастью может служить индексом в адресе, и дает нам нужное смещение в текстуре (вот почему текстура 256х256 удобна).
Если вы об этом никогда не думали в таком ключе - подумайте, это забавно.
И этот простой трюк широко эксплуатировался в те времена ("когда пенопласт делали из молока, умножения были дорогими, а за деление давали в морду"). В общем, fixed-point 8:8 довольно удобная штука.
Ну и что? Казалось бы всё? Что мы еще можем поделать? Кое-что можем. Давайте попробуем развернуть внутренний цикл, чтобы обрабатывать два пикселя за проход и записывать в видеопамять WORD, а не байт. Это всегда быстрее. Но для этого нам нехватает свободного 16-битного регистра... Все заняты. Что ж, пора делать слегка безумные вещи. Сделаем код самомодифицирующимся. Но не пугайтесь, на самом деле ничего заумного. Мы знаем, что наши "дельты" Tsin и Tcos, которыми мы приращаем xtemp, ytemp (регистры ax и dx) константны для всего рендер-цикла одного кадра. Сейчас мы храним значения Tsin,Tcos в регистрах SI и BP. А обращаться к переменным в памяти, напоминаю, внутри критического цикла мы не хотим. И не будем. Мы можем сделать иначе.
Давайте динамически менять код перед тем как попадем в цикл, подставляя значения прямо в OP-коды процессора. Главное правильно рассчитать смещения. К счастью и Турбо Паскаль и любой Ассемблер позволяют ставить метку в коде, указывать ее имя в адресе, а компилятор сам подставит нужное смещение. Выглядит это так:
push ds mov es, vga mov ds, texture ; Самомодификация кода внутри вложенного цикла mov ax, Tcos mov cs:[offset @tcos1+1], ax mov cs:[offset @tcos2+1], ax mov ax, Tsin mov cs:[offset @tsin1+2], ax mov cs:[offset @tsin2+2], ax jmp @clear_prefetch; @clear_prefetch: xor di, di ; n:=0; mov y, 200 @y: ; for y:=0 to 199 do begin mov ax, xr ; xtemp:=xr; mov dx, yr ; ytemp:=yr; mov si, 160 @x: ;for x:=0 to 159 do begin mov bl, ah mov bh, dh mov cl, ds:[bx] @tcos1: add ax, 1234h ;<--- то что мы модифицируем @tsin1: add dx, 1234h ;<--- заменяя 1234h на свои числа mov bl, ah mov bh, dh mov ch, ds:[bx] @tcos2: add ax, 1234h ;<--- тут тоже @tsin2: add dx, 1234h ;<--- mov es:[di], cx ;<--- бахаем сразу два пикселя на экран add di, 2 dec si jnz @x mov ax, Tsin ; xr := xr+Tsin; add xr, ax mov ax, Tcos ; yr := yr-Tcos; sub yr, ax dec y jnz @y pop ds
Важно знать размер OP-кода инструкции, чтобы записать значения в правильное место, не испортив сам OP-код. Почему это лучше, чем читать значения из памяти? Ну, потому что это быстрее. Фактически так же быстро как и сложение двух регистров. Код все равно кэшируется... Да, о кэшировании кода. Стоит отметить этот нюанс. У процессора есть механизм предвыборки кода (Instruction prefetching). То есть он по ходу выполнения читает из оперативной памяти не инструкцию за инструкцией, а забирает сразу какой-то кусок кода в свой внутренний кэш и оттуда уже отправляет дальше по коридору. Но мы ведь модифицируем код в оперативной памяти сразу перед предполагаемым началом его исполнения? Учитывает ли это процессор? Пентиум да. 486-й и более ранние - нет. Так что нам надо как-то очистить очередь предвыборки. Для этого достаточно сделать фиктивный переход JMP SHORT $+2, что мы и делаем, теперь процессор гарантировано обновит код в своем кэше и начнет выполнять модифицированный код.
Но вернемся к коллайдеру. Что теперь у нас по скорости? 45 кадров/сек. Подняли на 10 FPS!
Можем больше? До сих пор мы использовали только 16-битные инструкции, но если считать, что наш эффект требует как минимум процессора 80386, то можно задействовать 32-битные регистры. И нет, для этого не нужен защищенный режим.
Давайте развернем цикл еще больше, чтобы обрабатывать сразу 4 пикселя и нарисовать их одной инструкцией за раз:
mov di, 64000-4 <--- WHAT? mov y, 200 @y: ; for y:=0 to 199 do begin mov cx, xr ; xtemp:=xr; mov dx, yr ; ytemp:=yr; mov si, 80 @x: mov bl, ch mov bh, dh mov ah, ds:[bx] @tcos1: add cx, 1234h @tsin1: add dx, 1234h mov bl, ch mov bh, dh mov al, ds:[bx] @tcos2: add cx, 1234h @tsin2: add dx, 1234h shl eax, 16 ; <--- проталкиваем два байта в старшие 16-бит mov bl, ch mov bh, dh mov ah, ds:[bx] @tcos3: add cx, 1234h @tsin3: add dx, 1234h mov bl, ch mov bh, dh mov al, ds:[bx] @tcos4: add cx, 1234h @tsin4: add dx, 1234h mov es:[di], eax ; <---- рисуем сразу 4 пикселя sub di, 4 ; sub? dec si jnz @x dec y jnz @y
Проверяем: 50 кадров/сек. Немного, но прибавили. Однако тут есть подвох...
Но сначала признаюсь. Мы здесь скакнули на два шага вперед. Если присмотреться к коду, то можно увидеть, что теперь мы рисуем кадр задом наперед. То есть, из текстуры мы читаем так же как и прежде, но на экран рисуем пиксели в обратном порядке, начиная от нижнего правого угла. Зачем?

Потому что так мы получаем правильный, для вывода на экран, порядок байтов в регистре EAX "из коробки", иначе же нам бы потребовалось сделать "ROL EAX, 16" перед отправкой данных на экран. А это лишние затраченные 2 такта.
Если мы рисуем задом наперед картинка просто получается повернута на 180 градусов (при нулевом angle). Почему это нас должно волновать? Да вроде не должно.

Придирчивый и опытный кодер может заметить: почему я не использую инструкцию STOS для записи пикселей? Как ни странно, но на реальном 486-м это медленнее чем "MOV + ADD". Ну вот так... И по документации STOS - 5 тактов, тогда как MOV+ADD всего 2 такта. В Досбоксе STOS показывает лучший результат. Но я считаю, что ориентироваться правильнее на реальное "железо".
И вот теперь о подвохе. Дело в том, что DosBox не воспроизводит все нюансы реального железа. Не имитирует механизмы кэша процессора, скорость шины и видеокарты и т.д.
Так что производительность, выставив некое число циклов, можно прикинуть лишь приблизительно. То что в DosBox быстрее - на реальном железе может быть медленнее, и ровно наоборот.
Пора перейти к тестам на реальном железе!
К счастью, у меня, вот прям тут же на столе, щелкает дисководом 486DX2 66MHz, VLB-видеокартой Cirrus Logic. Почти такой же, как в детстве. Посмотрим же что он нам выдаст.
И вот тут нас ждет двоякое чувство. Наш ротозумер крутится с непостоянным FPS. Начинает весело и задорно, но приближаясь к 90 градусам поворота скисает. Потом опять оживает, и на 270 градусах опять кряхтит. Тестируем в статичном положении. При 0 и 180 градусах показывает 82 кадра/сек. При 90 и 270 - 23 кадра/сек. Увы, это известная проблема. Дело в кэше. Да, процессор кэширует не только инструкции, но и просто данные, читаемые из оперативки. Но чтобы попадать в кэш мы должны стараться читать данные из памяти последовательно. Как вы уже, наверное, догадались, чем ближе к 90 градусам мы поворачиваем, тем более нелинейным становится чтение текстуры. Если при нулевом повороте мы фактически идем байт за байтом, то при повороте на 90 градусов чтение происходит с шагом в 256 байт. Возможно, на современных процессорах механизм кэша достаточно крутой, чтобы обработать подобную ситуацию? Но у нас 93-й год, и наш топовый ПК - это 486DX2.
Что делать? Куда бежать? Кому жаловаться? У этой проблемы есть несколько решений:
- Забить и пойти вайбкодить бухать с пацанами за гаражи.
- Сделать вторую копию текстуры, но заранее повернутую на 90 градусов, и переключаться на нее при приближении к 90 и 270 градусам, с поправкой Angle на 90. Это сгладит FPS в моменте и повысит среднестатистический.
- Разбить рендер на клеточки (тайлы) 8х8 или 16х16, так чтобы кадр рендерился "клеточка за клеточкой". Так промахи по кэшу снизятся и перепады FPS будут менее всего 10-15%.
Дополнительно, можно в зависимости от текущего угла поворота выбирать порядок отрисовки клеточек: рядами или столбцами, это тоже даст некоторый прирост скорости.

И да, конечно, степень масштабирования тоже влияет на промахи/попадания по кэшу. Чем больше "отзума", тем дела хуже. Если это принципиально, можно, конечно, и на этот случай создать доп.текстуры (этакий MIP-маппинг).
Что касается метода. Рендер тайлами-клеточками мне нравится больше. Он практичнее, особенно если вы хотите сделать текстуру динамической. А по скорости результаты примерно одинаковые.
И это всё, что мы можем?
Если мы попробуем запустить это на 386-м, даже очень быстром, при самом оптимистичном прогнозе мы получим 10-15 фпс. Но как же Second Reality и прочие? А что с Амигой? А кто-то воскликнет: я это на Спектруме видел!
Возможно теперь вы лучше понимаете, почему те старые демки вызывали такой восторг даже у обычных пользователей ПК, и какими крутыми были те парни, которые их делали.
Да, конечно, в отличие от нашего ротозумера в той же Second Reality разрешение в 2 раза меньше, всего 160х100 (да и на 386SX-25MHz она уже не вывозит, если честно). А в других демках еще меньше (либо они не полноэкранные). Но это все еще не полностью объясняет - как добиться приличной скорости на том же 286-м, или классической Амиге (с 7 Мгц)?
Есть еще один фокус: заранее посчитанные таблицы для 360 градусов поворота для внутреннего цикла. Получим две таблицы - одна для смещений отрисовки строки, другая - смещения для каждой новой строки. Здесь, конечно, мы ограничены в вариативности эффекта.
Пример такого кода для PC/XT 8086 можно посмотреть по ссылке: https://github.com/mills32/CUTE_DEMO-MS-DOS/blob/main/src/rotozoom.asm [ youtube ]
Хочешь еще быстрее? (я уже как ЧатЖПТ заговорил?)
Таблицы не нужны. Можно развернуть предварительно-рассчитанные смещения прямо в код (самомодификацией на лету, или в статичную простыню). Собственно, ничто нам не мешает сгенерить развернутый код хоть на весь сегмент. Получаем вместо цикла портянку вида:
mov al, ds:[bx+1452h] mov ah, ds:[bx+1454h] mov es:[di], ax mov al, ds:[bx+1457h] mov ah, ds:[bx+1458h] mov es:[di+2], ax .... mov al, ds:[bx+2456h] mov ah, ds:[bx+2458h] mov es:[di+240], ax
Ну или что-то вроде, в зависимости от архитектуры (набора инструкций, способа адресации).
Чтобы это по размеру данных не превращалось в покадровую анимацию, мы рассчитываем такую портянку только для одной строки (для конкретного угла/зума), но повторяем для отрисовки всего экрана, корректируя начальное смещение для начала следующей строки с помощью значения в регистре BX. Это скажется на качестве картинки, но если у вас все равно 160х100 или 80х50, то прямо скажем, сильно это не навредит :)
Подведем итоги
На самом деле я не собирался сильно углубляться в изыскания. Но не удержался и давай углубляться... В итоге я написал три варианта рутины: версия с двумя текстурами, версия рендера тайлами 16х16, и еще одна с тайлами, но предвычисленными смещениями для строки.
Попутно придумав еще несколько небольших хитростей для оптимизации.
Например, оказалось, что изменив порядок инструкций в цикле можно повысить скорость.
Медленее:
mov bl, ch mov bh, dh mov al, ds:[bx] add cx, si add dx, bp
Быстрее:
mov bl, ch mov bh, dh add cx, si add dx, bp mov al, ds:[bx]
Почему? Без понятия :) Вероятно также что-то связанное с кэшем. Ваши версии?
На 486DX2 66MHz с VLB-видеокартой получился такой средний FPS:
(при постоянном zoom-факторе 1:1 ):
Рендер тайлами 16х16: 108
Построчно (2 текстуры): 114
Тайлами с предвычислением: 168 (хуже качество)
С гуляющим зумом в пределах 0..1.7 - стабильные > 70 fps дают все три варианта.
Неплохо получилось, учитывая, что начинали мы с 2-х кадров в секунду.
Вот такой вот простенький классический демо-эффект. При этом, подозреваю, что я не затронул и половины всех аспектов и секретов, и вряд ли мой код самый быстрый (хотя я старался).
Ротозумер актуален и сегодня

Занятный факт, на демосцене последние годы существует этакий "ротозумер-челендж" на классической Амиге. Но не простых "ротозумеров", а с количеством цветов, превышающих "документированные" возможности "железа". Например, изменяя палитру для каждой строки кадровой развертки. Основная суть соревнования: кто сделает больше пикселей по горизонтали и вертикали (fps при этом, как вы понимаете, должен быть не меньше частоты кадровой развертки). К счастью Амига, в отличие от современного ей PC, имеет дополнительные чипы для работы с графикой, так что простора для кодерского творчества там тоже определенно больше.

Ну а если вам вдруг хочется посмотреть на то, что там у нас (у меня) получилось в рамках данной статьи (возможно у вас есть старенький 486-й? Или даже 386-й? Не стесняйтесь, проверяйте), возможно найдутся даже те, кто захочет посмотреть на исходный код (в рамках статьи я его не привожу, так как это заняло бы слишком много места).
Вот вам архив tar.gz в Base64:
Скрытый текст
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
Вот найдут инопланетяне эту статью, когда этот ваш github уже лет 150 как заблокирован не существует, а файлик-то на месте. Посмотрят, порадуются.
Используйте, например https://emn178.github.io/online-tools/base64_decode_file.html для декодирования строки в файл. Кстати, а Виндовс умеет распаковывать tar.gz?
На этом откланиваюсь, всем веселого кодирования! Пишите в комментариях - какие еще старые демо-эффекты вам нравятся, о каких вы хотели бы узнать больше?
Ну и... "Пешите демасцены!" (с)
Полезные ссылки:
Другие статьи схожей тематики на Хабре:
HAL в 4000 байт
Разработка демо для NES — HEOHdemo
Программирование под БК 0010 в 2019-ом году
Графика древности: палитры, часть 1 и (часть 2-я)
Демосценерские ресурсы (международные):
https://www.pouet.net/ - новые демо-релизы и основная тусовка западной сцены
https://www.demoparty.net/ (календарь мировых демопати)
https://demozoo.org/ - новости (жиденькие) и каталог всех релизов мировой сцены
https://www.youtube.com/@psenough/videos - еженедельные обзоры жизни демосцены
Комментарии (23)

pda0
16.02.2026 10:30Учитывает ли это процессор? Пентиум да. 486-й и более ранние - нет.
Точнее так: Pentium - да, 486 - нет, 386 - не умел в кеш и предвыборку. :) Проблемы были только на 486.

BiTL Автор
16.02.2026 10:30386-й не имел встроенной кэш-памяти, но она могла быть установлена на материнке (вместе с контроллером Intel82385), тогда механизмы кэша работали. У меня есть живой 386DX, и у него есть кэш на материнке, и prefetching работает.
Да и вроде как 80286 и даже 8086 умели, по такой же схеме.

vanxant
16.02.2026 10:30У 386 был "кэш инструкций" длиной 6 байт. Если вы модифицировали код в пределах 6 байт от pc, процессор этого не видел.
Cpuid тогда не было, и это был один из способов опреденения типа процессора

JerryI
16.02.2026 10:30Что ж, как нам поворачивать текстуру? Когда я был маленький (и бегал в валенках, с кудрявой головой) и пытался программировать графон на Бейсике, то думал, что поворачивать (или как-то модифицировать) нужно координаты точки (пикселя), которую мы рисуем. И да, в других ситуациях так и есть. Но не в случае с растровыми эффектами. Здесь мы должны мыслить как шейдер. Точки мы рисуем подряд, проходя весь экран строку за строкой. А поворачиваем мы точку (координаты точки), которую собираемся прочитать из текстуры.
золото. Сам неоднократно натыкался :)

alliumnsk
16.02.2026 10:30STOS работал медленее, чем MOV+ADD, потому что к этому времени разработчики процессоров перестали оптимизировать выполнение инструкций, которые редко использовались компиляторами. REP STOS исключение, потому что она использовалась в стандартных memset.

NutsUnderline
16.02.2026 10:30читал и думал "а когда же таблички синусов" а они прям даже не сразу :)

BiTL Автор
16.02.2026 10:30здесь синус и косинус вычисляются только один раз на кадр. Ну, пусть еще один SIN, если Scale сделать подвижным.
Это для 486 вообще пустяк. Даже исходя из того факта, что ТурбоПаскаль неумеет использовать инструкции 386/387 и не знает про fsin/fcos.
Для порядку я проверил, откомпилировал с поддержкой эмуляции FPU, отключив поддержку сопроцессора. Это сказалось на fps в пределах +-1 кадр/сек :)
Так что городить огород с синусными таблицами в рамках этой статьи я посчитал излишним.

MaximKharin
16.02.2026 10:30Спасибо за статью! В целом хотелось бы побольше разборов конкретных эффектов. Потому что вот хочется как-то вкатиться, написать интро/демо. Т.е. понятно, вот у нас есть видеопамять, есть оперативная память, есть ассемблер, чтобы всем этим управлять, а дальше что?
А с разбором понятно, можно и самому попробовать

NutsUnderline
16.02.2026 10:30ну так, "по детски", можно с другой стороны зайти, даже на бейсике: точечки случайные разноцветные, кружочки, надписи переливающися, перемещающиеся, тоже быстро возникнет вопрос "почему так медленно"

BiTL Автор
16.02.2026 10:30Спасибо что прочитали :) В меру сил и времени буду разбирать и другие демо-эффекты.
А пока можете мою предыдущую статью полистать, там хоть и не подробно, но концептуально объясняется принцип многих эффектов, на примере демки, которую мы в прошлом году выпустили.
RodionGork
нас не проведёшь :) радиатор был точно, хотя пропеллер более-менее опционально
а по существу - нам-то стар...рам вспомнить мило (и даже со слезой взглянуть в собственную папочку-копилку с подобными экспериментами) - но в современной реальности по-моему слово "ротозумер" и то проассоциируют с чем-то другим :)
BiTL Автор
Мой первый PC 486SX 66MHz был без радиатора и без сокета. На 386-х радиаторов тем более не было обычно (иногда их лепили, но скорее для понтов).
dlinyj
На моём 486dlc радиатора нет
vanxant
На моём амд дх40 радиатор был, хотя и весьма смешной
MaFrance351
А на чуть более мощных четвёрках. AMD уже даже была надпись "Heatsink required". А то и "Heatsink and fan required".
Cooler2
А мой первый был SX2-80 :-) Радиатор был точно, насчет кулера - не помню.