ОПТИМИЗАЦИЯ
6.4. Тайловые текстуры
В пункте 6.1.2. кратко описана схема работы кэш-памяти для процессоров Intel
Pentium. Из этой схемы, в частности, видно, что при непоследовательном
чтении из памяти будут периодически случаться кэш-промахи, что не очень
хорошо влияет на скорость. Хрестоматийный пример - это поворачивающаяся
картинка; при угле поворота, равном 0, чтение из памяти последовательно и
ситуация с кэшированием идеальна; если мы читаем байт и получаем кэш-промах,
то следующий за ним 31 байт будет прочитан уже из L1 cache, по полтакта на
чтение. А при достаточно больших углах поворота, например, 90 градусов,
каждый следующий байт находится на достаточном расстоянии от предыдущего,
и получаем кэш-промах практически на каждом пикселе, что *очень* медленно.
Но эта же ситуация постоянно случается и при текстурировании, грани ведь у
нас ориентированы произвольным образом, камера - тоже. Тайловые текстуры как
раз и призваны бороться с кэш-промахами.
Идея такова. Обычно текстура хранится в памяти построчно, именно из-за этого
при движении вдоль строки все нормально, а при движении поперек строк будут
постоянные кэш-промахи (кэшируется ведь небольшой горизонтальный кусочек).
Разобьем ее на маленькие кусочки - тайлы, и будем хранить такими кусочками.
Вот пример для текстуры размера 256x256 и тайла размера 8x8:
Текстура в пикселах:
0, 1, 2, 3, ..., 255
256, 257, 258, 259, ..., 511
512, 512, 513, 514, ..., 767
...
Текстура в тайлах:
0, 1, 2, 3, ..., 31 (первые восемь строк пикселов)
32, 33, 34, 35, ..., 63 (вторые восемь строк пикселов)
64, 65, 67, 68, ..., 95
...
Тайл 0 в пикселах: Тайл 1 в пикселах:
0, 1, ..., 7 8, 9, ..., 15
256, 257, ..., 263 264, 265, ..., 271
512, 513, ..., 519 520, 521, ..., 527
... ...
1792, 1793, ..., 1799 1800, 1801, ..., 1807
В этом случае все близкие к текущему текселы почти наверняка находятся в
текущем тайле, и количество кэш-промахов хоть как-то, да уменьшается. То
есть, тайлы как бы позволяют двигаться в текстуре и по горизонтали, и по
вертикали. Зато изменяется код расчета смещения нужного пиксела в текстуре.
Посмотрим, что получится для случая на иллюстрации. Пусть координаты в
текстуре (то есть, их целые части) равны u, v; тогда номер нужного тайла
равен (v / 8) * 32 + (u / 8), а координаты в тайле равны (u % 8), (v % 8)
соответственно. Тут помогает то, что 8 - степень двойки, получается, что
номер и координаты в тайле можно посчитать и проще, а по ним находим и
смещение в текстуре:
tile_number = ((v >> 3) << 5) + (u >> 3);
tile_u = u & 0x07;
tile_v = v & 0x07;
texture_offset = (tile_number << 6) + (tile_v << 3) + tile_u;
Напишем эти формулы и для общего случая, то есть для текстуры размером
(2^TEXBITS)x(2^TEXBITS) и тайла размером (2^TILEBITS)x(2^TILEBITS):
TILEMASK = ((1 << TILEBITS) - 1);
tile_number = ((v >> TILEBITS) << (TEXBITS - TILEBITS)) +
(u >> TILEBITS);
tile_u = u & TILEMASK;
tile_v = v & TILEMASK;
texture_offset = (tile_number << (2*TILEBITS)) +
(tile_v << TILEBITS) + tile_u;
Делать такое преобразование для каждого пиксела текстуры - занятие довольно
небыстрое. Поэтому начинаем заниматься оптимизацией. Выделяем части смещения,
зависящие от целых частей u, v соответственно:
tile_u_part = ((u >> TILEBITS) << (2*TILEBITS)) +
(u & TILEMASK);
tile_v_part = ((v >> TILEBITS) << (TEXBITS + TILEBITS) +
((v & TILEMASK) << TILEBITS);
texture_offset = tile_u_part + tile_v_part;
Отсюда видно, что биты целой части u, v разделяются на две группы (нижние
TILEBITS и все остальные) и эти две группы как-то раскидываются, сдвигаются.
Посмотрим, как именно это происходит для конкретного случая, где u, v - 8:16
fixedpoint, TILEBITS = 3, TEXBITS = 8:
u 00000000 UUUUUuuu ffffffff ffffffff
v 00000000 VVVVVvvv ffffffff ffffffff
tile_u_part 00000UUU UU000uuu ffffffff ffffffff
tile_v_part VVVVV000 00vvv000 ffffffff ffffffff
Идея быстрого тайлового текстурирования заключается как раз в интерполяции
непосредственно tile_u_part и tile_v_part, а не u, v; мы заранее переставляем
биты u, v, du, dv нужным образом и интерполируем уже готовые к использованию
с тайловыми текстурами величины tile_u_part, tile_v_part. Но для того, чтобы
сложение давало правильный результат, "дырки" между кусками целой части и
дробной частью u, v в tile_u_part, tile_v_part надо перед каждым сложением
заполнять единицами; иначе, скажем, целая единица, получившаяся при сложении
v и dv уйдет в нижний бит целой части tile_v_part и вместо перехода на пиксел
вниз вызовет переход на пиксел вправо. Поэтому все должно выглядеть так:
u 00000000 UUUUUuuu ffffffff ffffffff
v 00000000 VVVVVvvv ffffffff ffffffff
tile_u_part 00000UUU UU111uuu ffffffff ffffffff
tile_v_part VVVVV111 11vvv111 ffffffff ffffffff
Теперь переносы при сложении будут обрабатываться правильно, при переносе все
эти единички обнуляются, а переносимый бит добавляется туда, куда надо. Зато
теперь не будет работать сложение, не вызывающее переноса - в этом случае
единички останутся на месте и испортят все смещение. Получается, что перед
сложением нужно выставлять нужные биты в единичку, а после сложения их же и
очищать. Соответствующий цикл будет выглядеть так:
// ...
u = make_tile_u(u);
v = make_tile_v(v);
du = make_tile_u(du);
dv = make_tile_v(dv);
for (current_sx = x_start; current_sx <= x_end; current_sx++) {
putpixel(current_sx, current_sy, texture[unfix(u) + unfix(v)];
u |= TILE_U_MASK;
v |= TILE_V_MASK;
u += du;
v += dv;
u &= (~TILE_U_MASK);
v &= (~TILE_U_MASK);
}
// ...
Здесь make_tile_u(), make_tile_v() осуществляет перевод u, v в "тайловую"
форму; unfix() просто сдвигает u, v на собственную дробную часть, оставляя
лишь целую, TILE_U_MASK, TILE_V_MASK заполняют нужные биты числа единичками.
В нашем примере видно, что
TILE_U_MASK = 0x380000; // 00000000 00111000 00000000 00000000
TILE_V_MASK = 0x7C3000; // 00000111 11000111 00000000 00000000
По сравнению с обычным текстурированием добавилось более четырех инструкций.
Много. Смотрим дальше. С той же самой целью - заставить биты "перепрыгивать
дырки" при сложении - можно не заполнять дырки единичками в u, v для каждой
точки, а сделать это один раз для du, dv. Кроме того, unfix() можно делать
один раз, а не два, заменив (unfix(u) + unfix(v)) на unfix(u + v). Но здесь
надо проследить за тем, чтобы дробные части u, v при сложении не вызвали бы
переноса и не испортили смещение на единичку. Достигается это использованием
fixedpoint 8:15 и вставкой одного запасного бита между целой и дробной частью
u, v. Т.о., битовые раскладки для нашего примера теперь выглядят вот так:
tile_u_part 00000UUU UU000uuu 0fffffff ffffffff
tile_v_part VVVVV000 00vvv000 0fffffff ffffffff
tile_du 00000UUU UU111uuu 1fffffff ffffffff
tile_dv VVVVV111 11vvv111 1fffffff ffffffff
TILE_U_MASK 00000000 00111000 10000000 00000000
TILE_V_MASK 00000111 11000111 10000000 00000000
А окончательная версия цикла выглядит вот так:
// ...
u = make_tile_u(u);
v = make_tile_v(v);
du = make_tile_u(du) | TILE_U_MASK;
dv = make_tile_v(dv) | TILE_V_MASK;
for (current_sx = x_start; current_sx <= x_end; current_sx++) {
putpixel(current_sx, current_sy, texture[unfix(u + v)];
u += du;
v += dv;
u &= (~TILE_U_MASK);
v &= (~TILE_V_MASK);
}
// ...
В переводе на ассемблер, заимствованном из все того же fatmap2.txt, все это
выглядит вот так:
mov eax,tile_u_part
mov ebx,tile_v_part
mov ecx,length
mov esi,texture
mov edi,outputbuffer
lea edi,[edi+ecx-1]
xor ecx,-1
lea ebp,[eax+ebx]
inc ecx
inner:
add eax,tile_du
add ebx,tile_dv
shr ebp,16
and eax,11111111110001110111111111111111b ; (~TILE_U_MASK)
and ebx,11111000001110000111111111111111b ; (~TILE_V_MASK)
inc ecx
mov dl,[esi+ebp]
lea ebp,[eax+ebx]
mov [edi+ecx],dl
jnz inner
Осталось упомянуть про то, что тайлы можно расположить не по горизонтали,
а по вертикали:
Текстура в тайлах:
0, 32, 64, ...
1, 33, 65, ...
2, 34, 66, ...
...
Тогда преобразование для целых частей u, v выглядит следующим образом:
tile_number = ((u >> TILEBITS) << (TEXBITS - TILEBITS)) +
(v >> TILEBITS);
tile_u = u & TILEMASK;
tile_v = v & TILEMASK;
texture_offset = (tile_number << (2*TILEBITS)) +
(tile_v << TILEBITS) + tile_u;
Выделяя u, v, получаем:
tile_u_part = ((u >> TILEBITS) << (TEXBITS + TILEBITS) +
(u & TILEMASK);
tile_v_part = ((v >> TILEBITS) << TILEBITS) +
((v & TILEMASK) << TILEBITS);
то есть
tile_u_part = ((u >> TILEBITS) << (TEXBITS + TILEBITS) +
(u & TILEMASK);
tile_v_part = v << TILEBITS;
Все это делается для единственной цели - скорости. В таком виде перевод u, v
в "тайловые координаты" делается немного проще и быстрее; внутрениий цикл же
остается таким же (ну, константы (~TILE_U_MASK) и (~TILE_V_MASK), конечно,
поменять придется, но это непринципиально). Здесь битовая раскладка выглядит
следующим образом:
tile_u_part UUUUU000 00000uuu 0fffffff ffffffff
tile_v_part 00000VVV VVVVV000 0fffffff ffffffff
tile_du UUUUU111 11111uuu 1fffffff ffffffff
tile_dv 00000VVV VVVVV111 1fffffff ffffffff
TILE_U_MASK 00000111 11111000 10000000 00000000
TILE_V_MASK 00000000 00000111 10000000 00000000
Полные формулы (функции) для перевода из 16:16 fixedpoint в "тайловый" 8:15
fixedpoint приведем именно для этого случая; выглядят они вот так:
int make_tile_u(int u) {
return
((u << 8) & 0xF8000000) +
(u & 0x70000) +
((u >> 1) & 0x7FFF);
}
int make_tile_v(int u) {
return
((v << 3) & 0x7F80000) +
((v >> 1) & 0x7FFF);
}
Для полной комплектности осталось только привести кусочек кода для перевода
обычной текстуры в тайловую форму (для случая текстуры 256x256, тайла 8x8,
"вертикального" расположения тайлов).
void tile_texture(char *dst, char *src) {
int u, v;
for (v = 0; v < 256; v++)
for (u = 0; u < 256; u++)
dst[((u << 8) & 0xF800) + (u & 0x7) +
((v << 3) & 0x7F8)] = *src++;
}