- PVSM.RU - https://www.pvsm.ru -
В коде id Software порой встречаются бесподобные жемчужины. Самая знаменитая — это, конечно, 0x5f3759df [1], удостоившаяся даже комикса на xkcd [2]. Здесь же речь пойдёт о заливке экрана: пиксели закрашиваются по одному в случайном порядке, без повторов. Как это сделано?
Лобовым решением было бы использовать генератор случайных чисел и перед закраской каждого пикселя проверять, не закрашен ли он до сих пор. Это было бы крайне неэффективно: к тому моменту, когда на экране остаётся последний незакрашенный пиксель, потребуется порядка 320×200 вызовов ГСЧ, чтобы в него «попасть». (Вспомним, что Wolfenstein 3D работал на 286 [3] с частотой 12МГц!) Другим лобовым решением было бы составить список из всех 320×200 возможных координат, перетасовать его (можно даже заранее, и вставить в код уже перетасованным), и закрашивать пиксели по списку; но для этого понадобилось бы как минимум 320×200×2 = 125КБ памяти — пятая часть всей памяти компьютера! (Помните ведь, что 640КБ должно было хватить любому [4]?)
Вот как [5] на самом деле реализована эта попиксельная заливка: (код немного упрощён по сравнению с оригиналом)
boolean FizzleFade(unsigned width, unsigned height)
{
unsigned x, y;
long rndval = 1;
do // while (1)
{
// seperate random value into x/y pair
asm mov ax, [WORD PTR rndval]
asm mov dx, [WORD PTR rndval+2]
asm mov bx, ax
asm dec bl
asm mov [BYTE PTR y], bl // low 8 bits - 1 = y xoordinate
asm mov bx, ax
asm mov cx, dx
asm mov [BYTE PTR x], ah // next 9 bits = x xoordinate
asm mov [BYTE PTR x+1], dl
// advance to next random element
asm shr dx, 1
asm rcr ax, 1
asm jnc noxor
asm xor dx, 0x0001
asm xor ax, 0x2000
noxor:
asm mov [WORD PTR rndval], ax
asm mov [WORD PTR rndval+2], dx
if (x>width || y>height)
continue;
// copy one pixel into (x, y)
if (rndval == 1) // entire sequence has been completed
return false ;
} while (1);
}
Что за чертовщина здесь происходит?
Для тех, кому тяжело разбираться в ассемблерном коде для 8086 — вот эквивалентный код на чистом Си:
void FizzleFade(unsigned width, unsigned height)
{
unsigned x, y;
long rndval = 1;
do {
y = (rndval & 0x000FF) - 1; // младшие 8 бит - 1 = координата y
x = (rndval & 0x1FF00) >> 8; // следующие 9 bits = координата x
unsigned lsb = rndval & 1; // младший бит потеряется при сдвиге
rndval >>= 1;
if (lsb) // если выдвинутый бит = 0, то не надо xor
rndval ^= 0x00012000;
if (x<=width && y<=height)
copy_pixel_into(x, y);
} while (rndval != 1);
}
Проще говоря:
rndval
начинается с 1;rndval
на координаты x и y, и закрашиваем пиксель с этими координатами;rndval
с непонятной «магической константой»;rndval
каким-то образом снова примет значение 1 — готово, весь экран залит.Эта «магия» называется регистр сдвига с линейной обратной связью [6]: для генерации каждого следующего значения мы выдвигаем один бит вправо, и вдвигаем слева новый бит, который получается как результат xor. Например, четырёхбитный РСЛОС с «отводами» на нулевом и втором битах, xor которых задаёт вдвигаемый слева бит, выглядит так:
Если взять начальное значение 1, то этот РСЛОС генерирует пять других значений, а потом зацикливается: 0001 → 1000 → 0100 → 1010 → 0101 → 0010 → 0001 (подчёркнуты биты, используемые для xor). Если взять начальное значение, не участвующее в этом цикле, то получится другой цикл: например, 0011 → 1001 → 1100 → 1110 → 1111 → 0111 → 0011. Легко проверить, что три четырёхбитных значения, не попавшие ни в один из этих двух циклов, образуют третий цикл. Нулевое значение всегда переходит само в себя, поэтому оно не рассматривается в числе возможных.
А можно ли подобрать отводы РСЛОС так, чтобы все возможные значения образовали один большой цикл? Да, если в поле по модулю 2 разложить на множители многочлен xN+1, где N=2m–1 — длина получающегося цикла. Опуская хардкорную математику [7], покажем, как четырёхбитный РСЛОС с отводами на нулевом и первом битах будет поочерёдно принимать все 15 возможных значений:
Wolfenstein 3D использует 17-битный РСЛОС с отводами на нулевом и третьем битах. Этот РСЛОС можно было бы реализовать «в лоб» за семь логических операций:
unsigned new_bit = (rndval & 1) ^ (rndval>>3 & 1);
rndval >>= 1;
rndval |= (new_bit << 16);
Такая реализация называется «конфигурацией Фибоначчи». Равнозначная ей «конфигурация Галуа» позволяет выполнить всё то же самое за три логические операции:
a → b → c → d → e → f → g → h → i → j → k → l → m → n → o → p → q ↑ Фибоначчи ↓ ↓ ·← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ←^← ← ← ← ← ←· o → p → q→^→a → b → c → d → e → f → g → h → i → j → k → l → m → n ↑ ↑ Галуа ↓ ·← ← ← ← ←·← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ←·
Если заметить, что старший бит после сдвига гарантированно нулевой, то копирование и xor можно объединить в одну операцию:
unsigned lsb = rndval & 1;
rndval >>= 1;
if (lsb)
rndval ^= 0x00012000;
— что мы и видим в коде Wolfenstein 3D.
Значение в «конфигурации Галуа» по сравнению с «конфигурацией Фибоначчи» циклически сдвинуто на три бита: начальному значению 1 в конфигурации Галуа (используемому в коде Wolfenstein 3D) соответствует начальное значение 8 в конфигурации Фибоначчи. Вторым значением РСЛОС будет 0x12000, соответствующее 0x10004 в конфигурации Фибоначчи, и так далее. Если одна из последовательностей принимает все возможные (ненулевые) значения — значит, и вторая тоже принимает все эти значения, хоть и в другом порядке. Из-за того, что нулевое значение для РСЛОС недостижимо, из значения координаты y в коде вычитается единица — иначе пиксель (0,0) так никогда бы и не закрасился.
В заключение автор оригинальной статьи упоминает, что из 217–1 = 131071 значений, генерируемых этим РСЛОС, используются только 320×200 = 64000, т.е. чуть меньше половины; каждое второе генерируемое значение отбрасывается, т.е. генератор работает в половину доступной скорости. Для нужд Wolfenstein 3D хватило бы и 16-битного РСЛОС, и тогда не пришлось бы заморачиваться с обработкой 32-битного rndval
на 16-битном процессоре. Автор предполагает, что программисты id Software просто не смогли найти подходящую комбинацию «отводов», которая давала бы максимальный 16-битный цикл. Мне же кажется, что он сильно их недооценивает :-) Дело в том, что для разделения 16-битного значения на координаты x и y его пришлось бы делить с остатком на 320 или на 200, и затраты на такую операцию деления с лихвой бы скомпенсировали всё ускорение от перехода на 16-битный РСЛОС.
Автор: tyomitch
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/263213
Ссылки в тексте:
[1] 0x5f3759df: https://habrahabr.ru/company/infopulse/blog/336110/
[2] комикса на xkcd: https://xkcd.com/664/
[3] 286: https://ru.wikipedia.org/wiki/80286
[4] 640КБ должно было хватить любому: https://quoteinvestigator.com/2011/09/08/640k-enough/
[5] Вот как: https://github.com/id-Software/wolf3d/blob/05167784ef009d0d0daefe8d012b027f39dc8541/WOLFSRC/ID_VH.C#L471
[6] регистр сдвига с линейной обратной связью: https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B3%D0%B8%D1%81%D1%82%D1%80_%D1%81%D0%B4%D0%B2%D0%B8%D0%B3%D0%B0_%D1%81_%D0%BB%D0%B8%D0%BD%D0%B5%D0%B9%D0%BD%D0%BE%D0%B9_%D0%BE%D0%B1%D1%80%D0%B0%D1%82%D0%BD%D0%BE%D0%B9_%D1%81%D0%B2%D1%8F%D0%B7%D1%8C%D1%8E
[7] хардкорную математику: http://www.newwaveinstruments.com/resources/articles/m_sequence_linear_feedback_shift_register_lfsr.htm#Galois%20Field%20Mathematics%20and%20M-Sequences
[8] Источник: https://habrahabr.ru/post/337036/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.