Оптимизация SSE2 для преобразования из RGB565 в RGB888 (без альфа-канала)

Я пытаюсь преобразовать буфер битов,
от 16 бит на пиксель:

RGB 565: rrrrrggggggbbbb|rrr..

до 24 бит на пиксель:

RGB888 rrrrrrrrgggggggbbbbbbb|rrr...

У меня довольно оптимизированный алгоритм, но мне любопытно, как это можно сделать с помощью SSE. Кажется хорошим кандидатом. Предположим, что входные данные представляют собой набор из 16 бит на пиксель, выровненный по памяти и размером 64х64 пикселя, так как он идеально подходит, поэтому буфер 64 * 64 * 16 и его необходимо преобразовать в буфер 64 * 64 *. 24.

Если загрузить начальный буфер цветов (16 бит / с) в реестр __m128i (и затем выполнить итерацию), я могу обработать 8 пикселей каждый раз. Если использовать маски и смены, я могу извлечь каждый компонент в разных реестрах (псевдокод):

eg r_c:

Input buffer c565
Ouput buffer c888

__m128i* ptr = (__m128i*)c565; // Original byte buffer rgb565
__m128i r_mask_16 = _mm_set_epi8(0xF8, 0, 0xF8...);
__m128i r_c = _mm_and_si128(*ptr, r_mask_16);

result:

__m128i r_c = [r0|0|r1|0|....r7|0]
__m128i g_c = [g0|0|g1|0|....g7|0]
__m128i b_c = [b0|0|r1|0|....b7|0]

But if I extract them manually it loses all its performance:

c888[0] = r_c[0];
c888[1] = g_c[0];
c888[2] = b_c[0];
c888[3] = r_c[1];
...

Я полагаю, что правильный способ должен состоять в том, чтобы объединить их в другом реестре и сохранить его на c888 напрямую, не делая каждый компонент отдельно. Но не уверен, как я могу сделать это эффективно, есть мысли?

Замечания: Этот вопрос не является дубликатом Оптимизация преобразований RGB565 в RGB888 с SSE2. Преобразование из RGB565 в ARGB8888 это не то же самое, что преобразование RGB565 в RGB888. Приведенный выше вопрос использует инструкции,

punpcklbw
punpckhbw

и эти инструкции хорошо работают, когда есть пары (xmm (rb) xmm (ga) xmm (rgba) x2), так как они берут RB из одного регистра xmm и GA из другого и упаковывают их в два xmms. Но случай, который я раскрываю, когда вы не хотите альфа-компонент.

3

Решение

К сожалению, у SSE нет хорошего способа выписать упакованные 24-битные целые числа, поэтому нам нужно упаковать данные пикселей самостоятельно.

Пиксели размером 24 бит на пиксель занимают 3 байта на пиксель, но регистр XMM составляет 16 байтов, что означает, что нам нужно обрабатывать 3 * 16 пикселей = 48 байтов за раз, чтобы не беспокоиться о сохранении только части регистра XMM.

Сначала нам нужно загрузить вектор данных 16 бит на пиксель, а затем преобразовать его в пару векторов данных 32 бит на пиксел. Я сделал это, распаковав данные в вектор uint32, затем сдвинув и замаскировав этот вектор, чтобы извлечь красный, зеленый и синий каналы. ИЛИ это вместе — последний шаг в переводе на 32bpp. Это может быть заменено кодом из связанного вопроса, если это быстрее, я не измерял производительность своего решения.

После того как мы преобразовали 16 пикселей в векторы размером 32 бит на пиксель, эти векторы должны быть упакованы вместе и записаны в массив результатов. Я решил маскировать каждый пиксель индивидуально и использовать _mm_bsrli_si128 а также _mm_bslli_si128 переместить его в конечную позицию в каждом из трех векторов результата. ИЛИ, объединяя каждый из этих пикселей снова, дает упакованные данные, которые записываются в массив результатов.

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

Это записывает данные 24bpp с красным каналом как MSB.

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <x86intrin.h>

#define SSE_ALIGN 16

int main(int argc, char *argv[]) {
// Create a small test buffer
// We process 16 pixels at a time, so size must be a multiple of 16
size_t buf_size = 64;
uint16_t *rgb565buf = aligned_alloc(SSE_ALIGN, buf_size * sizeof(uint16_t));

// Fill it with recognizable data
for (size_t i = 0; i < buf_size; i++) {
uint8_t r = 0x1F & (i + 10);
uint8_t g = 0x3F & i;
uint8_t b = 0x1F & (i + 20);
rgb565buf[i] = (r << 11) | (g << 5) | b;
}

// Create a buffer to hold the data after translation to 24bpp
uint8_t *rgb888buf = aligned_alloc(SSE_ALIGN, buf_size * 3*sizeof(uint8_t));

// Masks for extracting RGB channels
const __m128i mask_r = _mm_set1_epi32(0x00F80000);
const __m128i mask_g = _mm_set1_epi32(0x0000FC00);
const __m128i mask_b = _mm_set1_epi32(0x000000F8);

// Masks for extracting 24bpp pixels for the first 128b write
const __m128i mask_0_1st  = _mm_set_epi32(0,          0,          0,          0x00FFFFFF);
const __m128i mask_0_2nd  = _mm_set_epi32(0,          0,          0x0000FFFF, 0xFF000000);
const __m128i mask_0_3rd  = _mm_set_epi32(0,          0x000000FF, 0xFFFF0000, 0         );
const __m128i mask_0_4th  = _mm_set_epi32(0,          0xFFFFFF00, 0,          0         );
const __m128i mask_0_5th  = _mm_set_epi32(0x00FFFFFF, 0,          0,          0         );
const __m128i mask_0_6th  = _mm_set_epi32(0xFF000000, 0,          0,          0         );
// Masks for the second write
const __m128i mask_1_6th  = _mm_set_epi32(0,          0,          0,          0x0000FFFF);
const __m128i mask_1_7th  = _mm_set_epi32(0,          0,          0x000000FF, 0xFFFF0000);
const __m128i mask_1_8th  = _mm_set_epi32(0,          0,          0xFFFFFF00, 0         );
const __m128i mask_1_9th  = _mm_set_epi32(0,          0x00FFFFFF, 0,          0         );
const __m128i mask_1_10th = _mm_set_epi32(0x0000FFFF, 0xFF000000, 0,          0         );
const __m128i mask_1_11th = _mm_set_epi32(0xFFFF0000, 0,          0,          0         );
// Masks for the third write
const __m128i mask_2_11th = _mm_set_epi32(0,          0,          0,          0x000000FF);
const __m128i mask_2_12th = _mm_set_epi32(0,          0,          0,          0xFFFFFF00);
const __m128i mask_2_13th = _mm_set_epi32(0,          0,          0x00FFFFFF, 0         );
const __m128i mask_2_14th = _mm_set_epi32(0,          0x0000FFFF, 0xFF000000, 0         );
const __m128i mask_2_15th = _mm_set_epi32(0x000000FF, 0xFFFF0000, 0,          0         );
const __m128i mask_2_16th = _mm_set_epi32(0xFFFFFF00, 0,          0,          0         );

// Convert the RGB565 data into RGB888 data
__m128i *packed_rgb888_buf = (__m128i*)rgb888buf;
for (size_t i = 0; i < buf_size; i += 16) {
// Need to do 16 pixels at a time -> least number of 24bpp pixels that fit evenly in XMM register
__m128i rgb565pix0_raw = _mm_load_si128((__m128i *)(&rgb565buf[i]));
__m128i rgb565pix1_raw = _mm_load_si128((__m128i *)(&rgb565buf[i+8]));

// Extend the 16b ints to 32b ints
__m128i rgb565pix0lo_32b = _mm_unpacklo_epi16(rgb565pix0_raw, _mm_setzero_si128());
__m128i rgb565pix0hi_32b = _mm_unpackhi_epi16(rgb565pix0_raw, _mm_setzero_si128());
// Shift each color channel into the correct position and mask off the other bits
__m128i rgb888pix0lo_r = _mm_and_si128(mask_r, _mm_slli_epi32(rgb565pix0lo_32b, 8)); // Block 0 low pixels
__m128i rgb888pix0lo_g = _mm_and_si128(mask_g, _mm_slli_epi32(rgb565pix0lo_32b, 5));
__m128i rgb888pix0lo_b = _mm_and_si128(mask_b, _mm_slli_epi32(rgb565pix0lo_32b, 3));
__m128i rgb888pix0hi_r = _mm_and_si128(mask_r, _mm_slli_epi32(rgb565pix0hi_32b, 8)); // Block 0 high pixels
__m128i rgb888pix0hi_g = _mm_and_si128(mask_g, _mm_slli_epi32(rgb565pix0hi_32b, 5));
__m128i rgb888pix0hi_b = _mm_and_si128(mask_b, _mm_slli_epi32(rgb565pix0hi_32b, 3));
// Combine each color channel into a single vector of four 32bpp pixels
__m128i rgb888pix0lo_32b = _mm_or_si128(rgb888pix0lo_r, _mm_or_si128(rgb888pix0lo_g, rgb888pix0lo_b));
__m128i rgb888pix0hi_32b = _mm_or_si128(rgb888pix0hi_r, _mm_or_si128(rgb888pix0hi_g, rgb888pix0hi_b));

// Same thing as above for the next block of pixels
__m128i rgb565pix1lo_32b = _mm_unpacklo_epi16(rgb565pix1_raw, _mm_setzero_si128());
__m128i rgb565pix1hi_32b = _mm_unpackhi_epi16(rgb565pix1_raw, _mm_setzero_si128());
__m128i rgb888pix1lo_r = _mm_and_si128(mask_r, _mm_slli_epi32(rgb565pix1lo_32b, 8)); // Block 1 low pixels
__m128i rgb888pix1lo_g = _mm_and_si128(mask_g, _mm_slli_epi32(rgb565pix1lo_32b, 5));
__m128i rgb888pix1lo_b = _mm_and_si128(mask_b, _mm_slli_epi32(rgb565pix1lo_32b, 3));
__m128i rgb888pix1hi_r = _mm_and_si128(mask_r, _mm_slli_epi32(rgb565pix1hi_32b, 8)); // Block 1 high pixels
__m128i rgb888pix1hi_g = _mm_and_si128(mask_g, _mm_slli_epi32(rgb565pix1hi_32b, 5));
__m128i rgb888pix1hi_b = _mm_and_si128(mask_b, _mm_slli_epi32(rgb565pix1hi_32b, 3));
__m128i rgb888pix1lo_32b = _mm_or_si128(rgb888pix1lo_r, _mm_or_si128(rgb888pix1lo_g, rgb888pix1lo_b));
__m128i rgb888pix1hi_32b = _mm_or_si128(rgb888pix1hi_r, _mm_or_si128(rgb888pix1hi_g, rgb888pix1hi_b));

// At this point, rgb888pix_32b contains the pixel data in 32bpp format, need to compress it to 24bpp
// Use the _mm_bs*li_si128(__m128i, int) intrinsic to shift each 24bpp pixel into it's final position
// ...then mask off the other pixels and combine the result together with or
__m128i pix_0_1st = _mm_and_si128(mask_0_1st,                 rgb888pix0lo_32b     ); // First 4 pixels
__m128i pix_0_2nd = _mm_and_si128(mask_0_2nd, _mm_bsrli_si128(rgb888pix0lo_32b, 1 ));
__m128i pix_0_3rd = _mm_and_si128(mask_0_3rd, _mm_bsrli_si128(rgb888pix0lo_32b, 2 ));
__m128i pix_0_4th = _mm_and_si128(mask_0_4th, _mm_bsrli_si128(rgb888pix0lo_32b, 3 ));
__m128i pix_0_5th = _mm_and_si128(mask_0_5th, _mm_bslli_si128(rgb888pix0hi_32b, 12)); // Second 4 pixels
__m128i pix_0_6th = _mm_and_si128(mask_0_6th, _mm_bslli_si128(rgb888pix0hi_32b, 11));
// Combine each piece of 24bpp pixel data into a single 128b variable
__m128i pix128_0 = _mm_or_si128(_mm_or_si128(_mm_or_si128(pix_0_1st, pix_0_2nd), pix_0_3rd),
_mm_or_si128(_mm_or_si128(pix_0_4th, pix_0_5th), pix_0_6th));
_mm_store_si128(packed_rgb888_buf, pix128_0);

// Repeat the same for the second 128b write
__m128i pix_1_6th  = _mm_and_si128(mask_1_6th,  _mm_bsrli_si128(rgb888pix0hi_32b, 5 ));
__m128i pix_1_7th  = _mm_and_si128(mask_1_7th,  _mm_bsrli_si128(rgb888pix0hi_32b, 6 ));
__m128i pix_1_8th  = _mm_and_si128(mask_1_8th,  _mm_bsrli_si128(rgb888pix0hi_32b, 7 ));
__m128i pix_1_9th  = _mm_and_si128(mask_1_9th,  _mm_bslli_si128(rgb888pix1lo_32b, 8 )); // Third 4 pixels
__m128i pix_1_10th = _mm_and_si128(mask_1_10th, _mm_bslli_si128(rgb888pix1lo_32b, 7 ));
__m128i pix_1_11th = _mm_and_si128(mask_1_11th, _mm_bslli_si128(rgb888pix1lo_32b, 6 ));
__m128i pix128_1 = _mm_or_si128(_mm_or_si128(_mm_or_si128(pix_1_6th, pix_1_7th),  pix_1_8th ),
_mm_or_si128(_mm_or_si128(pix_1_9th, pix_1_10th), pix_1_11th));
_mm_store_si128(packed_rgb888_buf+1, pix128_1);

// And again for the third 128b write
__m128i pix_2_11th = _mm_and_si128(mask_2_11th, _mm_bsrli_si128(rgb888pix1lo_32b, 10));
__m128i pix_2_12th = _mm_and_si128(mask_2_12th, _mm_bsrli_si128(rgb888pix1lo_32b, 11));
__m128i pix_2_13th = _mm_and_si128(mask_2_13th, _mm_bslli_si128(rgb888pix1hi_32b,  4)); // Fourth 4 pixels
__m128i pix_2_14th = _mm_and_si128(mask_2_14th, _mm_bslli_si128(rgb888pix1hi_32b,  3));
__m128i pix_2_15th = _mm_and_si128(mask_2_15th, _mm_bslli_si128(rgb888pix1hi_32b,  2));
__m128i pix_2_16th = _mm_and_si128(mask_2_16th, _mm_bslli_si128(rgb888pix1hi_32b,  1));
__m128i pix128_2 = _mm_or_si128(_mm_or_si128(_mm_or_si128(pix_2_11th, pix_2_12th), pix_2_13th),
_mm_or_si128(_mm_or_si128(pix_2_14th, pix_2_15th), pix_2_16th));
_mm_store_si128(packed_rgb888_buf+2, pix128_2);

// Update pointer for next iteration
packed_rgb888_buf += 3;
}

for (int i = 0; i < buf_size; i++) {
uint8_t r565 = (i + 10) & 0x1F;
uint8_t g565 = i & 0x3F;
uint8_t b565 = (i + 20) & 0x1F;
printf("%2d] RGB = (%02x,%02x,%02x), should be (%02x,%02x,%02x)\n", i, rgb888buf[3*i+2],
rgb888buf[3*i+1], rgb888buf[3*i], r565 << 3, g565 << 2, b565 << 3);
}

return EXIT_SUCCESS;
}

РЕДАКТИРОВАТЬ: Вот второй способ сжать данные пикселей 32bpp в 24bpp. Я не проверял, быстрее это или нет, хотя я бы предположил, потому что он выполняет меньше инструкций и не нуждается в запуске дерева OR в конце. Однако менее понятно, как это работает.

В этой версии комбинация сдвигов и перетасовок используется для перемещения каждого блока пикселей вместе, а не для маскировки и сдвига каждого по отдельности. Метод, используемый для преобразования 16bpp в 32bpp, не изменился.

Во-первых, я определяю вспомогательную функцию для сдвига влево от нижнего значения uint32 в каждой половине __m128i.

__m128i bslli_low_dword_once(__m128i x) {
// Multiply low dwords by 256 to shift right 8 bits
const __m128i shift_multiplier = _mm_set1_epi32(1<<8);
// Mask off the high dwords
const __m128i mask = _mm_set_epi32(0xFFFFFFFF, 0, 0xFFFFFFFF, 0);

return _mm_or_si128(_mm_and_si128(x, mask), _mm_mul_epu32(x, shift_multiplier));
}

Тогда единственные другие изменения касаются кода для упаковки данных 32 бит / с в 24 бит / с.

// At this point, rgb888pix_32b contains the pixel data in 32bpp format, need to compress it to 24bpp
__m128i pix_0_block0lo = bslli_low_dword_once(rgb888pix0lo_32b);
pix_0_block0lo = _mm_srli_epi64(pix_0_block0lo, 8);
pix_0_block0lo = _mm_shufflelo_epi16(pix_0_block0lo, _MM_SHUFFLE(2, 1, 0, 3));
pix_0_block0lo = _mm_bsrli_si128(pix_0_block0lo, 2);

__m128i pix_0_block0hi = _mm_unpacklo_epi64(_mm_setzero_si128(), rgb888pix0hi_32b);
pix_0_block0hi = bslli_low_dword_once(pix_0_block0hi);
pix_0_block0hi = _mm_bslli_si128(pix_0_block0hi, 3);

__m128i pix128_0 = _mm_or_si128(pix_0_block0lo, pix_0_block0hi);
_mm_store_si128(packed_rgb888_buf, pix128_0);

// Do the same basic thing for the next 128b chunk of pixel data
__m128i pix_1_block0hi = bslli_low_dword_once(rgb888pix0hi_32b);
pix_1_block0hi = _mm_srli_epi64(pix_1_block0hi, 8);
pix_1_block0hi = _mm_shufflelo_epi16(pix_1_block0hi, _MM_SHUFFLE(2, 1, 0, 3));
pix_1_block0hi = _mm_bsrli_si128(pix_1_block0hi, 6);

__m128i pix_1_block1lo = bslli_low_dword_once(rgb888pix1lo_32b);
pix_1_block1lo = _mm_srli_epi64(pix_1_block1lo, 8);
pix_1_block1lo = _mm_shufflelo_epi16(pix_1_block1lo, _MM_SHUFFLE(2, 1, 0, 3));
pix_1_block1lo = _mm_bslli_si128(pix_1_block1lo, 6);

__m128i pix128_1 = _mm_or_si128(pix_1_block0hi, pix_1_block1lo);
_mm_store_si128(packed_rgb888_buf+1, pix128_1);

// And again for the final chunk
__m128i pix_2_block1lo = bslli_low_dword_once(rgb888pix1lo_32b);
pix_2_block1lo = _mm_bsrli_si128(pix_2_block1lo, 11);

__m128i pix_2_block1hi = bslli_low_dword_once(rgb888pix1hi_32b);
pix_2_block1hi = _mm_srli_epi64(pix_2_block1hi, 8);
pix_2_block1hi = _mm_shufflelo_epi16(pix_2_block1hi, _MM_SHUFFLE(2, 1, 0, 3));
pix_2_block1hi = _mm_bslli_si128(pix_2_block1hi, 2);

__m128i pix128_2 = _mm_or_si128(pix_2_block1lo, pix_2_block1hi);
_mm_store_si128(packed_rgb888_buf+2, pix128_2);
3

Другие решения

Других решений пока нет …