ОПТИМИЗАЦИЯ
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++;
}


 в самое начало


demo.design
3D programming FAQ