【视频编码】Intel Intrinsic的简单使用(YUYV422转换成NV12)
1.Intel Intrinsic
Intrinsic函数是在Windows平台上很常用的SIMD(Single Instruction, Multi-Data)指令集,性能介于C/C++和.asm之间,因其较好的并行处理能力,在视频编码领域中常用于DCT变换,格式转换等。SIMD是一种技术,而Intrinsic是提供给用户去方便使用SIMD指令的C/C++函数。Intel intrinsic guide的官网手册是:Intel intrinsic guide
这类指令集与传统C不同,使用的变量也不相同,例如,__m128i表示一个存储integer类型的变量,位数为128位,例如16个int8_t,8个int16_t和4个int32_t,能够实现并行处理。对比于C的循环,每次处理一个像素,这里假设处理4个int32_t,那么就是4倍的效率。即便选择在C循环中尝试一次循环处理4个像素,但由于这是串行操作,速度仍然低于使用并行处理的SIMD操作。因此,如果某些操作较为固定,且使用频繁,可以考虑使用SIMD来进行优化,从而实现加速的效果。
在视频压缩领域,使用汇编加速,往往比不使用汇编,大约能够提速30%,甚至更多。
2.YUYV422转换成NV12
这里记录一个从YUYV422转换到NV12格式的SSE41指令集的函数,不一定是效率最高的实现方式,但是有基本的功能,大致执行的流程是
(1)行和列的遍历
YUYV422格式中,1个像素占2个字节。举例说明,Y0 U0 Y1 V0 Y2 U1 Y3 V1,一共4个像素(4个Y分量),其中U0V0被Y0和Y1同时使用,U1V1被Y2和Y3同时使用,一共8个字节,所以1个像素占2个字节。因此,计算j时,移动字节数要乘以2。
(2)奇偶行处理
由于NV12格式的chroma分量在偶数行之间进行共享,所以只有偶数行进行chroma处理,奇数行只处理luma
(3)单行的处理
首先,两次使用_mm_loadu_si128(),读取前32个字节的信息,其中前16个字节存储在yuyv0中,后16个字节存储在yuyv1中,存储的信息为
yuyv0 : Y0 U0 Y1 V0 ... Y6 U3 Y7 V3
yuyv1 : Y8 U4 Y9 V4 ... Y14 U7 Y15 V7
_mm_and_si128()的作用是进行与运算,_mm_set1_epi16()的作用分别是将入参进行16位的广播,即获得
0x00FF:[0x00FF, 0x00FF, 0x00FF, 0x00FF, 0x00FF, 0x00FF, 0x00FF, 0x00FF] = [0x00FF00FF, 0x00FF00FF, 0x00FF00FF, 0x00FF00FF]
0xFF00:[0xFF00FF00, 0xFF00FF00, 0xFF00FF00, 0xFF00FF00]
将获得的yuyv0与mask_y进行与运算,得到的结果是
第1个32位:0xV0Y1U0Y0 & 0x00FF00FF (注意是小端存储) -> 0x00Y100Y0
第2个32位:0xV1Y3U1Y2 & 0x00FF00FF -> 0x00Y300Y2
第3个32位:0xV2Y5U2Y4 & 0x00FF00FF -> 0x00Y500Y4
第4个32位:0xV3Y7U3Y6 & 0x00FF00FF -> 0x00Y700Y6
y0 = [0x00Y100Y0, 0x00Y300Y2, 0x00Y500Y4, 0x00Y700Y6] = Y0, 0, Y1, 0, ...., Y6, 0, Y7, 0
同理,yuyv1与mask_y进行与运算,得到的结果是
第1个32位:0xV4Y9U4Y8 & 0x00FF00FF -> 0x00Y900Y8
第2个32位:0xV5Y11U5Y10 & 0x00FF00FF -> 0x00Y1100Y10
第3个32位:0xV6Y13U6Y12 & 0x00FF00FF -> 0x00Y1300Y12
第4个32位:0xV7Y15U7Y14 & 0x00FF00FF -> 0x00Y1500Y14
y1 = [0x00Y900Y8, 0x00Y1100Y10, 0x00Y1300Y12, 0x00Y1500Y14] = Y8, 0, Y9, 0, ...., Y14, 0, Y15, 0
_mm_packus_epi16()将y0和y1进行合并,输入分别是8个uint16_t,输出的结果是16个uint8_t
y0的8个16位:[0x00Y1, 0x00Y0, 0x00Y3, 0x00Y2, 0x00Y5, 0x00Y4, 0x00Y7, 0x00Y6],
y1的8个16位:[0x00Y9, 0x00Y8, 0x00Y11, 0x00Y10, 0x00Y13, 0x00Y12, 0x00Y15, 0x00Y14],
计算时,会先进行截取,将y0和y1中16位截取低8位,然后合并,最后使用_mm_storeu_si128()存储到指定的位置处
y0截取:[0xY1, 0xY0, 0xY3, 0xY2, 0xY5, 0xY4, 0xY7, 0xY6]
y1截取:[0xY9, 0xY8, 0xY11, 0xY10, 0xY13, 0xY12, 0xY15, 0xY14],
合并:[0xY1, 0xY0, 0xY3, 0xY2, 0xY5, 0xY4, 0xY7, 0xY6, 0xY9, 0xY8, 0xY11, 0xY10, 0xY13, 0xY12, 0xY15, 0xY14]
= [Y0, Y1, Y2, Y3, Y4, Y5, Y6, Y7, Y8, Y9, Y10, Y11, Y12, Y13, Y14, Y15]
其次,如果是偶数行,还需要处理chroma分量,按照同样的方式提取UV分量,最后存储到uv分量位置处
uv0 = [0xV0, 0x00, 0xU0, 0x00, 0xV1, 0x00, 0xU1, 0x00, 0xV2, 0x00, 0xU2, 0x00, 0xV3, 0x00, 0xU3, 0x00]
uv1 = [0xV4, 0x00, 0xU4, 0x00, 0xV5, 0x00, 0xU5, 0x00, 0xV6, 0x00, 0xU6, 0x00, 0xV7, 0x00, 0xU7, 0x00]
右移8位,得到:
uv0 = [0x00, 0xV0, 0x00, 0xU0, 0x00, 0xV1, 0x00, 0xU1, 0x00, 0xV2, 0x00, 0xU2, 0x00, 0xV3, 0x00, 0xU3]
uv1 = [0x00, 0xV4, 0x00, 0xU4, 0x00, 0xV5, 0x00, 0xU5, 0x00, 0xV6, 0x00, 0xU6, 0x00, 0xV7, 0x00, 0xU7]
合并:
packed_uv = [0xV0, 0xU0, 0xV1, 0xU1, 0xV2, 0xU2, 0xV3, 0xU3, .....] = [U0, V0, ..., U3, V3]
#include <emmintrin.h>
#include <smmintrin.h>
void yuyv422_to_nv12_sse41(const uint8_t *yuyv, uint8_t *nv12, int width, int height)
{
const int y_size = width * height;
uint8_t *dst_y = nv12; // NV12 的 Y 平面
uint8_t *dst_uv = nv12 + y_size; // NV12 的 UV 平面
const __m128i mask_y = _mm_set1_epi16(0x00FF); // 提取 Y 的掩码 (0x00FF)
const __m128i mask_uv = _mm_set1_epi16(0xFF00); // 提取 UV 的掩码 (高 16 位)
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j += 16)
{
// 每次处理 16 像素(32 字节)
// 加载 32 字节 YUYV 数据(2 个 128 位寄存器)
__m128i yuyv0 = _mm_loadu_si128((const __m128i *)(yuyv + j * 2));
__m128i yuyv1 = _mm_loadu_si128((const __m128i *)(yuyv + j * 2 + 16));
// 步骤 1:提取 Y 分量(低 8 位)
__m128i y0 = _mm_and_si128(yuyv0, mask_y); // Y0 Y1 Y2 Y3 ...
__m128i y1 = _mm_and_si128(yuyv1, mask_y);
// 将 16 位 Y 压缩为 8 位
__m128i packed_y = _mm_packus_epi16(y0, y1);
_mm_storeu_si128((__m128i *)(dst_y + j), packed_y);
// 步骤 2:提取 UV 分量(偶数行)
if (i % 2 == 0)
{
// 从 YUYV 中提取 U/V(高 16 位)
__m128i uv0 = _mm_and_si128(yuyv0, mask_uv); // U0 V0 U1 V1 ...
__m128i uv1 = _mm_and_si128(yuyv1, mask_uv);
// 右移 8 位,将 UV 放到低 8 位
uv0 = _mm_srli_epi16(uv0, 8);
uv1 = _mm_srli_epi16(uv1, 8);
// 重组为 NV12 的 UV 交错格式(U0 V0 U1 V1 ...)
__m128i packed_uv = _mm_packus_epi16(uv0, uv1);
_mm_storeu_si128((__m128i *)(dst_uv + j), packed_uv);
}
}
yuyv += width * 2; // 移动到下一行
dst_y += width;
if (i % 2 == 0)
{
dst_uv += width; // UV 平面隔行更新
}
}
}
更多推荐
所有评论(0)