Тайловые текстуры
В пункте кратко описана схема работы кэш-памяти для процессоров 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++; }
1 2 3 4 5
8 8 8
|