Здравствуйте, меня зовут 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. Но в итоге "Вторая реальность" стала хитом и нетленкой на все времена, так что говоря про "ротозумеры" обычно приводят в пример эффект с этой мордой:

Впрочем заслуженно, ротозумер в Second Reality был менее требователен к "железу".
Впрочем заслуженно, ротозумер в Second Reality был менее требователен к "железу".

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

В том же 1993 году, но в декабре, данный эффект продемонстрировали демки legend от Impact Studios  и Assembler Instinct от Gollum.

            1. https://youtu.be/5o1bIjFmeG4?t=77      2. https://youtu.be/af8W0WP9HSY?t=212      3.https://youtu.be/yaNsT31hQQw?t=105       4.https://youtu.be/qIRCeFqzJds?t=38                       5.https://youtu.be/NVytBkoUhXs?t=109         6.https://youtu.be/r7Vq32eL3d4?t=46

А потом понеслось. Ротозумеры стали одним из тех эффектов, которые делала каждая собака. Уважающие себя собаки демокодеры пытались привнести в него какие-то новшества, прокачать эффект каким-либо образом. Например, поиграться палитровой анимацией, добавить искажений, совместить с другим классическим эффектом.
Другие просто вставляли в свою интру/демку этот эффект и свою картинку, вероятно даже не разбираясь с кодом (исходники с незатейливой реализацией часто встречались на развалах 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 кадров/сек. Немного, но прибавили. Однако тут есть подвох...

Но сначала признаюсь. Мы здесь скакнули на два шага вперед. Если присмотреться к коду, то можно увидеть, что теперь мы рисуем кадр задом наперед. То есть, из текстуры мы читаем так же как и прежде, но на экран рисуем пиксели в обратном порядке, начиная от нижнего правого угла. Зачем?

Здесь надо понимать, что на экране порядок станет обратным: 4 3 2 1
Здесь надо понимать, что на экране порядок станет обратным: 4 3 2 1

Потому что так мы получаем правильный, для вывода на экран, порядок байтов в регистре 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-х кадров в секунду.

Вот такой вот простенький классический демо-эффект. При этом, подозреваю, что я не затронул и половины всех аспектов и секретов, и вряд ли мой код самый быстрый (хотя я старался).

Ротозумер актуален и сегодня

1. https://youtu.be/uUG2sPulZJ8?t=43    2. https://youtu.be/wJ2JBcDae3k?t=1                                             3.https://youtu.be/-P02eczb58U?t=168      4. https://youtu.be/POZNZ7XXiTk?t=33              5.https://youtu.be/9iz0Lyp6ARc?t=79     6. https://youtu.be/liuo9EzHprQ?t=6

Занятный факт, на демосцене последние годы существует этакий "ротозумер-челендж" на классической Амиге. Но не простых "ротозумеров", а с количеством цветов, превышающих "документированные" возможности "железа". Например, изменяя палитру для каждой строки кадровой развертки. Основная суть соревнования: кто сделает больше пикселей по горизонтали и вертикали (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 - еженедельные обзоры жизни демосцены