注:问号以及未注释部分 会在x265-1.9版本内更新

/*****************************************************************************
* Copyright (C) 2013 x265 project
*
* Authors: Gopu Govindaswamy <gopu@multicorewareinc.com>
*          Steve Borho <steve@borho.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111, USA.
*
* This program is also available under a commercial proprietary license.
* For more information, contact us at license @ x265.com.
*****************************************************************************/
#include "common.h"
#include "frame.h"
#include "framedata.h"
#include "picyuv.h"
#include "primitives.h"
#include "lowres.h"
#include "mv.h"
#include "slicetype.h"
#include "motion.h"
#include "ratecontrol.h"
#if DETAILED_CU_STATS
#define ProfileLookaheadTime(elapsed, count) ScopedElapsedTime _scope(elapsed); count++
#else
#define ProfileLookaheadTime(elapsed, count)
#endif
using namespace X265_NS;
namespace {
/*推算公式:
E =(Σx)/n
n*S^2=Σ(x - E)^2 
=Σ(x^2 -2 xE+E^2)
=Σ(x^2) – 2EΣx+ nE^2
= Σ(x^2) – 2((Σx)/n)*Σx+ ((Σx)*((Σx)/n)  
=(Σ(x^2) - (Σx * Σx)/n )
所以acEnergyVar 返回值 Σ(x^2) - (Σx * Σx)/n = n * s^2
*/
/** 函数功能    :将元素和和平方和累加到当前帧中的m_lowres  返回当前块Σ(x^2) - (Σx * Σx)/n = n*Variance (n倍的方差)。
/*  调用范围    :只在acEnergyPlane函数中被调用
* \参数 curFrame:当前帧
* \参数 sum_ssd :当前块的元素和以及平方和
* \参数 shift   :移位数目(如果当前16x16,则当前值为8,表示当前有n=2^8 = 256 个像素值)
* \参数 plane   :0,1,2 分别表示Y、U、V分量
* \返回值       :当前块的交流(AC)分量
*/
/* Compute variance to derive AC energy of each block */
inline uint32_t acEnergyVar(Frame *curFrame, uint64_t sum_ssd, int shift, int plane)
{
uint32_t sum = (uint32_t)sum_ssd;        //当前块所有元素的和Σx
uint32_t ssd = (uint32_t)(sum_ssd >> 32);//当前块所有元素的平方和Σ(x^2)
curFrame->m_lowres.wp_sum[plane] += sum;
curFrame->m_lowres.wp_ssd[plane] += ssd;
return ssd - ((uint64_t)sum * sum >> shift);
}
/** 函数功能       : acEnergyVar将元素和和平方和累加到当前帧中的m_lowres  返回当前块n*Variance。
/*  调用范围       : 只在LookaheadTLD::acEnergyCu函数中被调用
* \参数 curFrame   : 当前帧
* \参数 src        : 当前块在一帧视频中的地址
* \参数 srcStride  : 当前帧的步长
* \参数 plane      : 0,1,2 分别表示Y、U、V
* \参数 colorFormat: 数据格式,1 为420格式
/* Find the energy of each block in Y/Cb/Cr plane */
inline uint32_t acEnergyPlane(Frame *curFrame, pixel* src, intptr_t srcStride, int plane, int colorFormat)
{
if ((colorFormat != X265_CSP_I444) && plane)//如果当前是420格式,则plane= 0,1,2 分别为Y,U V
{
//对色度进行计算
ALIGN_VAR_8(pixel, pix[8 * 8]);
primitives.cu[BLOCK_8x8].copy_pp(pix, 8, src, srcStride);                    //8 表示 dstStride
return acEnergyVar(curFrame, primitives.cu[BLOCK_8x8].var(pix, 8), 6, plane);//6 表示当前的元素个数:2^6 = 64 = 8*8 
}
else
return acEnergyVar(curFrame, primitives.cu[BLOCK_16x16].var(src, srcStride), 8, plane); //8 表示当前的元素个数: 2^8 = 256 = 16*16
//primitives.cu[BLOCK_16x16].var   返回一个64位整数,低32位存储当前16x16所有元素的和,高32位存储当前16x16所有元素的平方和
//primitives.cu[BLOCK_8x8].var     作用同上,返回的是8x8对应的数据
//primitives.cu[BLOCK_8x8].copy_pp 将源8x8数据copy到对应缓冲区
}
} // end anonymous namespace
/** 函数功能       : acEnergyVar将元素和和平方和累加到当前帧中的m_lowres  返回当前块YUV的AC能量。
/*  调用范围       : 只在LookaheadTLD::calcAdaptiveQuantFrame函数中被调用
* \参数 curFrame   : 当前帧(含有原始帧数据)
* \参数 blockX     : 当前16x16的左偏移量(单位像素)
* \参数 blockY     : 当前16x16的下偏移量(单位像素)
* \参数 csp        : 数据格式,1 为420格式
/* Find the total AC energy of each block in all planes */
uint32_t LookaheadTLD::acEnergyCu(Frame* curFrame, uint32_t blockX, uint32_t blockY, int csp)
{
/*假设原始帧亮度16x16块的方差标记为 sY  像素个数nY = 16x16 =256  AC能量记为: acY = nY*sY
假设原始帧色度 8x8 块的方差标记为 sCr、sCb  像素个数nC = 8x8 =64 AC能量记为: acCb = nC*sCb acCr = nC*sCr
当前块的AC能量记为:ac =  acY + acCb + acCr
**/
intptr_t stride = curFrame->m_fencPic->m_stride;        //原始帧亮度的步长
intptr_t cStride = curFrame->m_fencPic->m_strideC;      //原始帧色度的步长
intptr_t blockOffsetLuma = blockX + (blockY * stride);  //当前亮度位置相对于原始帧左上角像素的偏移地址
int hShift = CHROMA_H_SHIFT(csp);                       //如:420格式,色度宽高是亮度的1/2 ,则此处为1,表示需要右移1位
int vShift = CHROMA_V_SHIFT(csp);                       //如:420格式,色度宽高是亮度的1/2 ,则此处为1,表示需要右移1位
intptr_t blockOffsetChroma = (blockX >> hShift) + ((blockY >> vShift) * cStride);//当前色度位置相对于原始帧左上角像素的偏移地址
uint32_t var;
var  = acEnergyPlane(curFrame, curFrame->m_fencPic->m_picOrg[0] + blockOffsetLuma, stride, 0, csp);    //返回当前亮度块16x16的 n*Variance 值
var += acEnergyPlane(curFrame, curFrame->m_fencPic->m_picOrg[1] + blockOffsetChroma, cStride, 1, csp); //返回当前色度块8 x 8的 n*Variance 值
var += acEnergyPlane(curFrame, curFrame->m_fencPic->m_picOrg[2] + blockOffsetChroma, cStride, 2, csp); //返回当前色度块8 x 8的 n*Variance 值
x265_emms();//清除MMX寄存器中的内容,即初始化(以避免和浮点数操作发生冲突)。
return var; //返回亮度、色度的 n*Variance 值
}
/** 函数功能       : 初始化m_lowres中的qpCuTreeOffset等信息,获取整帧的像素和和AC能量。
/*  调用范围       : 只在PreLookaheadGroup::processTasks函数中被调用
* \参数 curFrame   : 当前帧(含有原始帧数据)
* \参数 param      : 编码器配置的参数 */
void LookaheadTLD::calcAdaptiveQuantFrame(Frame *curFrame, x265_param* param)
{
/* Actual adaptive quantization */
int maxCol = curFrame->m_fencPic->m_picWidth; //原始帧的宽度信息 
int maxRow = curFrame->m_fencPic->m_picHeight;//原始帧的高度信息
int blockCount = curFrame->m_lowres.maxBlocksInRow * curFrame->m_lowres.maxBlocksInCol;//当前帧下采样1/2 后总共有多少个8x8块,而在原始帧上刚好等于其16x16的个数
for (int y = 0; y < 3; y++)
{
curFrame->m_lowres.wp_ssd[y] = 0;  //初始化为0, 用于累加整帧像素的平方和
curFrame->m_lowres.wp_sum[y] = 0;  //初始化为0, 用于累加整帧像素的和
}
/* Calculate Qp offset for each 16x16 block in the frame */
int blockXY = 0;
int blockX = 0, blockY = 0;
double strength = 0.f;
if (param->rc.aqMode == X265_AQ_NONE || param->rc.aqStrength == 0)
{
/* Need to init it anyways for CU tree */
int cuCount = widthInCU * heightInCU;
//初始化QP-offset信息
if (param->rc.aqMode && param->rc.aqStrength == 0)
{
memset(curFrame->m_lowres.qpCuTreeOffset, 0, cuCount * sizeof(double));
memset(curFrame->m_lowres.qpAqOffset, 0, cuCount * sizeof(double));
for (int cuxy = 0; cuxy < cuCount; cuxy++)
curFrame->m_lowres.invQscaleFactor[cuxy] = 256;
}
//将当前未下采样的帧分成16x16计算其AC能量(用方差估算)
//存储未下采样原始帧所有像素的平方和Σ(x^2) 0,1,2 分别表示Y、U、V 
//存储未下采样原始帧所有像素的和(Σx)  0,1,2 分别表示Y、U、V
/* Need variance data for weighted prediction */
if (param->bEnableWeightedPred || param->bEnableWeightedBiPred)
{
for (blockY = 0; blockY < maxRow; blockY += 16)
for (blockX = 0; blockX < maxCol; blockX += 16)
acEnergyCu(curFrame, blockX, blockY, param->internalCsp);//计算每个16x16的AC能量:n*Variance 值
}
}
else
{
blockXY = 0;
double avg_adj_pow2 = 0, avg_adj = 0, qp_adj = 0;
double bias_strength = 0.f;
if (param->rc.aqMode == X265_AQ_AUTO_VARIANCE || param->rc.aqMode == X265_AQ_AUTO_VARIANCE_BIASED)
{
double bit_depth_correction = 1.f / (1 << (2*(X265_DEPTH-8)));//值: 1/(2^(2*(X265_DEPTH-8))) 
for (blockY = 0; blockY < maxRow; blockY += 16)
{
for (blockX = 0; blockX < maxCol; blockX += 16)
{
uint32_t energy = acEnergyCu(curFrame, blockX, blockY, param->internalCsp);//计算每个16x16的AC能量:energy =n*Variance 值
qp_adj = pow(energy * bit_depth_correction + 1, 0.1);                      //qp_adj = (energy*(1/(2^(2*(X265_DEPTH-8)))) + 1)^0.1
curFrame->m_lowres.qpCuTreeOffset[blockXY] = qp_adj;                       //初始化m_lowres.qpCuTreeOffset
avg_adj += qp_adj;
avg_adj_pow2 += qp_adj * qp_adj;
blockXY++;
}
}
avg_adj /= blockCount;                                                               //当前为qp_adj的平均值 (Σx)/n
avg_adj_pow2 /= blockCount;                                                          //当前为qp_adj的平方和均值(Σx^2)/n
strength = param->rc.aqStrength * avg_adj;                                           //修正当前的strength值 ,param->rc.aqStrength 为1.0 表示  strength = (Σx)/n
avg_adj = avg_adj - 0.5f * (avg_adj_pow2 - (11.f)) / avg_adj;                        //修正当前qp_adj的平均值:(Σx)/n - 0.5 * ( (Σx^2)/n  - 11) / ((Σx)/n) (当前为8位位宽)
bias_strength = param->rc.aqStrength;                                                //获取strength
}
else
strength = param->rc.aqStrength * 1.0397f;                                           //如果X265_AQ_VARIANCE strength = param->rc.aqStrength * 1.0397f;  
blockXY = 0;
for (blockY = 0; blockY < maxRow; blockY += 16)
{
for (blockX = 0; blockX < maxCol; blockX += 16)
{
if (param->rc.aqMode == X265_AQ_AUTO_VARIANCE_BIASED)
{
qp_adj = curFrame->m_lowres.qpCuTreeOffset[blockXY];
qp_adj = strength * (qp_adj - avg_adj) + bias_strength * (1.f - 11.f / (qp_adj * qp_adj));
}
else if (param->rc.aqMode == X265_AQ_AUTO_VARIANCE)
{
qp_adj = curFrame->m_lowres.qpCuTreeOffset[blockXY];                        //获取前面的(energy*(1/(2^(2*(X265_DEPTH-8)))) + 1)^0.1
qp_adj = strength * (qp_adj - avg_adj);                                     //更新当前的adj
}
else
{
uint32_t energy = acEnergyCu(curFrame, blockX, blockY, param->internalCsp);
qp_adj = strength * (X265_LOG2(X265_MAX(energy, 1)) - (14.427f + 2 * (X265_DEPTH - 8)));
}
curFrame->m_lowres.qpAqOffset[blockXY] = qp_adj;                             
curFrame->m_lowres.qpCuTreeOffset[blockXY] = qp_adj;
curFrame->m_lowres.invQscaleFactor[blockXY] = x265_exp2fix8(qp_adj);         //获取量化权重系数,能量(方差)越大,量化权重系数越小
blockXY++;
/*假设原始帧亮度16x16块的方差标记为 sY  像素个数nY = 16x16 =256  AC能量记为: acY = nY*sY
假设原始帧色度 8x8 块的方差标记为 sCr、sCb  像素个数nC = 8x8 =64 AC能量记为: acCb = nC*sCb acCr = nC*sCr
当前块的AC能量记为:ac =  acY + acCb + acCr
每个16x16块按照光栅扫描记为 ac(i) , 总共个数为n
AC能量经过矫正后的值:Jac = (energy*(1/(2^(2*(X265_DEPTH-8)))) + 1)^0.1
当前所有块的平均值为 JAveAc = (Σ(Jac)/n
当前所有块的平方和均值为 JAveAc2 = (Σ((Jac)^2))/n
当前自适应量化的强度记为:strength   
当前的位宽为 depth
如果:param->rc.aqMode = X265_AQ_NONE 
qpAqOffset     [i] =  0 
qpCuTreeOffset [i] =  0
invQscaleFactor[i] =  256(即权重系数为1 因为真正的权重为 invQscaleFactor[i]>>8)
如果:param->rc.aqMode = X265_AQ_VARIANCE
strength = param->rc.aqStrength * 1.0397f;  
qp_adj   = strength * (log2(Jac(i)) - (14.427 + 2*(depth-8))))
qpAqOffset     [i] = qp_adj
qpCuTreeOffset [i] = qp_adj
invQscaleFactor[i] = x265_exp2fix8(qp_adj)
如果:param->rc.aqMode = X265_AQ_AUTO_VARIANCE
strength = param->rc.aqStrength * JAveAc;
bias_strength = param->rc.aqStrength;     
qp_adj   = strength * (Jac - JAveAc); 
qpAqOffset     [i] = qp_adj
qpCuTreeOffset [i] = qp_adj
invQscaleFactor[i] = x265_exp2fix8(qp_adj)
如果:param->rc.aqMode = X265_AQ_AUTO_VARIANCE_BIASED
strength = param->rc.aqStrength * JAveAc ;
bias_strength = param->rc.aqStrength;     
qp_adj   =   strength * (Jac - JAveAc) + bias_strength * (1.f - 11.f / (Jac * Jac));     
qpAqOffset     [i] = qp_adj
qpCuTreeOffset [i] = qp_adj
invQscaleFactor[i] = x265_exp2fix8(qp_adj)
**/
}
}
}
if (param->bEnableWeightedPred || param->bEnableWeightedBiPred)
{
int hShift = CHROMA_H_SHIFT(param->internalCsp); //如:420格式,色度宽高是亮度的1/2 ,则此处为1,表示需要右移1位
int vShift = CHROMA_V_SHIFT(param->internalCsp); //如:420格式,色度宽高是亮度的1/2 ,则此处为1,表示需要右移1位
maxCol = ((maxCol + 8) >> 4) << 4;               //如果当前图像宽度整除16后的余数小于8,省去,大于8则补全16,如当前为417 = 26*16 +1,则返回26*16
//当前为424 = 26*16 +8,则返回26*17
maxRow = ((maxRow + 8) >> 4) << 4;               //如果当前图像高度整除16后的余数小于8,省去,大于8则补全16
int width[3]  = { maxCol, maxCol >> hShift, maxCol >> hShift }; //分别存储亮度、色度的宽度
int height[3] = { maxRow, maxRow >> vShift, maxRow >> vShift }; //分别存储亮度、色度的高度
for (int i = 0; i < 3; i++) //遍历Y、U、V
{
uint64_t sum, ssd;
sum = curFrame->m_lowres.wp_sum[i];   //未下采样原始帧所有像素的和(Σx)
ssd = curFrame->m_lowres.wp_ssd[i];   //未下采样原始帧所有像素的平方和Σ(x^2)
curFrame->m_lowres.wp_ssd[i] = ssd - (sum * sum + (width[i] * height[i]) / 2) / (width[i] * height[i]);
//返回一整帧的AC能量(约等于 n倍的方差 n为图像像素个数(n=width[i] * height[i])) 加上n/2是为了四舍五入)
}
}
}
/** 函数功能       : 计算当前1/2下采样帧的intra SATD值以及最优intra模式
/*  调用范围       : 只在PreLookaheadGroup::processTasks函数中被调用
* \参数 fenc       : 当前帧(经过1/2下采样后的数据)
*   返回值         : null
**/
void LookaheadTLD::lowresIntraEstimate(Lowres& fenc)
{
ALIGN_VAR_32(pixel, prediction[X265_LOWRES_CU_SIZE * X265_LOWRES_CU_SIZE]);//申请空间大小8x8 并内存对齐 用于存储当前预测块的像素值
pixel fencIntra[X265_LOWRES_CU_SIZE * X265_LOWRES_CU_SIZE];                //申请空间大小8x8 用于存储当前编码块的原始像素值
pixel neighbours[2][X265_LOWRES_CU_SIZE * 4 + 1];                          //存储当前周边行的数据 [0][]是参考周边数据,[1][]是经过intra滤波后的数据
//存储方式 0 left-above 1~8 ~ above 9~16 above-right 17~24 left 25~32 bottom-left
pixel* samples = neighbours[0], *filtered = neighbours[1];                 //samples是参考周边数据,filtered是经过intra滤波后的数据
const int lookAheadLambda = (int)x265_lambda_tab[X265_LOOKAHEAD_QP];       //X265_LOOKAHEAD_QP 为 12 + 6 * (X265_DEPTH - 8))  ,当前的lamda为 2^(qp/6 - 2) 并取整
//注意:当前QP并不一定是最优,可以继续修正,因为当前只是估算1/2 下采样视频的 intra编码代价
const int intraPenalty = 5 * lookAheadLambda;
const int lowresPenalty = 4; /* fixed CU cost overhead */                  //intra方向 bits估算:intraPenalty + lowresPenalty 注意:当前估算方式并一定最优,可以进一步优化
const int cuSize  = X265_LOWRES_CU_SIZE;                                   //当前用于估算的CU大小,在x265中统一用x88
const int cuSize2 = cuSize << 1;                                           //用于快速索引left存储位置区域
const int sizeIdx = X265_LOWRES_CU_BITS - 2;                               //便于找到asm代码函数,sizeIdx 0,1,2,3 分别对应着4x4、8x8、16x16、32x32
pixelcmp_t satd = primitives.pu[sizeIdx].satd;                             //计算8x8哈达玛变换代价的asm函数
int planar = !!(cuSize >= 8);
int costEst = 0, costEstAq = 0;                                            //costEst用于存储当前lowres中除边界块的全部intra8x8的SATD的累加和
//costEstAq用于存储当前lowres中除边界块的全部intra8x8的SATD乘以invQscaleFactor后的累加和
for (int cuY = 0; cuY < heightInCU; cuY++)
{
fenc.rowSatds[0][0][cuY] = 0;                                          //初始化一行cost为0
for (int cuX = 0; cuX < widthInCU; cuX++)
{
const int cuXY = cuX + cuY * widthInCU;
const intptr_t pelOffset = cuSize * cuX + cuSize * cuY * fenc.lumaStride; //计算当前8x8首地址相对于帧首地址的偏移量
pixel *pixCur = fenc.lowresPlane[0] + pelOffset;                          //计算当前8x8的首地址
/* copy fenc pixels */
primitives.cu[sizeIdx].copy_pp(fencIntra, cuSize, pixCur, fenc.lumaStride);//从帧中copy对应原始数据到8x8空间中,便于后面计算
/* collect reference sample pixels */
pixCur -= fenc.lumaStride + 1;                             //假设当前块的左上角像素点坐标为(x,y) ,则当前位置为(x-1,y-1)
memcpy(samples, pixCur, (2 * cuSize + 1) * sizeof(pixel)); /* top */ //获取0 left-above 1~8 ~ above 9~16 above-right与当前块相邻的行数据
for (int i = 1; i <= 2 * cuSize; i++)
samples[cuSize2 + i] = pixCur[i * fenc.lumaStride];    /* left */ //获取17~24 left 25~32 bottom-left与当前块相邻的行数据
primitives.cu[sizeIdx].intra_filter(samples, filtered); //将相邻行参考像素数据进行滤波 intraFilter函数
/*
滤波前数据:
45  39  36  32  31  33  31  32  33  30  35  43  40  49  52  64  67  44  45  42  42  42  46  46  44  33  33  42  48  60  58  46  44 
滤波后数据:
43  40  36  33  32  32  32  32  32  32  36  40  43  48  54  62  67  45  44  43  42  43  45  46  42  36  35  41  50  57  56  49  44 
采用1、2、1滤波
其中samples[0] 采用top行第一个数据和left列第一个数据进行滤波 filtered[0] = (samples[1] + 2*samples[0] + samples[2*8+1] +2 )>>2 = (39 + 45*2 +44 +2 )>>2 = 43
left列最后一个数据与top行最后一个数据直接进行copy: filtered[4*8] = samples[4*8] = 44 filtered[2*8] = samples[2*8] = 67
left列第一个数据:filtered[2*8+1] = (samples[0] + 2*samples[2*8+1] + samples[2*8+2] +2 )>>2 = (45 + 2*44 + 45 +2 )>>2 = 45 因为samples[0] 与samples[2*8+1]在列位置上相邻
其它像素采用相邻像素进行滤波
**/
int cost, icost = me.COST_MAX;         //icost 存储当前最优模式的cost
uint32_t ilowmode = 0;                 //ilowmod 存储当前最优的模式
/* DC and planar */
primitives.cu[sizeIdx].intra_pred[DC_IDX](prediction, cuSize, samples, 0, cuSize <= 16); //DC 模式不需要滤波
cost = satd(fencIntra, cuSize, prediction, cuSize);//计算当前模式的satd值
COPY2_IF_LT(icost, cost, ilowmode, DC_IDX);        //如果当前cost小于最优的代价,更新最优模式为当前模式,更新最优cost为当前cost
/*
例如当前原始数据:fencIntra
39  33  35  34  31  33  34  30 
37  35  38  37  33  31  30  34 
38  36  34  35  34  31  32  35 
42  36  37  36  33  37  39  40 
41  38  34  32  34  33  35  36 
41  34  31  36  34  38  38  34 
38  30  36  34  34  34  31  32 
30  37  41  36  34  32  33  40 
周边块数据:
滤波前数据:
45  39  36  32  31  33  31  32  33  30  35  43  40  49  52  64  67  44  45  42  42  42  46  46  44  33  33  42  48  60  58  46  44 
滤波后数据:
43  40  36  33  32  32  32  32  32  32  36  40  43  48  54  62  67  45  44  43  42  43  45  46  42  36  35  41  50  57  56  49  44 (当前为DC模式,无须滤波)
则预测数据:prediction
40  38  37  37  38  37  37  38 
41  39  39  39  39  39  39  39 
40  39  39  39  39  39  39  39 
40  39  39  39  39  39  39  39 
40  39  39  39  39  39  39  39 
41  39  39  39  39  39  39  39 
41  39  39  39  39  39  39  39 
40  39  39  39  39  39  39  39 
dcValue = (left像素和 + above像素和 + 当前块宽度)>>(log2(N)+ 1)
=((44+45+42+42+42+46+46+44) + (39+36+32+31+33+31+32+33) + 8)>>4
= 39
如果当前块大于16x16,全部为dcValue
否则,
预测左上角像素 = (left[0] + above[0] + 2*dcValue+2)>>2 = (44+39+2*39+2)>>2 = 40
预测第一行像素[x] = (above[x]+3*dcValue+2)>>2 如:预测第一行像素[4] = (33+3*39+2)>>2 = 38
预测第一列像素[x] = (left[x]+3*dcValue+2)>>2  如: 预测第一列像素[5] = (46+3*39+2)>>2 = 41
SATD计算:
首先计算编码块与预测块的残差:fencIntra - prediction
-1  -5  -2  -3  -7  -4  -3  -8 
-4  -4  -1  -2  -6  -8  -9  -5 
-2  -3  -5  -4  -5  -8  -7  -4 
2  -3  -2  -3  -6  -2   0   1 
1  -1  -5  -7  -5  -6  -4  -3 
0  -5  -8  -3  -5  -1  -1  -5 
-3  -9  -3  -5  -5  -5  -8  -7 
-10 -2   2  -3  -5  -7  -6   1 
然后将残差块拆减为4x4块
-1  -5  -2  -3      -7  -4  -3  -8    1  -1  -5  -7    -5  -6  -4  -3 
-4  -4  -1  -2      -6  -8  -9  -5    0  -5  -8  -3    -5  -1  -1  -5 
-2  -3  -5  -4      -5  -8  -7  -4   -3  -9  -3  -5    -5  -5  -8  -7 
2  -3  -2  -3      -6  -2   0   1   -10 -2   2  -3    -5  -7  -6   1 
哈达玛矩阵:hadma
1     1     1     1
1    -1     1    -1
1     1    -1    -1
1    -1    -1     1
对其做哈达玛变换:
1     1     1     1
1    -1     1    -1
1     1    -1    -1
1    -1    -1     1
hadma*A*hadma' = 1     1     1     1       -1  -5  -2  -3      1     1     1     1
1    -1     1    -1   *   -4  -4  -1  -2  *   1    -1     1    -1
1     1    -1    -1       -2  -3  -5  -4      1     1    -1    -1
1    -1    -1     1        2  -3  -2  -3      1    -1    -1     1
= -42    12     2     8
-8    -2     4     2
-2     0   -14    -4
8    10     4     6
求得矩阵的绝对值和为:Σabs(x) = 128 则satd = 128/2 = 64
累加4个4x4的satd值为当前8x8的satd值: 64+112+108+96 = 380   
**/       
primitives.cu[sizeIdx].intra_pred[PLANAR_IDX](prediction, cuSize, neighbours[planar], 0, 0);//计算planar模式
cost = satd(fencIntra, cuSize, prediction, cuSize);
COPY2_IF_LT(icost, cost, ilowmode, PLANAR_IDX); //如果当前cost小于最优的代价,更新最优模式为当前模式,更新最优cost为当前cost
/* scan angular predictions */
int filter, acost = me.COST_MAX;
uint32_t mode, alowmode = 4;          //alowmode存储当前最优的角度模式
for (mode = 5; mode < 35; mode += 5)  //快速遍历帧内角度模式,每隔五个模式遍历一个模式
{
filter = !!(g_intraFilterFlags[mode] & cuSize);  //判断当前是否用于滤波后参考像素
primitives.cu[sizeIdx].intra_pred[mode](prediction, cuSize, neighbours[filter], mode, cuSize <= 16);
cost = satd(fencIntra, cuSize, prediction, cuSize);
COPY2_IF_LT(acost, cost, alowmode, mode); //如果当前cost小于最优的代价,更新最优模式为当前模式,更新最优cost为当前cost
}
for (uint32_t dist = 2; dist >= 1; dist--) //遍历角度模式 alowmode-1, alowmode -2, alowmode +1, alowmode+2
{
int minusmode = alowmode - dist;
int plusmode = alowmode + dist;
mode = minusmode;
filter = !!(g_intraFilterFlags[mode] & cuSize);
primitives.cu[sizeIdx].intra_pred[mode](prediction, cuSize, neighbours[filter], mode, cuSize <= 16);
cost = satd(fencIntra, cuSize, prediction, cuSize);
COPY2_IF_LT(acost, cost, alowmode, mode);
mode = plusmode;
filter = !!(g_intraFilterFlags[mode] & cuSize);
primitives.cu[sizeIdx].intra_pred[mode](prediction, cuSize, neighbours[filter], mode, cuSize <= 16);
cost = satd(fencIntra, cuSize, prediction, cuSize);
COPY2_IF_LT(acost, cost, alowmode, mode);
}
COPY2_IF_LT(icost, acost, ilowmode, alowmode); //角度模式与DC、planar模式比较获取最优的预测模式
icost += intraPenalty + lowresPenalty; /* estimate intra signal cost */ //加上方向角度估计占用的bits
fenc.lowresCosts[0][0][cuXY] = (uint16_t)(X265_MIN(icost, LOWRES_COST_MASK) | (0 << LOWRES_COST_SHIFT));  //存储intra块最优SATD值
fenc.intraCost[cuXY] = icost;                                                                             //存储当前块的SATD值
fenc.intraMode[cuXY] = (uint8_t)ilowmode;                                                                 //存储当前块的最优帧内模式
/* do not include edge blocks in the frame cost estimates, they are not very accurate */
const bool bFrameScoreCU = (cuX > 0 && cuX < widthInCU - 1 &&
cuY > 0 && cuY < heightInCU - 1) || widthInCU <= 2 || heightInCU <= 2; //判断当前是否是边界上的CU,因为边界上的块不够准确,是边界返回false
int icostAq = (bFrameScoreCU && fenc.invQscaleFactor) ? ((icost * fenc.invQscaleFactor[cuXY] + 128) >> 8) : icost;
//如果是边界块:icostAq = icost
//  不是边界块:icostAq = (icost * fenc.invQscaleFactor[cuXY] + 128) >> 8
if (bFrameScoreCU)
{
costEst += icost;
costEstAq += icostAq;
}
fenc.rowSatds[0][0][cuY] += icostAq; //累加当前行的icostAq
}
}
fenc.costEst[0][0] = costEst;              //当前lowres中除边界块的全部intra8x8的SATD的累加和
fenc.costEstAq[0][0] = costEstAq;          //当前lowres中除边界块的全部intra8x8的SATD乘以invQscaleFactor后的累加和
}
/** 函数功能             :计算两帧(wp.bPresentFlag 为 ref是否加权)之间的SATD值
/*  调用范围             :只在LookaheadTLD::weightsAnalyse函数中被调用
* \参数 fenc             :当前帧
* \参数 ref              :前向帧
* \返回                  :两帧(wp.bPresentFlag 为 ref是否加权)之间的SATD值 */
uint32_t LookaheadTLD::weightCostLuma(Lowres& fenc, Lowres& ref, WeightParam& wp)
{
pixel *src = ref.fpelPlane[0];
intptr_t stride = fenc.lumaStride;
if (wp.bPresentFlag)
{
int offset = wp.inputOffset << (X265_DEPTH - 8);//offset信息 整帧所有像素偏移值
int scale = wp.inputWeight;                     //权重系数w<<wp.log2WeightDenom
int denom = wp.log2WeightDenom;                 //权重系数左移位个数(为了保证精度)
int round = denom ? 1 << (denom - 1) : 0;       //四舍五入操作
int correction = IF_INTERNAL_PREC - X265_DEPTH; // intermediate interpolation depth
int widthHeight = (int)stride;
primitives.weight_pp(ref.buffer[0], wbuffer[0], stride, widthHeight, paddedLines,
scale, round << correction, denom + correction, offset);  //P帧加权参考帧获取 void weight_pp_c(const pixel* src, pixel* dst, intptr_t stride, int width, int height, int w0, int round, int shift, int offset)
src = weightedRef.fpelPlane[0];                //src 更新为加权参考帧像素
}
uint32_t cost = 0;
intptr_t pixoff = 0;
int mb = 0;
//计算当前两帧之间的SATD值
for (int y = 0; y < fenc.lines; y += 8, pixoff = y * stride)
{
for (int x = 0; x < fenc.width; x += 8, mb++, pixoff += 8)
{
int satd = primitives.pu[LUMA_8x8].satd(src + pixoff, stride, fenc.fpelPlane[0] + pixoff, stride);
cost += X265_MIN(satd, fenc.intraCost[mb]);
}
}
return cost;//返回当前两帧的SATD值
}
/** 函数功能             :申请存储空间并初始化weightedRef
/*  调用范围             :只在LookaheadTLD::weightsAnalyse函数中被调用
* \参数 fenc             :当前帧
* \返回                  :内存申请成功为true 失败为false */
bool LookaheadTLD::allocWeightedRef(Lowres& fenc)
{
intptr_t planesize = fenc.buffer[1] - fenc.buffer[0];      //lumaStride * (lines + 2 * origPic->m_lumaMarginY); 1/2下采样视频帧+扩边的大小
intptr_t padoffset = fenc.lowresPlane[0] - fenc.buffer[0]; //真实数据到数据buffer首地址的偏移地址(偏移部分是扩边信息)
paddedLines = (int)(planesize / fenc.lumaStride);          //buf行数
wbuffer[0] = X265_MALLOC(pixel, 4 * planesize);           //申请空间方式同Lowres.buf
if (wbuffer[0])
{
wbuffer[1] = wbuffer[0] + planesize;
wbuffer[2] = wbuffer[1] + planesize;
wbuffer[3] = wbuffer[2] + planesize;
}
else
return false;
for (int i = 0; i < 4; i++)
weightedRef.lowresPlane[i] = wbuffer[i] + padoffset;//真实数据区域
weightedRef.fpelPlane[0] = weightedRef.lowresPlane[0];  //标记ref相应信息
weightedRef.lumaStride = fenc.lumaStride;
weightedRef.isLowres = true;                           
weightedRef.isWeighted = false;
return true;
}
/** 函数功能             :判断当前两帧是否进行加权 ,结果存储在weightedRef.isWeighted
/*  调用范围             :只在CostEstimateGroup::estimateFrameCost函数中被调用
* \参数 fenc             :当前帧
* \参数 ref              :前向帧
* \返回                  :NULL */
void LookaheadTLD::weightsAnalyse(Lowres& fenc, Lowres& ref)
{
static const float epsilon = 1.f / 128.f;       //ε= 1/128  阈值限定,如果加权系数小于ε,表示无须对齐进行加权分析
int deltaIndex = fenc.frameNum - ref.frameNum;  //当前帧与前向帧的poc差值
WeightParam wp;                                //用于存储加权参量
wp.bPresentFlag = false;                       //先判定不加权预测情况
if (!wbuffer[0])
{
if (!allocWeightedRef(fenc))              //申请存储空间并初始化weightedRef
return;
}
/* epsilon is chosen to require at least a numerator of 127 (with denominator = 128) */
float guessScale, fencMean, refMean;         //guessScale通过两帧的AC能量预估一个加权系数w, fencMean存储当前帧的像素平均值, refMean存储参考帧的像素平均值;
x265_emms();                                 //清除MMX寄存器中的内容,即初始化(以避免和浮点数操作发生冲突)。
if (fenc.wp_ssd[0] && ref.wp_ssd[0])         //分别表示未下采样原始帧整帧的AC能量(n方差)
guessScale = sqrtf((float)fenc.wp_ssd[0] / ref.wp_ssd[0]); //预估一个因子√(AC(rec)/AC(ref))
else
guessScale = 1.0f;
fencMean = (float)fenc.wp_sum[0] / (fenc.lines * fenc.width) / (1 << (X265_DEPTH - 8)); //计算其平均值,因其(含补全8x8块)边像素值,所以其平均值比正常偏大
refMean = (float)ref.wp_sum[0] / (fenc.lines * fenc.width) / (1 << (X265_DEPTH - 8));   //计算其平均值,因其(含补全8x8块)边像素值,所以其平均值比正常偏大
/* Early termination */
if (fabsf(refMean - fencMean) < 0.5f && fabsf(1.f - guessScale) < epsilon)  //提前终止判断,如果其平均值差小于0.5 或者  1- √(AC(rec)/AC(ref)) < 1/128
return;
int minoff = 0, minscale, mindenom;           //minoff 记录最优的offset, minscale记录最优的w, mindenom记录最优w精度扩大位数;
unsigned int minscore = 0, origscore = 1;     //minscore 记录最优的SATD值, origscore 不加权两帧之间的SATD值
int found = 0;                                //记录是否应用加权
wp.setFromWeightAndOffset((int)(guessScale * 128 + 0.5f), 0, 7, true);//设置weight参数,分别为加权参数w, offset,扩大精度7位(乘以128)
mindenom = wp.log2WeightDenom;               //更新最优w精度扩大位数
minscale = wp.inputWeight;                   //更新最优加权系数w
origscore = minscore = weightCostLuma(fenc, ref, wp); //计算不加权两帧之间的SATD值
if (!minscore)  //如果计算为0,说明当前没有差异,直接返回退出
return;
unsigned int s = 0;
int curScale = minscale;
int curOffset = (int)(fencMean - refMean * curScale / (1 << mindenom) + 0.5f);//计算加权帧的像素偏移量: 原始帧像素平均值 - 重构帧像素平均值*w +0.5f (w= curScale/1 << mindenom) = √(AC(rec)/AC(ref)))
if (curOffset < -128 || curOffset > 127)//clip操作,offset 应该属于(-128,128)
{
/* Rescale considering the constraints on curOffset. We do it in this order
* because scale has a much wider range than offset (because of denom), so
* it should almost never need to be clamped. */
curOffset = x265_clip3(-128, 127, curOffset);
curScale = (int)((1 << mindenom) * (fencMean - curOffset) / refMean + 0.5f);
curScale = x265_clip3(0, 127, curScale);
}
SET_WEIGHT(wp, true, curScale, mindenom, curOffset);//设置WeightParam类数据
/* SET_WEIGHT宏如下:
(w).inputWeight = (curScale,); 
(w).log2WeightDenom = ( mindenom); 
(w).inputOffset = (curOffset); 
(w).bPresentFlag = (true); 
**/
s = weightCostLuma(fenc, ref, wp); //计算加权帧与fenc的SATD值
COPY4_IF_LT(minscore, s, minscale, curScale, minoff, curOffset, found, 1);//更新当前最优的选择,加权是否
/* Use a smaller denominator if possible */
while (mindenom > 0 && !(minscale & 1)) //值保证不变,将左移位数减少
{
mindenom--;
minscale >>= 1;
}
if (!found || (minscale == 1 << mindenom && minoff == 0) || (float)minscore / origscore > 0.998f)//有任意三点的情况,都判为不加权1.加权SATD不是最优 2.偏移值为0并且w为1(minscale == 1 << mindenom 表示w为1)  3.加权SATD比不加权SATD不足够小
return;
else
{
SET_WEIGHT(wp, true, minscale, mindenom, minoff);//设置WeightParam类数据
// set weighted delta cost
fenc.weightedCostDelta[deltaIndex] = minscore / origscore;//存储参考帧(也是下采样的原始帧)加权的SATD/不加权的SATD
int offset = wp.inputOffset << (X265_DEPTH - 8);  //获取相应weight参数
int scale = wp.inputWeight;
int denom = wp.log2WeightDenom;
int round = denom ? 1 << (denom - 1) : 0;
int correction = IF_INTERNAL_PREC - X265_DEPTH; // intermediate interpolation depth
intptr_t stride = ref.lumaStride;
int widthHeight = (int)stride;
for (int i = 0; i < 4; i++)
primitives.weight_pp(ref.buffer[i], wbuffer[i], stride, widthHeight, paddedLines,
scale, round << correction, denom + correction, offset);  //P帧加权参考帧获取 void weight_pp_c(const pixel* src, pixel* dst, intptr_t stride, int width, int height, int w0, int round, int shift, int offset)
weightedRef.isWeighted = true;//将当前分析结果标记:加权  
}
}
/** 函数功能             : 初始化信息
/*  调用范围             : 只在主线程Encoder::create()中应用
* \参数 param            : 配置参数
* \参数 pool             : NAMA模式下的线程池
* \返回                  : null * */
Lookahead::Lookahead(x265_param *param, ThreadPool* pool)
{
m_param = param;
m_pool  = pool;
m_lastNonB = NULL;
m_isSceneTransition = false;
m_scratch  = NULL;
m_tld      = NULL;
m_filled   = false;
m_outputSignalRequired = false;
m_isActive = true;
m_8x8Height = ((m_param->sourceHeight / 2) + X265_LOWRES_CU_SIZE - 1) >> X265_LOWRES_CU_BITS;
m_8x8Width = ((m_param->sourceWidth / 2) + X265_LOWRES_CU_SIZE - 1) >> X265_LOWRES_CU_BITS;
m_8x8Blocks = m_8x8Width > 2 && m_8x8Height > 2 ? (m_8x8Width - 2) * (m_8x8Height - 2) : m_8x8Width * m_8x8Height;
m_lastKeyframe = -m_param->keyframeMax;   //标记当前前一个关键帧位置,初始化为-maxKeyframe, 保证第一帧是关键帧
m_sliceTypeBusy = false;
m_fullQueueSize = X265_MAX(1, m_param->lookaheadDepth);
m_bAdaptiveQuant = m_param->rc.aqMode || m_param->bEnableWeightedPred || m_param->bEnableWeightedBiPred;
/* If we have a thread pool and are using --b-adapt 2, it is generally
* preferable to perform all motion searches for each lowres frame in large
* batched; this will create one job per --bframe per lowres frame, and
* these jobs are performed by workers bonded to the thread running
* slicetypeDecide() */
m_bBatchMotionSearch = m_pool && m_param->bFrameAdaptive == X265_B_ADAPT_TRELLIS;
/* It is also beneficial to pre-calculate all possible frame cost estimates
* using worker threads bonded to the worker thread running
* slicetypeDecide(). This creates bframes * bframes jobs which take less
* time than the motion search batches but there are many of them. This may
* do much unnecessary work, some frame cost estimates are not needed, so if
* the thread pool is small we disable this feature after the initial burst
* of work */
m_bBatchFrameCosts = m_bBatchMotionSearch;
if (m_param->lookaheadSlices && !m_pool)
m_param->lookaheadSlices = 0;
if (m_param->lookaheadSlices > 1)
{
m_numRowsPerSlice = m_8x8Height / m_param->lookaheadSlices;
m_numRowsPerSlice = X265_MAX(m_numRowsPerSlice, 10);            // at least 10 rows per slice
m_numRowsPerSlice = X265_MIN(m_numRowsPerSlice, m_8x8Height);   // but no more than the full picture
m_numCoopSlices = m_8x8Height / m_numRowsPerSlice;
m_param->lookaheadSlices = m_numCoopSlices;                     // report actual final slice count
}
else
{
m_numRowsPerSlice = m_8x8Height;
m_numCoopSlices = 1;
}
#if DETAILED_CU_STATS
m_slicetypeDecideElapsedTime = 0;
m_preLookaheadElapsedTime = 0;
m_countSlicetypeDecide = 0;
m_countPreLookahead = 0;
#endif
memset(m_histogram, 0, sizeof(m_histogram));
}
#if DETAILED_CU_STATS
void Lookahead::getWorkerStats(int64_t& batchElapsedTime, uint64_t& batchCount, int64_t& coopSliceElapsedTime, uint64_t& coopSliceCount)
{
batchElapsedTime = coopSliceElapsedTime = 0;
coopSliceCount = batchCount = 0;
int tldCount = m_pool ? m_pool->m_numWorkers : 1;
for (int i = 0; i < tldCount; i++)
{
batchElapsedTime += m_tld[i].batchElapsedTime;
coopSliceElapsedTime += m_tld[i].coopSliceElapsedTime;
batchCount += m_tld[i].countBatches;
coopSliceCount += m_tld[i].countCoopSlices;
}
}
#endif
/** 函数功能             : 申请空间
/*  调用范围             : 只在主线程Encoder::create()中应用
* \返回                  : 成为为true 失败为fasle * */
bool Lookahead::create()
{
int numTLD = 1 + (m_pool ? m_pool->m_numWorkers : 0);
m_tld = new LookaheadTLD[numTLD];
for (int i = 0; i < numTLD; i++)
m_tld[i].init(m_8x8Width, m_8x8Height, m_8x8Blocks);
m_scratch = X265_MALLOC(int, m_tld[0].widthInCU);
return m_tld && m_scratch;
}
/** 函数功能             : 关闭帧类型决策任务,如果有任务在执行,则等待完毕,再停止任务
/*  调用范围             : 只在主线程Encoder::stopJobs()中应用
* \返回                  : null* */
void Lookahead::stopJobs()
{
if (m_pool && !m_inputQueue.empty())//如果输入列表中还有帧类型为决策,则当前还不该退出
{
m_inputLock.acquire();
m_isActive = false;
bool wait = m_outputSignalRequired = m_sliceTypeBusy;
m_inputLock.release();
if (wait)
m_outputSignal.wait(); //等待任务全部完成
}
}
/** 函数功能             : 释放内存
/*  调用范围             : 只在主线程Encoder::destroy()中应用
* \返回                  : null* */
void Lookahead::destroy()
{
// these two queues will be empty unless the encode was aborted
while (!m_inputQueue.empty())//该队列一般都已经进入输出队列m_outputQueue中,一般不会进入此
{
Frame* curFrame = m_inputQueue.popFront();
curFrame->destroy();
delete curFrame;
}
while (!m_outputQueue.empty())//该队列一般都已经进入DPB.m_picList中,一般不会进入此
{
Frame* curFrame = m_outputQueue.popFront();
curFrame->destroy();
delete curFrame;
}
X265_FREE(m_scratch);
delete [] m_tld;
}
/* The synchronization of slicetypeDecide is managed here.  The findJob() method
* polls the occupancy of the input queue. If the queue is
* full, it will run slicetypeDecide() and output a mini-gop of frames to the
* output queue. If the flush() method has been called (implying no new pictures
* will be received) then the input queue is considered full if it has even one
* picture left. getDecidedPicture() removes pictures from the output queue and
* only blocks as a last resort. It does not start removing pictures until
* m_filled is true, which occurs after *more than* the lookahead depth of
* pictures have been input so slicetypeDecide() should have started prior to
* output pictures being withdrawn. The first slicetypeDecide() will obviously
* still require a blocking wait, but after this slicetypeDecide() will maintain
* its lead over the encoder (because one picture is added to the input queue
* each time one is removed from the output) and decides slice types of pictures
* just ahead of when the encoder needs them */
/* Called by API thread */
/** 函数功能             : 向输入列表中添加原始帧准备帧类型决策,在buffer满时,触发帧类型决策
/*  调用范围             : 只在主线程Encoder中应用
* \参数 curFrame         : 传入的原始帧
* \参数 sliceType        : 1pass 中为AUTO 2pass中为具体的帧类型
* \返回                  : null * */
void Lookahead::addPicture(Frame& curFrame, int sliceType)
{
curFrame.m_lowres.sliceType = sliceType;//设置帧类型
/* determine if the lookahead is (over) filled enough for frames to begin to
* be consumed by frame encoders */
if (!m_filled)// 如果当前未满
{
if (!m_param->bframes & !m_param->lookaheadDepth) //零延迟,直接标记为true
m_filled = true; /* zero-latency */
else if (curFrame.m_poc >= m_param->lookaheadDepth + 2 + m_param->bframes)//buffer以满,标记为true
m_filled = true; /* full capacity plus mini-gop lag */
}
m_inputLock.acquire(); //多线程锁,将输入列表加锁,防止多线程破坏
m_inputQueue.pushBack(curFrame);//将当前帧加入队列
if (m_pool && m_inputQueue.size() >= m_fullQueueSize)//如果输入列表的帧数已经大于搜索的最大帧数
tryWakeOne();//触发开始进行帧类型决策
m_inputLock.release();//解锁
}
/* Called by API thread */
/** 函数功能             : 当前已经读取原始帧完毕,往后不用再继续读取,告知lookahead已满
/*  调用范围             : 只在主线程Encoder::encode中应用
* \返回                  : null * */
void Lookahead::flush()
{
/* force slicetypeDecide to run until the input queue is empty */
m_fullQueueSize = 1;
m_filled = true;
}
/** 函数功能             : 触发帧类型决策(在threadMain()主动发起,在getDecidedPicture()被动发起,因为当前发现帧类型不可用,被动发起)
/*  调用范围             : 只在WorkerThread::threadMain()(在addPicture中触发执行)和Lookahead::getDecidedPicture()函数中被调用
* \返回                  : null * */
void Lookahead::findJob(int /*workerThreadID*/)
{
bool doDecide;//用于指示是否进行帧类型决策
m_inputLock.acquire();//将m_inputQueue列表加锁,准备读取其数据,加锁防止多线程造成破坏
if (m_inputQueue.size() >= m_fullQueueSize && !m_sliceTypeBusy && m_isActive)//如果当前输入列表的帧数大于等于lookachead最大深度并且当前不再进行sliceDecide并且当前lookachead是触发状态
doDecide = m_sliceTypeBusy = true;//满足条件将其置为true,准备帧类型决策 (其中m_sliceTypeBusy保证系统只有一个线程进行帧类型决策)
else
doDecide = m_helpWanted = false;//不满足条件,将其置为false 准备退出
m_inputLock.release();//释放m_inputQueue列表锁
if (!doDecide) //不满足帧类型决策的条件,直接退出
return;
ProfileLookaheadTime(m_slicetypeDecideElapsedTime, m_countSlicetypeDecide);//统计时间信息(有宏DETAILED_CU_STATS控制)
ProfileScopeEvent(slicetypeDecideEV);
slicetypeDecide(); //获取帧类型并计算frame-cost
m_inputLock.acquire();//将m_inputQueue列表加锁
//与event m_outputSignal;配套使用,初始化为false  true表示需要完成sliceDecide,在Lookahead::findJob中完成sliceDecide置为false并触发m_outputSignal,说明完成sliceDecide
//在Lookahead::getDecidedPicture()函数中会进行检测并阻塞
if (m_outputSignalRequired)//如果前面编码过程中等待所需的帧类型决策完毕
{
m_outputSignal.trigger();//触发帧类型决策完毕,使其不再等待,继续编码
m_outputSignalRequired = false;//帧类型决策完毕,置为false
}
m_sliceTypeBusy = false;//帧类型决策完毕,不再忙
m_inputLock.release();//将m_inputQueue列表解锁
}
/* Called by API thread */
/** 函数功能             : 只在主线程Encoder中应用,获取已经得到帧类型的原始帧
/*  调用范围             : 只在Encoder::encode函数中被调用
* \返回                  : 返回当前帧类型决策完毕的待编码帧 * */
Frame* Lookahead::getDecidedPicture()
{
if (m_filled)//当前buf是否已满
{
m_outputLock.acquire();//多线程锁,防止数据别破坏
Frame *out = m_outputQueue.popFront();//抛出已经决定好帧类型列表中的第一帧
m_outputLock.release();//多线程解锁
if (out)//有可用帧直接输出
return out;
findJob(-1); //没有可用帧,准备findjob调用slicetype并得到可用帧,作为补充,如果获取编码帧快于slicetype的情况,这种情况较少,但是也会进入/* run slicetypeDecide() if necessary */
m_inputLock.acquire();//多线程锁,防止数据别破坏
bool wait = m_outputSignalRequired = m_sliceTypeBusy;//如果当前正在进行slicetype 需要等待
m_inputLock.release();//多线程解锁
if (wait)
m_outputSignal.wait();//一直等待直到帧类型决策完毕
return m_outputQueue.popFront();//抛出可用帧
}
else
return NULL;//没满,不能做帧类型决策,直接抛出null
}
/* Called by rate-control to calculate the estimated SATD cost for a given
* picture.  It assumes dpb->prepareEncode() has already been called for the
* picture and all the references are established */
/** 函数功能             : 获取当前帧每个CTU行对应下采样帧的每个8x8的块cost的累计值
/*  调用范围             : 只在Encoder::encode函数中被调用
* \参数 curFrame         : 传入的原始帧
* \返回                  : null * */
void Lookahead::getEstimatedPictureCost(Frame *curFrame)
{
Lowres *frames[X265_LOOKAHEAD_MAX];//存储相关的下采样图像  存储方式:前向帧  当前帧  后向帧
/*如当前编码为:
1   3   5    7
2       6
0      4        8
当前帧为6 
则在frame的存储方式为:
4 x 6 x 8
p0一定为0  b 为相对于p0的偏移2   p1为相对于b的偏移 4
**/
// POC distances to each reference
Slice *slice = curFrame->m_encData->m_slice;//获取当前slice
int p0 = 0, p1, b;//b:当前帧   P0前向帧  p1后向帧
int poc = slice->m_poc;//当前POC
int l0poc = slice->m_refPOCList[0][0];//获取list0的第一帧的poc
int l1poc = slice->m_refPOCList[1][0];//获取list1的第一帧的poc
switch (slice->m_sliceType)
{
case I_SLICE:
frames[p0] = &curFrame->m_lowres;//当前为I帧,直接获取当前帧的下采样图像
b = p1 = 0;
break;
case P_SLICE:
b = p1 = poc - l0poc;//获取当前帧应该存放的位置
frames[p0] = &slice->m_refPicList[0][0]->m_lowres;//获取前向帧
frames[b] = &curFrame->m_lowres;//获取当前帧
break;
case B_SLICE:
b = poc - l0poc;//获取当前帧应该存放的位置
p1 = b + l1poc - poc;//获取后向帧应该存放的位置
frames[p0] = &slice->m_refPicList[0][0]->m_lowres;
frames[b] = &curFrame->m_lowres;
frames[p1] = &slice->m_refPicList[1][0]->m_lowres;
break;
default:
return;
}
X265_CHECK(curFrame->m_lowres.costEst[b - p0][p1 - b] > 0, "Slice cost not estimated\n")
if (m_param->rc.cuTree && !m_param->rc.bStatRead)//如果应用cutree 并且不是1pass(2pass以上直接获取)
/* update row satds based on cutree offsets */
curFrame->m_lowres.satdCost = frameCostRecalculate(frames, p0, p1, b);//如果当前是B帧直接返回其framecost:costEstAq (qpAqOffset加权)否则,重新计算framecost  经过qpCuTreeOffset加权后的数据
else if (m_param->rc.aqMode)
curFrame->m_lowres.satdCost = curFrame->m_lowres.costEstAq[b - p0][p1 - b];//如果应用自适应量化 获取自适应量化的cost
else
curFrame->m_lowres.satdCost = curFrame->m_lowres.costEst[b - p0][p1 - b];//获取framecost
if (m_param->rc.vbvBufferSize && m_param->rc.vbvMaxBitrate)//如果应用VBV
{
/* aggregate lowres row satds to CTU resolution */
curFrame->m_lowres.lowresCostForRc = curFrame->m_lowres.lowresCosts[b - p0][p1 - b];//获取对应帧计算framecost时的每个8x8块值
uint32_t lowresRow = 0, lowresCol = 0, lowresCuIdx = 0, sum = 0, intraSum = 0;//lowresRow 遍历下采样的行号, lowresCol 用于遍历每行8x8块的每个8x8块, lowresCuIdx当前CTU行对应下采样8x8行位置的 8x8块的index, sum intraSum 用于累加计算当前8x8行的cost
uint32_t scale = m_param->maxCUSize / (2 * X265_LOWRES_CU_SIZE);//因为是下采样图像,底层的8x8块相当于原始帧的16x16 这是CTU与8x8的缩放关系 64/16
uint32_t numCuInHeight = (m_param->sourceHeight + g_maxCUSize - 1) / g_maxCUSize;//获取有多少CTU行
uint32_t widthInLowresCu = (uint32_t)m_8x8Width, heightInLowresCu = (uint32_t)m_8x8Height;//分别获取有多少8x8行和8x8列
double *qp_offset = 0;//自适应量化的权重系数
/* Factor in qpoffsets based on Aq/Cutree in CU costs */
if (m_param->rc.aqMode)//如果应用自适应量化,根据情况选择
qp_offset = (frames[b]->sliceType == X265_TYPE_B || !m_param->rc.cuTree) ? frames[b]->qpAqOffset : frames[b]->qpCuTreeOffset;
for (uint32_t row = 0; row < numCuInHeight; row++)//遍历CTU行
{
lowresRow = row * scale;//对应下采样图像的行号
for (uint32_t cnt = 0; cnt < scale && lowresRow < heightInLowresCu; lowresRow++, cnt++)//遍历一个CTU行对应的scale个的下采样8x8行
{
sum = 0; intraSum = 0;//用于累加计算当前8x8行的cost
lowresCuIdx = lowresRow * widthInLowresCu;//获取当前CTU行对应下采样8x8行位置的 8x8块的index
for (lowresCol = 0; lowresCol < widthInLowresCu; lowresCol++, lowresCuIdx++)//遍历每行每个8x8块的cost
{
uint16_t lowresCuCost = curFrame->m_lowres.lowresCostForRc[lowresCuIdx] & LOWRES_COST_MASK;//获取当前块的8x8块cost  与操作原因:高14位的数字:0 表示 intra  1 表示前向搜索  2表示后向搜索  3 表示bi搜索  
if (qp_offset)//如果应用自适应量化
{
lowresCuCost = (uint16_t)((lowresCuCost * x265_exp2fix8(qp_offset[lowresCuIdx]) + 128) >> 8);//重新计算cost,乘以权重系数
int32_t intraCuCost = curFrame->m_lowres.intraCost[lowresCuIdx]; //获取intracost
curFrame->m_lowres.intraCost[lowresCuIdx] = (intraCuCost * x265_exp2fix8(qp_offset[lowresCuIdx]) + 128) >> 8;//重新计算cost,乘以权重系数
}
curFrame->m_lowres.lowresCostForRc[lowresCuIdx] = lowresCuCost;//重新获得cost
sum += lowresCuCost;//累加cost
intraSum += curFrame->m_lowres.intraCost[lowresCuIdx];//累加intracost
}
curFrame->m_encData->m_rowStat[row].satdForVbv += sum;//每个CTU行对应所有8x8块的cost累加值
curFrame->m_encData->m_rowStat[row].intraSatdForVbv += intraSum;//每个CTU行对应所有8x8块的intracost累加值
}
}
}
}
/** 函数功能             : 初始化lowres并进行下采样、扩边、计算qpCuTreeOffset等信息,获取整帧的像素和和AC能量、计算当前1/2下采样帧的intra SATD值以及最优intra模式、并行处理
/*  调用范围             : 只在WorkerThread::threadMain()和sliceTypeDecide函数中被调用
* \参数 workerThreadID   : 当前运行的内核号
* \返回                  : null * */
void PreLookaheadGroup::processTasks(int workerThreadID) //此函数可在不同线程中执行
{
//workerThreadID为当前的内核号,在sliceTypeDecide中为-1 表示在本线程中继续执行
//在WorkerThread::threadMain()中为大于0的一个内核号,在相应线程中执行
if (workerThreadID < 0)
workerThreadID = m_lookahead.m_pool ? m_lookahead.m_pool->m_numWorkers : 0;
//如果workerThreadID<0 则将其置为最后一个id,因为申请的空间为核数+1,如四个核:0,1,2,3
//-1 则为虚拟的4,此时在本线程中继续执行,其它核号在相应线程中执行
LookaheadTLD& tld = m_lookahead.m_tld[workerThreadID];
m_lock.acquire(); //临界资源加锁
while (m_jobAcquired < m_jobTotal) //m_jobTotal 表示有多少帧需要初始化,m_jobAcquired用于计数,这两个数据是线程之间共有的数据
{
Frame* preFrame = m_preframes[m_jobAcquired++]; //获取需要初始化的帧
ProfileLookaheadTime(m_lookahead.m_preLookaheadElapsedTime, m_lookahead.m_countPreLookahead);//在DETAILED_CU_STATS打开 统计时用到
ProfileScopeEvent(prelookahead);
m_lock.release();//已经读完,可以释放临界资源了
preFrame->m_lowres.init(preFrame->m_fencPic, preFrame->m_poc); //初始化信息,并进行下采样和扩边
if (m_lookahead.m_param->rc.bStatRead && m_lookahead.m_param->rc.cuTree && IS_REFERENCED(preFrame)) 
/* cu-tree offsets were read from stats file */; //如果当前是2pass,并且不是b(非参考帧)(其它 I i B,这三个都是可以当作参考帧的),则直接从1pass获取,无须重新计算
else if (m_lookahead.m_bAdaptiveQuant)
tld.calcAdaptiveQuantFrame(preFrame, m_lookahead.m_param);//初始化m_lowres中的qpCuTreeOffset等信息,获取整帧的像素和和AC能量。
tld.lowresIntraEstimate(preFrame->m_lowres);                  //计算当前1/2下采样帧的intra SATD值以及最优intra模式
preFrame->m_lowresInit = true;                                //标记当前lowres已经初始化完毕
m_lock.acquire();                                             //临界资源加锁
}
m_lock.release();//已完成,可以释放临界资源了
}
/** 函数功能             : 获取帧类型并计算frame-cost
/*  调用范围             : 只在Lookahead::findJob函数中被调用
* \返回                  : null * */
/* called by API thread or worker thread with inputQueueLock acquired */
void Lookahead::slicetypeDecide()
{
PreLookaheadGroup pre(*this); //用于初始化lowres的类
Lowres* frames[X265_LOOKAHEAD_MAX + X265_BFRAME_MAX + 4];//对应下采样数据的信息,长度为lookachead depth 其中frame[0] 为m_lastNonB,Frame[1~depth] 为具体帧
Frame*  list[X265_BFRAME_MAX + 4];                       //用于存储m_param->bframes + 2帧   用于寻找最优的b帧位置
memset(frames, 0, sizeof(frames));
memset(list, 0, sizeof(list));
int maxSearch = X265_MIN(m_param->lookaheadDepth, X265_LOOKAHEAD_MAX);//设置最大需要查看的帧数
maxSearch = X265_MAX(1, maxSearch);                                   //容错处理
{
ScopedLock lock(m_inputLock);                     //防止数据被多线程破坏
//获取Frame*  list数据,顺序编号
Frame *curFrame = m_inputQueue.first();
int j;
for (j = 0; j < m_param->bframes + 2; j++)
{
if (!curFrame) break;
list[j] = curFrame;
curFrame = curFrame->m_next;
}
//获取对应对应下采样数据的信息,长度为lookachead depth
//获取未初始化的视频pre.m_preframes
curFrame = m_inputQueue.first();
frames[0] = m_lastNonB;
for (j = 0; j < maxSearch; j++)
{
if (!curFrame) break;
frames[j + 1] = &curFrame->m_lowres;
if (!curFrame->m_lowresInit)
pre.m_preframes[pre.m_jobTotal++] = curFrame;
curFrame = curFrame->m_next;
}
maxSearch = j;
}
/* perform pre-analysis on frames which need it, using a bonded task group */
//所有数据都在pre操作,无论在threadmain调用还是下面的procesTask都是操作的该对象,多线程并行处理帧初始化
//初始化lowres并进行下采样、扩边、计算pCuTreeOffset等信息,获取整帧的像素和和AC能量、计算当前1/2下采样帧的intra SATD值以及最优intra模式、并行处理
if (pre.m_jobTotal)
{
if (m_pool)
pre.tryBondPeers(*m_pool, pre.m_jobTotal);//在threadmain中触发相应processtask,只要是sleep状态的核都可以触发
pre.processTasks(-1);                         //在本线程中初始化lowres
pre.waitForExit();                            //一直等待所有任务都完成
}
if (m_lastNonB && !m_param->rc.bStatRead &&
((m_param->bFrameAdaptive && m_param->bframes) ||
m_param->rc.cuTree || m_param->scenecutThreshold ||
(m_param->lookaheadDepth && m_param->rc.vbvBufferSize)))   //在1pass 并且m_lastNonB不为空会进入; 2pass等情况不会进入
{
slicetypeAnalyse(frames, false);                            //获取当前GOP的帧类型,获取可参考帧的qpCuTreeOffset值
}
int bframes, brefs;    //bframes:计数当前GOP中B或者b帧数计数 brefs计数当前X265_TYPE_BREF(B)个数
//循环功能:根据配置情况,修正当前GOP的帧类型,如IDR帧插入等情况
//2pass 进入会有相应帧类型, 1pass进入:只有X265_TYPE_B、X265_TYPE_AUTO、X265_TYPE_P
for (bframes = 0, brefs = 0;; bframes++)                       //在1pass中进来的sliceType初始都为X265_TYPE_AUTO,2pass中都会有具体的帧类型
{
Lowres& frm = list[bframes]->m_lowres;                     //获取当前GOP的当前帧的小采样帧(里面含有帧类型)
if (frm.sliceType == X265_TYPE_BREF && !m_param->bBPyramid && brefs == m_param->bBPyramid)//容错处理:一般不进入,当前帧类型为B(可参考B帧)
{                                                                                         //!m_param->bBPyramid 当前B帧关闭,不应该有此帧类型, 
frm.sliceType = X265_TYPE_B;//强制将X265_TYPE_BREF(B) 更改为  X265_TYPE_B(b)          // brefs == m_param->bBPyramid  前面已经保证m_param->bBPyramid = 0 这里必须保证brefs个数为0
x265_log(m_param, X265_LOG_WARNING, "B-ref at frame %d incompatible with B-pyramid\n",
frm.frameNum); 
}
/* pyramid with multiple B-refs needs a big enough dpb that the preceding P-frame stays available.
* smaller dpb could be supported by smart enough use of mmco, but it's easier just to forbid it. */
else if (frm.sliceType == X265_TYPE_BREF && m_param->bBPyramid && brefs &&              //容错处理:一般不进入,当前帧类型为B(可参考B帧) bBPyramid打开(可以有B) 
m_param->maxNumReferences <= (brefs + 3))                                      //bref 已经有参考B帧   L0最多参考个数小于等于(brefs+3) (原因:一个可参考B帧,周边应该有两个b帧)
{
frm.sliceType = X265_TYPE_B;//强制将X265_TYPE_BREF(B) 更改为  X265_TYPE_B(b)
x265_log(m_param, X265_LOG_WARNING, "B-ref at frame %d incompatible with B-pyramid and %d reference frames\n",
frm.sliceType, m_param->maxNumReferences);
}
if (/* (!param->intraRefresh || frm.frameNum == 0) && */ frm.frameNum - m_lastKeyframe >= m_param->keyframeMax)//如果当前帧号与前一个关键帧位置间隔大于最大关键帧间隔,则强制标记当前为IDR帧
{
if (frm.sliceType == X265_TYPE_AUTO || frm.sliceType == X265_TYPE_I)
frm.sliceType = m_param->bOpenGOP && m_lastKeyframe >= 0 ? X265_TYPE_I : X265_TYPE_IDR;//第一帧为IDR, 其它情况: 打开bOpenGOP为X265_TYPE_I,关闭为X265_TYPE_IDR
bool warn = frm.sliceType != X265_TYPE_IDR;
if (warn && m_param->bOpenGOP)
warn &= frm.sliceType != X265_TYPE_I;  //容错处理:一般不进入,防止上条语句没有赋值成功,再赋值一次                                
if (warn)
{
x265_log(m_param, X265_LOG_WARNING, "specified frame type (%d) at %d is not compatible with keyframe interval\n",
frm.sliceType, frm.frameNum);
frm.sliceType = m_param->bOpenGOP && m_lastKeyframe >= 0 ? X265_TYPE_I : X265_TYPE_IDR;
}
}
if (frm.sliceType == X265_TYPE_I && frm.frameNum - m_lastKeyframe >= m_param->keyframeMin) //如果当前为 X265_TYPE_I,并且当前I帧间隔大于等于keyframeMin
{
if (m_param->bOpenGOP)
{
m_lastKeyframe = frm.frameNum;  //因为打开bOpenGOP,除第一帧,其它为X265_TYPE_I,所以在此标记当前I帧位置,为后续计算I帧间隔
frm.bKeyframe = true;           //标记当前为关键帧
}
else
frm.sliceType = X265_TYPE_IDR;  //如果关闭bOpenGOP,当前标记为IDR帧
}
if (frm.sliceType == X265_TYPE_IDR)
{
/* Closed GOP */                   //打开bOpenGOP,只有第一帧会进入其它不会进入此if,因为在打开bOpenGOP中为X265_TYPE_I, 关闭bOpenGOP只要是X265_TYPE_IDR都会进入
m_lastKeyframe = frm.frameNum;     //标记当前I帧位置,为后续计算I帧间隔
frm.bKeyframe = true;              //标记当前为关键帧
if (bframes > 0)
{
list[bframes - 1]->m_lowres.sliceType = X265_TYPE_P; //如果当前不是当前GOP的第一帧,则将其前一帧设置为P帧,并将bframes--,注意当前并不是直接确定为P帧,后续可能会更改
bframes--;
}
}
if (bframes == m_param->bframes || !list[bframes + 1])//如果当前B或者b帧个数已经达到配置上限,或者下一帧无数据(注意:当前位置是当前GOP的最后一帧,所以应该为非B帧)
{
if (IS_X265_TYPE_B(frm.sliceType))
x265_log(m_param, X265_LOG_WARNING, "specified frame type is not compatible with max B-frames\n"); //当前为B或者b帧不合法;一般不进入
if (frm.sliceType == X265_TYPE_AUTO || IS_X265_TYPE_B(frm.sliceType))
frm.sliceType = X265_TYPE_P;//将当前不合法的帧类型强制设置为非B帧(P帧);一般不进入
}
if (frm.sliceType == X265_TYPE_BREF)   //计数当前X265_TYPE_BREF(B)的个数 注意:1pass 一般不进入vbvLookahead中并没有将类型置到.sliceType中,1pass此时全为b帧,2pass 进入统计可参考B帧个数
brefs++;
if (frm.sliceType == X265_TYPE_AUTO)  //在1pass情况下当前没有帧类型,则设置为X265_TYPE_BREF(B) //2pass 一般不进入
frm.sliceType = X265_TYPE_B;
else if (!IS_X265_TYPE_B(frm.sliceType))//如果当前不是B、b帧,退出,遇到P、I、i帧退出
break;
} //for (bframes = 0, brefs = 0;; bframes++)
if (bframes)
list[bframes - 1]->m_lowres.bLastMiniGopBFrame = true;  //记录当前GOP最后一个B帧,因为bframes为B帧个数,B帧时从0开始计数,所以选择bframes - 1
list[bframes]->m_lowres.leadingBframes = bframes;           //设置当前帧前面有几个B\b帧(当前GOP下,此时位置为当前GOP的后向非B帧)
m_lastNonB = &list[bframes]->m_lowres;                      //上面是非B帧才退出的,所以当前为I/P帧,记录最近的非B帧位置
m_histogram[bframes]++;                                     //统计信息,用于统计GOP中B或者b帧数个数
/* insert a bref into the sequence */
if (m_param->bBPyramid && bframes > 1 && !brefs)           //1pass进入:设置中间帧为可参考B帧 2pass不进入
{
list[bframes / 2]->m_lowres.sliceType = X265_TYPE_BREF;
brefs++;
}
/* calculate the frame costs ahead of time for estimateFrameCost while we still have lowres */ //功能,保证RC能够获得framecost
if (m_param->rc.rateControlMode != X265_RC_CQP)         //当前模式不为固定QP模式
{
int p0, p1, b;                                      //临时变量:b 当前帧 p0 前向帧 p1 后向帧
/* For zero latency tuning, calculate frame cost to be used later in RC */
if (!maxSearch)                                   //考虑零延迟,没有lookachead情况,前面没有存储frames列表,在此存储相应帧数
{
for (int i = 0; i <= bframes; i++)            //存储frame[0] 非B帧 frame[1~brames] 当前GOP
frames[i + 1] = &list[i]->m_lowres;
}
/* estimate new non-B cost */
p1 = b = bframes + 1;                                            //选取当前GOP的后向非B帧
p0 = (IS_X265_TYPE_I(frames[bframes + 1]->sliceType)) ? b : 0;   //选取当前GOP的前向非B帧 (p1是I帧,则GOP=1 直接计算当前帧的I-cost即可)
CostEstimateGroup estGroup(*this, frames);                      //多线程计算帧间的framecost
estGroup.singleCost(p0, p1, b);                                //以前向帧p0为参考,p1=b 为当前搜索帧的 P-cost,如果是I帧,则计算I-cost
if (bframes) //计算每个B帧的 frame-cost
{
p0 = 0; // last nonb 初始化当前的前向参考帧为当前GOP的前向非B帧
for (b = 1; b <= bframes; b++)//遍历所有B帧
{
if (frames[b]->sliceType == X265_TYPE_B)
for (p1 = b; frames[p1]->sliceType == X265_TYPE_B; p1++) //寻找后向参考帧
; // find new nonb or bref
else
p1 = bframes + 1; //如果当前为可参考B帧,则其后向参考帧为当前GOP的后向非B帧
estGroup.singleCost(p0, p1, b);//计算当前b帧的framecost,前向p0,后向 p1
if (frames[b]->sliceType == X265_TYPE_BREF) //如果当前为可参考B帧, 则后面的B帧的前向参考帧将为此帧
p0 = b;
}
}
}
m_inputLock.acquire();       //准备抛出帧类型已经确定的帧,现将其加锁,防止多线程重复修改
/* dequeue all frames from inputQueue that are about to be enqueued
* in the output queue. The order is important because Frame can
* only be in one list at a time */
int64_t pts[X265_BFRAME_MAX + 1];//存储当前GOP的pts(可以理解为poc),前向非B帧不存储,只存储所有B帧和后向非B帧
for (int i = 0; i <= bframes; i++)
{
Frame *curFrame;
curFrame = m_inputQueue.popFront();//获取当前列表中最前面的帧  
pts[i] = curFrame->m_pts;     //暂存当前的pts
maxSearch--;
}
m_inputLock.release();  //已经抛出,将临界资源释放
m_outputLock.acquire(); //准备推进 已经决策类型的帧,现将其加锁,防止多线程重复修改
/* add non-B to output queue */
int idx = 0; //临时变量,记录序号
list[bframes]->m_reorderedPts = pts[idx++]; //设置当前GOP后向非B帧(可以理解为P帧)的编码序号  后向P帧获取当前GOP第一个B帧的PTS
m_outputQueue.pushBack(*list[bframes]);//将当前GOP后向非B帧(可以理解为P帧)推入输出列表
/* Add B-ref frame next to P frame in output queue, the B-ref encode before non B-ref frame */
if (bframes > 1 && m_param->bBPyramid)//如果当前B帧个数大于1并且采用可参考B帧
{
for (int i = 0; i < bframes; i++)
{
if (list[i]->m_lowres.sliceType == X265_TYPE_BREF)
{
list[i]->m_reorderedPts = pts[idx++];//设置当前GOP可参考B帧的编码序号
m_outputQueue.pushBack(*list[i]);    //将当前前GOP可参考B帧推入输出列表
}
}
}
/* add B frames to output queue */
for (int i = 0; i < bframes; i++) //将剩余b帧推入输出列表
{
/* push all the B frames into output queue except B-ref, which already pushed into output queue */
if (list[i]->m_lowres.sliceType != X265_TYPE_BREF)
{
list[i]->m_reorderedPts = pts[idx++];//设置b帧的编码序号
m_outputQueue.pushBack(*list[i]);    //将b帧推入输入列表
}
}
//frame[0] 当前GOP的前向非B帧 m_lastNonB 当前GOP的后向非B帧
bool isKeyFrameAnalyse = (m_param->rc.cuTree || (m_param->rc.vbvBufferSize && m_param->lookaheadDepth)) && !m_param->rc.bStatRead;//2pass 不会进入下面操作, 在1pass中会对I帧进行关键帧分析(判定是否为IDR帧)
if (isKeyFrameAnalyse && IS_X265_TYPE_I(m_lastNonB->sliceType))//对当前I帧进行分析 功能:重新计算当前I帧的 frame-cost等信息以及qpCuTreeOffset等信息
{
m_inputLock.acquire(); //加锁
Frame *curFrame = m_inputQueue.first();
frames[0] = m_lastNonB;  //获取最近的非B帧
int j;
for (j = 0; j < maxSearch; j++)   //获取除上面抛出去的帧外的剩余帧
{
frames[j + 1] = &curFrame->m_lowres;
curFrame = curFrame->m_next;
}
m_inputLock.release();//解锁
frames[j + 1] = NULL;
slicetypeAnalyse(frames, true); //获取可参考帧的qpCuTreeOffset值
}
m_outputLock.release();//已经推进,将临界资源释放
}
/** 函数功能             : 重新计算framecost经过qpCuTreeOffset加权后的数据并吧相应信息存储到第一个GOP中,并将相应的B帧设置为参考B帧
/*  调用范围             : 只在slicetypeAnalyse函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 numframes        : frames列表帧数
* \参数 keyframe         : 是否是IDR帧检测
* \返回                  : null * */
void Lookahead::vbvLookahead(Lowres **frames, int numFrames, int keyframe)
{
/************************************************************************/
/*函数功能:重新计算framecost经过qpCuTreeOffset加权后的数据并吧相应信息存储到第一个GOP中,并将相应的B帧设置为参考B帧
/* 只存储到第一个GOP中:plannedType、plannedSatd、indB(用于计数) 
/* 例如当前列表:PbbbPbbbPbbbP  出来之后变为:PbBbPbBbPbBbP 
/* 第一个GOP(PbbbP) 存储了当前所有的framecost
/* 进行标号:P0b1b2b3P4b5b6b7P8b9b10b11P12
/* P0: 无任何存储
/* b1:b3 P8 b5 b6 b7 P12 b9 b10 b11
/* b2:b1 b3 P8 b5 b6 b7 P12 b9 b10 b11
/* b3:P8 b5 b6 b7 P12 b9 b10 b11
/* P4:b1 b2 b3 P8 b5 b6 b7 P12 b9 b10 b11
/************************************************************************/
int prevNonB = 0, curNonB = 1, idx = 0;  //prevNonB当前GOP前向非B帧, curNonB当前GOP后向非B帧, idx 计数frame列表的帧
while (curNonB < numFrames && frames[curNonB]->sliceType == X265_TYPE_B)  //寻找下一个非B帧 ,curNonB指向下一个非B帧
curNonB++;
int nextNonB = keyframe ? prevNonB : curNonB;//第一个GOP下的后向非B帧,IDR检测是由于GOP长度为1,所以指向本身
int nextB = prevNonB + 1;   //第一个B帧
int nextBRef = 0, curBRef = 0; // nextBRef 用于指向当前GOP的B帧参考帧, curBRef 用于标记第一个GOP中的可参考B帧
if (m_param->bBPyramid && curNonB - prevNonB > 1) //curBRef 用于指向当前GOP的B帧参考帧
curBRef = (prevNonB + curNonB + 1) / 2;
int miniGopEnd = keyframe ? prevNonB : curNonB; //如果当前是IDR帧检测,GOP=1,所以最后位置依然是当前I帧,否则最后位置为当前GOP下的后向P帧
while (curNonB < numFrames + !keyframe) //遍历所有GOP
{
/* P/I cost: This shouldn't include the cost of nextNonB */
if (nextNonB != curNonB) //如果不是第一个GOP 
{
int p0 = IS_X265_TYPE_I(frames[curNonB]->sliceType) ? curNonB : prevNonB; //如果curNonB是I帧,则p0=curNonB 否则 p0= prevNonB
frames[nextNonB]->plannedSatd[idx] = vbvFrameCost(frames, p0, curNonB, curNonB);//如果当前帧号curNonB是B帧直接返回其framecost:costEstAq (qpAqOffset加权)否则,重新计算framecost经过qpCuTreeOffset加权后的数据
frames[nextNonB]->plannedType[idx] = frames[curNonB]->sliceType;//获取curNonB的帧类型
/* Save the nextNonB Cost in each B frame of the current miniGop */
if (curNonB > miniGopEnd)//如果当前遍历的不是第一个GOP
{
for (int j = nextB; j < miniGopEnd; j++)//存储当前的P帧cost
{
frames[j]->plannedSatd[frames[j]->indB] = frames[nextNonB]->plannedSatd[idx];
frames[j]->plannedType[frames[j]->indB++] = frames[nextNonB]->plannedType[idx];
}
}
idx++;//指向下一帧
}
/* Handle the B-frames: coded order */
if (m_param->bBPyramid && curNonB - prevNonB > 1) //获取当前GOP的可参考B帧位置
nextBRef = (prevNonB + curNonB + 1) / 2;
for (int i = prevNonB + 1; i < curNonB; i++, idx++)//遍历当前GOP的所有B帧
{
int64_t satdCost = 0;   //用于存储当前帧的famecost
int type = X265_TYPE_B;//初始帧类型为B帧
if (nextBRef)
{
if (i == nextBRef)//如果当前GOP的可参考B帧位置不为0
{
satdCost = vbvFrameCost(frames, prevNonB, curNonB, nextBRef);//直接返回其framecost:costEstAq (qpAqOffset加权)
type = X265_TYPE_BREF; //设置当前帧类型为可参考B帧
}
else if (i < nextBRef)
satdCost = vbvFrameCost(frames, prevNonB, nextBRef, i);//直接返回其framecost:costEstAq (qpAqOffset加权)
else
satdCost = vbvFrameCost(frames, nextBRef, curNonB, i);//直接返回其framecost:costEstAq (qpAqOffset加权)
}
else
satdCost = vbvFrameCost(frames, prevNonB, curNonB, i);//直接返回其framecost:costEstAq (qpAqOffset加权)
frames[nextNonB]->plannedSatd[idx] = satdCost;//存储当前idx帧号的framecost
frames[nextNonB]->plannedType[idx] = type;    //存储帧类型
/* Save the nextB Cost in each B frame of the current miniGop */
for (int j = nextB; j < miniGopEnd; j++)//将相应framecost 存储到第一个GOP相应帧中
{
if (curBRef && curBRef == i) //如果当前是B参考帧 直接退出
break;
if (j >= i && j !=nextBRef)  //j要比i小 或者 j是当前GOP的B参考帧并且是第一个GOP
continue;
frames[j]->plannedSatd[frames[j]->indB] = satdCost; //存储framecost
frames[j]->plannedType[frames[j]->indB++] = type;   //存储帧类型
}
}
prevNonB = curNonB;//重新设置下一个GOP的前向非B帧
curNonB++;
while (curNonB <= numFrames && frames[curNonB]->sliceType == X265_TYPE_B)//重新设置下一个GOP的后向非B帧
curNonB++;
}
frames[nextNonB]->plannedType[idx] = X265_TYPE_AUTO;//将最后一帧帧类型重置
}
/** 函数功能             : 如果当前帧号b是B帧直接返回其framecost:costEstAq (qpAqOffset加权)否则,重新计算framecost  经过qpCuTreeOffset加权后的数据
/*  调用范围             : 只在Lookahead::vbvLookahead函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 p0               : 前向帧
* \参数 p1               : 后向帧
* \参数 b                : 当前帧号
* \返回                  : 返回重新计算的framecost* */
int64_t Lookahead::vbvFrameCost(Lowres **frames, int p0, int p1, int b)
{
CostEstimateGroup estGroup(*this, frames);  //用于计算framecost
int64_t cost = estGroup.singleCost(p0, p1, b);//计算framecost  编码帧:b 前向参考帧:p0, 后向参考帧:p1
if (m_param->rc.aqMode) //如果应用自适应量化
{
if (m_param->rc.cuTree)//如果应用cuTree
return frameCostRecalculate(frames, p0, p1, b);//如果当前帧号b是B帧直接返回其framecost:costEstAq (qpAqOffset加权)否则,重新计算framecost  经过qpCuTreeOffset加权后的数据
else
return frames[b]->costEstAq[b - p0][p1 - b];//直接返回当前整帧的 加权(invQscaleFactor)framecost
}
return cost;
}
/** 函数功能             : 获取当前GOP的帧类型,获取可参考帧的qpCuTreeOffset值
/*  调用范围             : 只在sliceTypeDecide函数中被调用 (一般只在1pass中进入)
* \参数 frames           : 当前搜索的frames列表
* \参数 bKeyframe        : 是否是IDR帧检测
* \返回                  : null * */
void Lookahead::slicetypeAnalyse(Lowres **frames, bool bKeyframe)
{
int numFrames, origNumFrames, keyintLimit, framecnt;                    //numFrames待处理帧数, origNumFrames= X265_MIN(framecnt, keyintLimit), keyintLimit与下一个IDR帧之间的最大间隔帧数, framecnt计数未帧类型决策的个数
int maxSearch = X265_MIN(m_param->lookaheadDepth, X265_LOOKAHEAD_MAX);  //设置最大需要查看的帧数
int cuCount = m_8x8Blocks;                                              //等于(m_8x8Width - 2) * (m_8x8Height - 2)
int resetStart;                                                         //从当前位置开始往后分析的帧类型全部抛弃 置为X265_TYPE_AUTO
bool bIsVbvLookahead = m_param->rc.vbvBufferSize && m_param->lookaheadDepth;//是否应用vbv Lookachead
/* count undecided frames */
for (framecnt = 0; framecnt < maxSearch; framecnt++)                  //计数未帧类型决策的个数
{
Lowres *fenc = frames[framecnt + 1];                              //frames[0]存储最近的非B帧
if (!fenc || fenc->sliceType != X265_TYPE_AUTO)
break;
}
if (!framecnt) //如果是零 一般不会进入                                              
{
if (m_param->rc.cuTree)
cuTree(frames, 0, bKeyframe);//计算可参考帧的qpCuTreeOffset值
return;
}
frames[framecnt + 1] = NULL;   //将最后一帧后面置为null 防止程序出错
keyintLimit = m_param->keyframeMax - frames[0]->frameNum + m_lastKeyframe - 1; //frame[0] 为I帧或者P帧, 此时表示与下一个IDR帧之间的最大间隔帧数
origNumFrames = numFrames = X265_MIN(framecnt, keyintLimit);
if (bIsVbvLookahead)     //根据相应情况更新numFrames值
numFrames = framecnt;
else if (m_param->bOpenGOP && numFrames < framecnt) //说明在当前搜索区域必须插入一个IDR帧,当前已经打开openGOP,所以可以将插入IDR帧置为i
numFrames++;
else if (numFrames == 0)                           // 说明在当前搜索区域第一帧就必须插入一个IDR帧,直接将其置为I帧
{
frames[1]->sliceType = X265_TYPE_I;
return;
}
if (m_bBatchMotionSearch)       //m_param->bFrameAdaptive == X265_B_ADAPT_TRELLIS 执行
{
/* pre-calculate all motion searches, using many worker threads */
CostEstimateGroup estGroup(*this, frames);   //多线程计算帧间的framecost
for (int b = 2; b < numFrames; b++)          //前后帧等间隔计算:如当前帧为 3 ,则计算(2,3,4),(1,3,5),(0,3,6).....
{
for (int i = 1; i <= m_param->bframes + 1; i++)
{
int p0 = b - i;
if (p0 < 0)
continue;
/* Skip search if already done */
if (frames[b]->lowresMvs[0][i - 1][0].x != 0x7FFF)    //如果当前已经搜索计算过,则无须再搜索一次
continue;
/* perform search to p1 at same distance, if possible */
int p1 = b + i;
if (p1 >= numFrames || frames[b]->lowresMvs[1][i - 1][0].x != 0x7FFF) //如果后向帧超出搜索范围或者已经计算过,则将后向帧置为当前帧
p1 = b;
estGroup.add(p0, p1, b); //添加任务,为后面并发执行做准备
}
}
/* auto-disable after the first batch if pool is small */
m_bBatchMotionSearch &= m_pool->m_numWorkers >= 4;  //保证4个内核以上才进行多线程
estGroup.finishBatch();                             //触发并发执行并一整等到所有任务执行完毕:计算每帧与其对应参考帧之间的帧间cost
if (m_bBatchFrameCosts)
{
/* pre-calculate all frame cost estimates, using many worker threads */
for (int b = 2; b < numFrames; b++)             //全部情况计算:如当前帧为 3 ,则计算(2,3,4),(2,3,5),(2,3,6),(2,3,7),(1,3,4),(1,3,5),(1,3,6),(1,3,7).....
{
for (int i = 1; i <= m_param->bframes + 1; i++)
{
if (b < i)                             //必须保证前向帧在当前帧前面
continue;
/* only measure frame cost in this pass if motion searches
* are already done */
if (frames[b]->lowresMvs[0][i - 1][0].x == 0x7FFF) //如果当前已经搜索计算过,则无须再搜索一次
continue;
int p0 = b - i;                                   //获取前向帧
for (int j = 0; j <= m_param->bframes; j++)      //全遍历后向帧
{
int p1 = b + j;                              //获取后向帧
if (p1 >= numFrames)                        //保证不能越界,超出当前的搜索窗口
break;
/* ensure P1 search is done */
if (j && frames[b]->lowresMvs[1][j - 1][0].x == 0x7FFF)  //如果当前已经搜索计算过,则无须再搜索一次
continue;
/* ensure frame cost is not done */
if (frames[b]->costEst[i][j] >= 0)                      //如果当前的最后cost已经指定,则无需继续计算
continue;
estGroup.add(p0, p1, b);                                //添加任务,为后面并发执行做准备
}
}
}
/* auto-disable after the first batch if the pool is not large */
m_bBatchFrameCosts &= m_pool->m_numWorkers > 12;//保证12个内核以上才进行多线程
estGroup.finishBatch();                         //触发并发执行并一整等到所有任务执行完毕:计算每帧与其对应参考帧之间的帧间cost
}
} //end if (m_bBatchMotionSearch)
int numBFrames = 0;                                   //计数从1开始到第一个P帧前的B帧个数
int numAnalyzed = numFrames;                          //numAnalyzed 已经分析的帧数
bool isScenecut = scenecut(frames, 0, 1, true, origNumFrames);//判断当前是否是场景切换帧
/* When scenecut threshold is set, use scenecut detection for I frame placements */
if (m_param->scenecutThreshold && isScenecut)//场景切换帧
{
frames[1]->sliceType = X265_TYPE_I;  //如果是场景切换帧,将当前帧置为I帧
return;
}
if (m_param->bframes)
{
if (m_param->bFrameAdaptive == X265_B_ADAPT_TRELLIS)
{
if (numFrames > 1)
{
char best_paths[X265_BFRAME_MAX + 1][X265_LOOKAHEAD_MAX + 1] = { "", "P" };  //循环队列,存储帧类型路径
int best_path_index = numFrames % (X265_BFRAME_MAX + 1);                     //最后一次帧类型路径存储在队列的位置
/* Perform the frame type analysis. */ 
for (int j = 2; j <= numFrames; j++)                                         //依次求 前面2帧最优的帧类型路径、前面3帧最优的帧类型路径...前面numFrames帧最优的帧类型路径(即最优的帧类型路径)
slicetypePath(frames, j, best_paths);                                    //计算当前搜索长度下的最优帧类型路径
numBFrames = (int)strspn(best_paths[best_path_index], "B");                  //strspn(返回字符串中第一个不在指定字符串中出现的字符下标) 在此的功能为计算前面B帧个数
/* Load the results of the analysis into the frame types. */
for (int j = 1; j < numFrames; j++)
frames[j]->sliceType = best_paths[best_path_index][j - 1] == 'B' ? X265_TYPE_B : X265_TYPE_P;//按照帧类型路径结构给帧类型赋值
}
frames[numFrames]->sliceType = X265_TYPE_P;//将当前搜索列表的最后一帧置为P帧
}
else if (m_param->bFrameAdaptive == X265_B_ADAPT_FAST)
{
CostEstimateGroup estGroup(*this, frames);  //用于计算frame cost
int64_t cost1p0, cost2p0, cost1b1, cost2p1; //存储相应的frame cost
/************************************************************************/
/* 假设当前位置为i  
/* cost1p0  P(i)参考帧 P(i+1)编码帧   
/* cost2p0  P(i+1)参考帧 P(i+2)编码帧  
/* cost1b1  P(i+0)参考帧 B(i+1)编码帧 P(i+2)参考帧
/* cost2p1  P(i+0)参考帧 P(i+2)
/************************************************************************/
for (int i = 0; i <= numFrames - 2; )      //遍历所有帧
{
cost2p1 = estGroup.singleCost(i + 0, i + 2, i + 2, true); //计算P帧的cost (P帧为当前i,i+2为其后向帧)
if (frames[i + 2]->intraMbs[2] > cuCount / 2)             //如果计算的intra块个数大于总共8x8个数的一半,则置当前i+1、i+2为P帧
{
frames[i + 1]->sliceType = X265_TYPE_P;
frames[i + 2]->sliceType = X265_TYPE_P;
i += 2;
continue;
}
cost1b1 = estGroup.singleCost(i + 0, i + 2, i + 1);   //计算当前位置i+1,作为B帧 i+0,i+2作为P帧的frame cost
cost1p0 = estGroup.singleCost(i + 0, i + 1, i + 1);   //计算当前位置i+1,作为B帧 i+0作为P帧的frame cost
cost2p0 = estGroup.singleCost(i + 1, i + 2, i + 2);   //计算当前位置i+2,作为B帧 i+1作为P帧的frame cost
if (cost1p0 + cost2p0 < cost1b1 + cost2p1)           //如果P帧更优,将i+1置为P帧
{
frames[i + 1]->sliceType = X265_TYPE_P;
i += 1;
continue;
}
// arbitrary and untuned
#define INTER_THRESH 300
#define P_SENS_BIAS (50 - m_param->bFrameBias)
frames[i + 1]->sliceType = X265_TYPE_B;           //将i+1置为B帧
int j;
for (j = i + 2; j <= X265_MIN(i + m_param->bframes, numFrames - 1); j++)
{
int64_t pthresh = X265_MAX(INTER_THRESH - P_SENS_BIAS * (j - i - 1), INTER_THRESH / 10);   //获取相应阈值
int64_t pcost = estGroup.singleCost(i + 0, j + 1, j + 1, true);                            //i为前向帧 j为编码帧的 cost
if (pcost > pthresh * cuCount || frames[j + 1]->intraMbs[j - i + 1] > cuCount / 3)         //大于阈值直接退出
break;
frames[j]->sliceType = X265_TYPE_B; //将当前j置为B帧
}
frames[j]->sliceType = X265_TYPE_P;//将当前帧置为P帧
i = j;
}
frames[numFrames]->sliceType = X265_TYPE_P; //将当前搜索列表的最后一帧置为P帧
numBFrames = 0;
while (numBFrames < numFrames && frames[numBFrames + 1]->sliceType == X265_TYPE_B)
numBFrames++;  //回去B帧个数
}
else
{
//固定B帧模式
numBFrames = X265_MIN(numFrames - 1, m_param->bframes);
for (int j = 1; j < numFrames; j++)
frames[j]->sliceType = (j % (numBFrames + 1)) ? X265_TYPE_B : X265_TYPE_P;//每隔numBFrames帧设置一个P帧
frames[numFrames]->sliceType = X265_TYPE_P;//将当前搜索列表的最后一帧置为P帧
}
/* Check scenecut on the first minigop. */
for (int j = 1; j < numBFrames + 1; j++)
{
if (scenecut(frames, j, j + 1, false, origNumFrames)) //判断当前是否场景切换 ,如果是场景切换,曾将当前帧置为P帧
{
frames[j]->sliceType = X265_TYPE_P;
numAnalyzed = j;
break;
}
}
resetStart = bKeyframe ? 1 : X265_MIN(numBFrames + 2, numAnalyzed + 1); //如果当前只是分析当前帧是否为IDR帧 ,则后面分析帧率的全部抛弃,否则抛弃序号numBFrames + 2后面的帧类型
}
else
{
for (int j = 1; j <= numFrames; j++)
frames[j]->sliceType = X265_TYPE_P;   //当前配置为无B帧,全部为P帧
resetStart = bKeyframe ? 1 : 2;          //如果当前只是分析当前帧是否为IDR帧 ,则后面分析帧率的全部抛弃,否则抛弃序号2后面的帧类型
}
if (m_param->rc.cuTree)
cuTree(frames, X265_MIN(numFrames, m_param->keyframeMax), bKeyframe); //计算可参考帧的qpCuTreeOffset值
// if (!param->bIntraRefresh)
for (int j = keyintLimit + 1; j <= numFrames; j += m_param->keyframeMax) //如果当期的阵列表超过IDR间隔数目,则配置下一个IDR帧相应位置为IDR
{
frames[j]->sliceType = X265_TYPE_I;  //设置I帧
resetStart = X265_MIN(resetStart, j + 1); //后面的帧类型全部丢弃
}
if (bIsVbvLookahead)  
vbvLookahead(frames, numFrames, bKeyframe);//如果应用VBV:重新计算framecost经过qpCuTreeOffset加权后的数据并把相应信息存储到第一个GOP中,并将相应的B帧设置为参考B帧
int maxp1 = X265_MIN(m_param->bframes + 1, origNumFrames);//获取后向帧的最大位置
/* Restore frame types for all frames that haven't actually been decided yet. */
for (int j = resetStart; j <= numFrames; j++)//将后面的帧类型丢弃,后面会重新搜索
{
frames[j]->sliceType = X265_TYPE_AUTO;
/* If any frame marked as scenecut is being restarted for sliceDecision, 
* undo scene Transition flag */
if (j <= maxp1 && frames[j]->bScenecut && m_isSceneTransition)
m_isSceneTransition = false;
}
}
/** 函数功能             : 判断当前是否场景切换(是否需要将p1帧类型设置为I帧)
/*  调用范围             : 只在Lookahead::slicetypeAnalyse函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 p0               : 前一帧 注:p0 p1 相邻
* \参数 p1               : 后一帧
* \参数 bRealScenecut    : 当前是否是真正的场景切换判断,而不是预分析
* \参数 numFrames        : 列表中原始帧个数
* \返回                  : 返回当前是否场景切换(是否需要将其帧类型设置为I帧) * */
bool Lookahead::scenecut(Lowres **frames, int p0, int p1, bool bRealScenecut, int numFrames)
{
/* Only do analysis during a normal scenecut check. */
if (bRealScenecut && m_param->bframes)    //如果当前是真正的判断场景切换,在下面进行精细搜索
{
int origmaxp1 = p0 + 1;               //计数需要搜索P1的最大位置
/* Look ahead to avoid coding short flashes as scenecuts. */
origmaxp1 += m_param->bframes;        //加上搜索B帧的的个数
int maxp1 = X265_MIN(origmaxp1, numFrames); //保证当前帧不能超过列表中原始帧个数
bool fluctuate = false; //标记第二轮搜索为场景切换
bool noScenecuts = false;//标记第一轮搜索之后是否有场景切换帧
int64_t avgSatdCost = 0; //累加第一轮搜索的framecost 然后求其平均
if (frames[0]->costEst[1][0] > -1)
avgSatdCost = frames[0]->costEst[1][0];//如果已经有framecost值 获取前向帧的Pcost
int cnt = 1;//用于计数 累加了多少帧的frame cost
/* Where A and B are scenes: AAAAAABBBAAAAAA
* If BBB is shorter than (maxp1-p0), it is detected as a flash
* and not considered a scenecut. */
for (int cp1 = p1; cp1 <= maxp1; cp1++)
{
if (!scenecutInternal(frames, p0, cp1, false)) //判断当前是否场景切换(是否需要将其帧类型设置为I帧) (如果不是进入)
{
/* Any frame in between p0 and cur_p1 cannot be a real scenecut. */
for (int i = cp1; i > p0; i--)             //如果当前结果不是场景切换帧,则将跟区域标记为false
{
frames[i]->bScenecut = false;
noScenecuts = false;
}
}
else if (scenecutInternal(frames, cp1 - 1, cp1, false))//判断当前搜索帧与前一帧是否也是场景切换 (前面已经判断出 cp1相对于p0是场景切换)
{
/* If current frame is a Scenecut from p0 frame as well as Scenecut from
* preceeding frame, mark it as a Scenecut */
frames[cp1]->bScenecut = true;
noScenecuts = true;
}
/* compute average satdcost of all the frames in the mini-gop to confirm 
* whether there is any great fluctuation among them to rule out false positives */
X265_CHECK(frames[cp1]->costEst[cp1 - p0][0]!= -1, "costEst is not done \n");
avgSatdCost += frames[cp1]->costEst[cp1 - p0][0]; //累加framecost
cnt++;//计数
}
/* Identify possible scene fluctuations by comparing the satd cost of the frames.
* This could denote the beginning or ending of scene transitions.
* During a scene transition(fade in/fade outs), if fluctuate remains false,
* then the scene had completed its transition or stabilized */
if (noScenecuts)//如果第一轮搜索 有场景切换帧
{
fluctuate = false;//初始化为false
avgSatdCost /= cnt;//第一轮的平均framecost
for (int i = p1; i <= maxp1; i++)
{
int64_t curCost  = frames[i]->costEst[i - p0][0];//获取当前以P0为参考当前为i的framecost
int64_t prevCost = frames[i - 1]->costEst[i - 1 - p0][0];//获取前一帧以P0为参考的framecost
if (fabs((double)(curCost - avgSatdCost)) > 0.1 * avgSatdCost || 
fabs((double)(curCost - prevCost)) > 0.1 * prevCost) //当前framecost过大 判断为场景切换
{
fluctuate = true; //标记
if (!m_isSceneTransition && frames[i]->bScenecut)//如果当前lookachead还没有场景切换帧 并且上轮搜索当前帧为场景切换帧
{
m_isSceneTransition = true; //置为ture
/* just mark the first scenechange in the scene transition as a scenecut. */
for (int j = i + 1; j <= maxp1; j++) //标记其它帧为非场景切换帧
frames[j]->bScenecut = false;
break;
}
}
frames[i]->bScenecut = false;//不是场景切换帧
}
}
if (!fluctuate && !noScenecuts)//如果第一轮和第二轮都没有场景切换帧
m_isSceneTransition = false; /* Signal end of scene transitioning */
}
/* A frame is always analysed with bRealScenecut = true first, and then bRealScenecut = false,
the former for I decisions and the latter for P/B decisions. It's possible that the first 
analysis detected scenecuts which were later nulled due to scene transitioning, in which 
case do not return a true scenecut for this frame */
if (!frames[p1]->bScenecut)    //如果已经计算过,直接返回false (注:因为初始化为true, false说明已经计算过)
return false;
return scenecutInternal(frames, p0, p1, bRealScenecut);//判断当前是否场景切换(是否需要将其帧类型设置为I帧)(进入此表示是场景切换:再做一次重复计算,主要为了打印log)
}
/** 函数功能             : 判断当前是否场景切换(是否需要将其帧类型设置为I帧)
/*  调用范围             : 只在Lookahead::scenecut函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 p0               : 当前帧 注:p0 p1 不相邻 p1是p0后面某一帧
* \参数 p1               : 后一帧
* \参数 bRealScenecut    : 当前是否是真正的场景切换判断,而不是预分析
* \返回                  : 返回当前是否场景切换(是否需要将其帧类型设置为I帧) * */
bool Lookahead::scenecutInternal(Lowres **frames, int p0, int p1, bool bRealScenecut)
{
Lowres *frame = frames[p1];                 //获取后向帧
CostEstimateGroup estGroup(*this, frames); //用于计算最优frame cost 
estGroup.singleCost(p0, p1, p1);           //计算当前帧与前向参考帧之间的最优frame cost 
int64_t icost = frame->costEst[0][0];      //获取后向帧的I帧编码cost
int64_t pcost = frame->costEst[p1 - p0][0];//获取当前帧与前向参考帧之间的最优frame cost 
int gopSize = frame->frameNum - m_lastKeyframe; //当前的GOP 大小:当前帧到最近关键帧的距离
float threshMax = (float)(m_param->scenecutThreshold / 100.0);//场景切换阈值
/* magic numbers pulled out of thin air */
float threshMin = (float)(threshMax * 0.25);                 //场景切换阈值乘以0.25
double bias = 0.05;                                          //没有初始化,可能在下面语句有bug; 值越大,越容易判断为场景切换
if (bRealScenecut)
{
if (m_param->keyframeMin == m_param->keyframeMax)  //获取bias
threshMin = threshMax;
if (gopSize <= m_param->keyframeMin / 4)
bias = threshMin / 4;
else if (gopSize <= m_param->keyframeMin)
bias = threshMin * gopSize / m_param->keyframeMin;
else
{
bias = threshMin
+ (threshMax - threshMin)
* (gopSize - m_param->keyframeMin)
/ (m_param->keyframeMax - m_param->keyframeMin);
}
}
bool res = pcost >= (1.0 - bias) * icost;             //P帧的cost比I帧的cost要大,说明P帧不优,当前可能存在场景切换导致前后帧的相似性不大,将其res置为ture,表示当前需要场景切换
if (res && bRealScenecut)
{
int imb = frame->intraMbs[p1 - p0];              //当前需要场景切换,输出相应场景切换的信息,如i宏块个数,p宏块个数等
int pmb = m_8x8Blocks - imb;
x265_log(m_param, X265_LOG_DEBUG, "scene cut at %d Icost:%d Pcost:%d ratio:%.4f bias:%.4f gop:%d (imb:%d pmb:%d)\n",
frame->frameNum, icost, pcost, 1. - (double)pcost / icost, bias, gopSize, imb, pmb);
}
return res;                                        //返回当前是否场景切换(是否需要将其帧类型设置为I帧)
}
/** 函数功能             : 计算当前搜索长度下的最优帧类型路径
/*  调用范围             : 只在Lookahead::slicetypeAnalyse函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 length           : 当前搜索frame列表的长度 (1~length)
* \参数 *best_paths      : 存储最优帧类型路径
* \返回                  : null * */
void Lookahead::slicetypePath(Lowres **frames, int length, char(*best_paths)[X265_LOOKAHEAD_MAX + 1])
{
char paths[2][X265_LOOKAHEAD_MAX + 1];                  //暂存帧类型path的cost
int num_paths = X265_MIN(m_param->bframes + 1, length); //计算需要遍历的B帧个数
int64_t best_cost = 1LL << 62;                          //存储最优帧类型path的位置的cost
int idx = 0;                                            //指示最优帧类型path的位置取反
/************************************************************************/
/* 遍历P帧的合适位置    
/* 如当前传入的长度为length为10帧 bframes 为3 
/* 则依次遍历的序列为: x 表示前面已经获取的帧类型
/* xxxxxxxxxP
/* xxxxxxxxBP
/* xxxxxxxBBP
/* xxxxxxBBBP
/************************************************************************/
/* Iterate over all currently possible paths */
for (int path = 0; path < num_paths; path++)            //path表示后面需要置为B帧的个数
{
/* Add suffixes to the current path */
int len = length - (path + 1);                     //获取直接得到帧类型的长度
memcpy(paths[idx], best_paths[len % (X265_BFRAME_MAX + 1)], len); //将先前计算的帧类型 copy到当前位置 为了节省存储空间:取模
memset(paths[idx] + len, 'B', path);                              //设置B帧
strcpy(paths[idx] + len + path, "P");                             //将最后一帧设置为P帧
/* Calculate the actual cost of the current path */
int64_t cost = slicetypePathCost(frames, paths[idx], best_cost); //计算当前PB帧设置的frame cost 累加值 计算范围 当前length
if (cost < best_cost)
{
best_cost = cost;   //更新最优cost 
idx ^= 1;           //更新最优位置的idx
}
}
/* Store the best path. */
memcpy(best_paths[length % (X265_BFRAME_MAX + 1)], paths[idx ^ 1], length);//将当前最优帧类型位置copy到best path  为了节省存储空间:取模
}
/** 函数功能             : 计算当前PB帧设置的frame cost 累加值
/*  调用范围             : 只在Lookahead::slicetypePath函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 path             : 当前frame列表的帧类型:如BPBBBPxxxx(x:暂未制定类型 第一个x前一定是P)
* \参数 threshold        : 当前最优的framecost
* \返回                  : 返回当前PB帧设置的frame cost 累加值 * */
int64_t Lookahead::slicetypePathCost(Lowres **frames, char *path, int64_t threshold)
{
int64_t cost = 0;                             //当前区间的cost, 用于累加
int loc = 1;                                  //第一个B帧位置
int cur_p = 0;                                //当前的P帧(其实是非B帧 因为frame[0] 存储着非B帧)
CostEstimateGroup estGroup(*this, frames);    //用于计算fame cost
path--; /* Since the 1st path element is really the second frame */ //因为frame[0] 存储着非B帧,将path--可以直接对应起当前的fame列表,如:BPBBBPxxxx  P(I) + BPBBBPxxxx
//分区段计算: 如先计算 PBP  在计算 PBBBP
while (path[loc]) //循环遍历当前区间的frame cost
{
int next_p = loc; //标记下一个P帧位置
/* Find the location of the next P-frame. */
while (path[next_p] != 'P')  //寻找下一个P帧
next_p++;
/* Add the cost of the P-frame found above */
cost += estGroup.singleCost(cur_p, next_p, next_p); //计算当前P帧与下一P帧间的framecost,并累加到当前cost中
/* Early terminate if the cost we have found is larger than the best path cost so far */
if (cost > threshold)                              //如果当前cost比最优cost大,说明不优,直接退出终止计算
break;
if (m_param->bBPyramid && next_p - cur_p > 2)     //如果当前的B帧可以参考并且两P帧距离大于2(如果小于等于2,中间只有一个B帧,肯定不是可以参考的)
{
int middle = cur_p + (next_p - cur_p) / 2;    // 选择中间位置(向下取整)作为可参考B帧 : B  
cost += estGroup.singleCost(cur_p, next_p, middle); // 计算中间可参考B帧的frame cost
for (int next_b = loc; next_b < middle && cost < threshold; next_b++) //将当前区间分为两段:当前计算前半段的frame cost
cost += estGroup.singleCost(cur_p, middle, next_b);
for (int next_b = middle + 1; next_b < next_p && cost < threshold; next_b++)//将当前区间分为两段:当前计算后半段的frame cost
cost += estGroup.singleCost(middle, next_p, next_b);
}
else
{
for (int next_b = loc; next_b < next_p && cost < threshold; next_b++) //当前计算B帧不可以作参考帧的情况:分别累加当前B帧:next_b 与前向P帧 后向P帧的 frame cost
cost += estGroup.singleCost(cur_p, next_p, next_b);
}
loc = next_p + 1;  //计算下一个P帧区间:当前位置更改为下一个P帧区间的第一个B帧
cur_p = next_p;    //更新当前P帧位置
}
return cost;          //返回当前PB帧设置的frame cost 累加值
}
/** 函数功能             : 计算可参考帧的qpCuTreeOffset值
/*  调用范围             : 只在slicetypeAnalyse函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 numframes        : frames列表帧数
* \参数 bIntra           : 是否是IDR帧检测
* \返回                  : null * */
void Lookahead::cuTree(Lowres **frames, int numframes, bool bIntra)
{
int idx = !bIntra;         //如果当前是IDR检测,从0帧开始遍历,0帧为非B帧,否则从1帧(B帧)开始遍历
int lastnonb, curnonb = 1; //lastnonb 记录一个GOP的后向非B帧位置  curnonb 记录一个GOP的前向非B帧位置
int bframes = 0;           //存储当前两P帧间的B帧个数
x265_emms();               //清除MMX寄存器中的内容,即初始化(以避免和浮点数操作发生冲突)。
double totalDuration = 0.0;//当前帧数的播放时长 单位 秒
for (int j = 0; j <= numframes; j++)
totalDuration += (double)m_param->fpsDenom / m_param->fpsNum; //累加播放时长
double averageDuration = totalDuration / (numframes + 1); //平均每帧的播放时长 单位秒
int i = numframes;                     //计数,用于倒序寻找最后一个非B帧
int cuCount = m_8x8Width * m_8x8Height;//1/2分辨率中:每帧的8x8块个数
while (i > 0 && frames[i]->sliceType == X265_TYPE_B) //倒序获取最后一个非B帧
i--;
lastnonb = i; //获取最后一个非B帧
/* Lookaheadless MB-tree is not a theoretically distinct case; the same extrapolation could
* be applied to the end of a lookahead buffer of any size.  However, it's most needed when
* lookahead=0, so that's what's currently implemented. */
if (!m_param->lookaheadDepth) //如果配置的lookachead depth 为0
{
if (bIntra)
{
memset(frames[0]->propagateCost, 0, cuCount * sizeof(uint16_t));  //将第一个非B帧全部的传播cost设置为0
memcpy(frames[0]->qpCuTreeOffset, frames[0]->qpAqOffset, cuCount * sizeof(double)); //将AQcopy到qpCuTree
return;
}
std::swap(frames[lastnonb]->propagateCost, frames[0]->propagateCost);  //将第一个非B帧传播cost 与最后一个非B帧的传播cost值交换
memset(frames[0]->propagateCost, 0, cuCount * sizeof(uint16_t));       //将第一个非B帧全部的传播cost设置为0
}
else
{
if (lastnonb < idx)  //当前列表数据太少,直接退出
return;
memset(frames[lastnonb]->propagateCost, 0, cuCount * sizeof(uint16_t));//将最后一个非B帧全部的传播cost初始化为0
}
CostEstimateGroup estGroup(*this, frames);  //用于计算frame cost
while (i-- > idx)  //从最后一个非B帧开始遍历
{
curnonb = i;  //i,指向当前GOP最后一个B帧  
while (frames[curnonb]->sliceType == X265_TYPE_B && curnonb > 0) //获取下一个P帧,curnonb存储当前GOP的前向非B帧
curnonb--;
if (curnonb < idx)  //如果搜索完毕退出,当前curnonb 为P帧
break;
estGroup.singleCost(curnonb, lastnonb, lastnonb);//计算P帧cost:当前编码P帧,lastnonb,参考帧 前一个P帧curnonb
/************************************************************************/
/* 该循环按照每个GOP进行计算: 以GOP PBBBBP为例:(P0,B0,B1,B2,P1) ,从后往前遍历GOP。初始化P0的传播cost  P1的传播cost 继承前面计算的传播cost
/* 首先将P0的传播cost置为0
/*
/* 如果B帧可参考:(P0,b0,B1,b2,P1) 
/*
/*    将B1参考B帧的传播cost置为0
/*
/*    计算b2的传播cost:
/*        每个8x8块的propagate_amount: ((加权intracost)*(intracost-最优cost)/intracost ) 
/*        对应参考帧 B1 P1 中的参考块 累加 加权propagate_amount
/*    计算b0的传播cost:
/*        每个8x8块的propagate_amount: ((加权intracost)*(intracost-最优cost)/intracost ) 
/*        对应参考帧 P0 B1 中的参考块 累加 加权propagate_amount
/*    计算B1的传播cost:
/*        每个8x8块的propagate_amount: ((加权intracost)*(intracost-最优cost)/intracost ) 
/*        对应参考帧 P0 B1 中的参考块 累累加 加权propagate_amount  并更新B1的qpCuTreeOffset值
/* 如果B帧不可参考:(P0,b0,b1,b2,P1) 
/*    计算b2的传播cost:
/*        每个8x8块的propagate_amount: ((加权intracost)*(intracost-最优cost)/intracost ) 
/*        对应参考帧 P0 P1 中的参考块 累加 加权propagate_amount
/*    计算b1的传播cost:
/*        每个8x8块的propagate_amount: ((加权intracost)*(intracost-最优cost)/intracost ) 
/*        对应参考帧 P0 P1 中的参考块 累加 加权propagate_amount
/*    计算b0的传播cost:
/*        每个8x8块的propagate_amount: ((加权intracost)*(intracost-最优cost)/intracost ) 
/*        对应参考帧 P0 P1 中的参考块 累加 加权propagate_amount  
/*
/*计算P1的传播cost
/*   每个8x8块的propagate_amount: ((加权intracost)*(intracost-最优cost)/intracost ) 
/*   对应参考帧 P0 中的参考块 累加 加权propagate_amount  并更新P1的qpCuTreeOffset值
/************************************************************************/
memset(frames[curnonb]->propagateCost, 0, cuCount * sizeof(uint16_t)); //将当前GOP的前向P帧的传播cost初始化为0
bframes = lastnonb - curnonb - 1;                                      //获取当前两P帧间的B帧个数
if (m_param->bBPyramid && bframes > 1)                                 //如果B帧可以作参考
{
int middle = (bframes + 1) / 2 + curnonb;                          //四舍五入 取中间帧
estGroup.singleCost(curnonb, lastnonb, middle);                    //计算当前B的frame cost:编码帧:middle 前向参考帧:curnonb 后向参考帧:lastnonb
memset(frames[middle]->propagateCost, 0, cuCount * sizeof(uint16_t)); //初始化middle帧的 传播cost为0
while (i > curnonb)                                                   //计算当前GOP所有B帧的frame cost
{
int p0 = i > middle ? middle : curnonb;                         //当前当前GOP分为两组,前面一组 前向参考帧p0= curnonb p1 = middle
int p1 = i < middle ? middle : lastnonb;                        //当前当前GOP分为两组,后面一组 前向参考帧p0= middle  p1 = lastnonb
if (i != middle)
{
estGroup.singleCost(p0, p1, i);                            //计算非参考B帧 i 的frame cost  前向帧:p0 后向帧:p1
estimateCUPropagate(frames, averageDuration, p0, p1, i, 0);//计算非参考B帧的传播cost 并累加相应参考帧P0、P1中对应参考块的list的加权cost
}
i--;
}
estimateCUPropagate(frames, averageDuration, curnonb, lastnonb, middle, 1);//计算当前编码帧每个8x8块对应参考块的传播cost、计算其qpCuTreeOffset值
}
else
{
while (i > curnonb)
{
estGroup.singleCost(curnonb, lastnonb, i);                     //计算b帧 i 的frame cost  前向帧:curnonb 后向帧:lastnonb
estimateCUPropagate(frames, averageDuration, curnonb, lastnonb, i, 0);//计算当前编码帧每个8x8块对应参考块的传播cost
i--;                                                                  //指向下一帧
}
}
estimateCUPropagate(frames, averageDuration, curnonb, lastnonb, lastnonb, 1); //计算当前编码帧每个8x8块对应参考块的传播cost、计算其qpCuTreeOffset值
lastnonb = curnonb; //计算下一个GOP
}
if (!m_param->lookaheadDepth) //如果配置的lookachead depth 为0
{
estGroup.singleCost(0, lastnonb, lastnonb); //计算P帧 lastnonb 的frame cost  前向帧:0
estimateCUPropagate(frames, averageDuration, 0, lastnonb, lastnonb, 1); //计算当前编码帧每个8x8块对应参考块的传播cost、计算其qpCuTreeOffset值
std::swap(frames[lastnonb]->propagateCost, frames[0]->propagateCost);   //将第一个非B帧传播cost 与最后一个非B帧的传播cost值交换
}
cuTreeFinish(frames[lastnonb], averageDuration, lastnonb);   //计算参考帧frame[0]的qpCuTreeOffset值 因为当前已经遍历到第一个非B帧 0 号帧
if (m_param->bBPyramid && bframes > 1 && !m_param->rc.vbvBufferSize)  //如果当前配置B帧可参考,并且rc.vbvBufferSize为0
cuTreeFinish(frames[lastnonb + (bframes + 1) / 2], averageDuration, 0); //计算中间帧的qpCuTreeOffset值
}
/** 函数功能             : 计算当前编码帧每个8x8块对应参考块的传播cost、如果当前编码帧是可被参考帧计算其qpCuTreeOffset值
/*  调用范围             : 只在Lookahead::cuTree函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 averageDuration  : 平均每帧的播放时长 单位秒
* \参数 p0               : 前向帧
* \参数 p1               : 后向帧
* \参数 b                : 当前帧号
* \参数 referenced       : 当前计算的b号帧是否是可被参考的
* \返回                  : null * */
void Lookahead::estimateCUPropagate(Lowres **frames, double averageDuration, int p0, int p1, int b, int referenced)
{
uint16_t *refCosts[2] = { frames[p0]->propagateCost, frames[p1]->propagateCost };         //获取参考帧的传播cost 存储地址
int32_t distScaleFactor = (((b - p0) << 8) + ((p1 - p0) >> 1)) / (p1 - p0);               //获取B帧的距离缩放因子:(当前编码帧距离前向帧的距离/GOP长度)*256   四舍五入取整 128 说明是处于中间位置
int32_t bipredWeight = m_param->bEnableWeightedBiPred ? 64 - (distScaleFactor >> 2) : 32; //获取加权系数: 距离缩放因子/4  32 说明处于中间位置
int32_t bipredWeights[2] = { bipredWeight, 64 - bipredWeight };                           //前后两个参考帧的加权系数,当前编码帧距离参考帧越大,加权系数越大
int listDist[2] = { b - p0 - 1, p1 - b - 1 };                                             //存储前后两帧到当前帧的距离:因为是从0开始计数,所以需要减一,主要用于获取存储地址
memset(m_scratch, 0, m_8x8Width * sizeof(int));                                           //初始化为0
uint16_t *propagateCost = frames[b]->propagateCost;                                       //获取当前编码帧的传播cost地址
x265_emms();                                                                              //清除MMX寄存器中的内容,即初始化(以避免和浮点数操作发生冲突)。
double fpsFactor = CLIP_DURATION((double)m_param->fpsDenom / m_param->fpsNum) / CLIP_DURATION(averageDuration);//fps因子:配置每帧播放时间/传入的实际每帧播放时间   一般为1.0
/* For non-referred frames the source costs are always zero, so just memset one row and re-use it. */
if (!referenced)
memset(frames[b]->propagateCost, 0, m_8x8Width * sizeof(uint16_t));                //因为propagateCost函数是累加计算,这里将首行初始化为0,表示往后直接计算当前的传播cost,无须累加
int32_t strideInCU = m_8x8Width;                                                       //获取步长
for (uint16_t blocky = 0; blocky < m_8x8Height; blocky++)                              //按照8x8行进行计算
{
int cuIndex = blocky * strideInCU;                                                 //获取每行的第一个8x8的索引号
primitives.propagateCost(m_scratch, propagateCost,
frames[b]->intraCost + cuIndex, frames[b]->lowresCosts[b - p0][p1 - b] + cuIndex,
frames[b]->invQscaleFactor + cuIndex, &fpsFactor, m_8x8Width); //计算每行的8x8的传播cost(累加传播cost + 加权intracost)*(intracost-最优cost)/intracost
if (referenced)
propagateCost += m_8x8Width;          //如果是非参考帧,则无需指示到下一行,因为无须累加,参考帧需要累加计算,所以需要指示到下一行
for (uint16_t blockx = 0; blockx < m_8x8Width; blockx++, cuIndex++) //遍历当前行的每个8x8块
{
int32_t propagate_amount = m_scratch[blockx];                   //获取当前8x8的传播cost
/* Don't propagate for an intra block. */
if (propagate_amount > 0)                                      //小于0说明intra优,大于0说明inter优
{
/* Access width-2 bitfield. */
int32_t lists_used = frames[b]->lowresCosts[b - p0][p1 - b][cuIndex] >> LOWRES_COST_SHIFT; //0 表示 intra  1 表示前向搜索  2表示后向搜索  3 表示bi搜索   
/* Follow the MVs to the previous frame(s). */
for (uint16_t list = 0; list < 2; list++)    //遍历前后两个list
{
if ((lists_used >> list) & 1)            //查看当前list是否被应用到
{
#define CLIP_ADD(s, x) (s) = (uint16_t)X265_MIN((s) + (x), (1 << 16) - 1)
int32_t listamount = propagate_amount; //将当前list的传播cost赋初值:前面计算的传播cost
/* Apply bipred weighting. */
if (lists_used == 3)
listamount = (listamount * bipredWeights[list] + 32) >> 6; //如果当前最优模式为bi,则根据前后参考帧的加权系数获得当前list的传播cost  +32 是为了四舍五入
MV *mvs = frames[b]->lowresMvs[list][listDist[list]];          //获得前向/后向 参考帧中搜索的MV首地址
/* Early termination for simple case of mv0. */
if (!mvs[cuIndex].word)
{
CLIP_ADD(refCosts[list][cuIndex], listamount); //如果MV为0  直接获取当前8x8块的传播cost   refCosts[list][cuIndex] += listamount
continue;
}
int32_t x = mvs[cuIndex].x;               //当前8x8块MV的x坐标       
int32_t y = mvs[cuIndex].y;               //当前8x8块MV的y坐标     
int32_t cux = (x >> 5) + blockx;          //将MV 坐标 分为 高5位,与低5位分别计算 (5=3+2 分别为1/4分像素精度 和8x8块的大小)
int32_t cuy = (y >> 5) + blocky;          //将MV 坐标 分为 高5位,与低5位分别计算 (5=3+2 分别为1/4分像素精度 和8x8块的大小)
int32_t idx0 = cux + cuy * strideInCU;    //获取参考块首地址所在的8x8块
int32_t idx1 = idx0 + 1;                  //右边块
int32_t idx2 = idx0 + strideInCU;         //下边块
int32_t idx3 = idx0 + strideInCU + 1;     //右下块
x &= 31;                                  //低5位MV x坐标
y &= 31;                                  //低5位MV y坐标
int32_t idx0weight = (32 - y) * (32 - x); //idx0的权重,x,y偏移越大,权重越小,传播cost越小
int32_t idx1weight = (32 - y) * x;        //idx1 右边块的权重 x偏移越小,y偏移越大,权重越小,传播cost越小
int32_t idx2weight = y * (32 - x);        //idx2 下边块的权重 x偏移越大,y偏移越小,权重越小,传播cost越小
int32_t idx3weight = y * x;               //idx3 右下边块的权重 x偏移越小,y偏移越小,权重越小,传播cost越小
/* We could just clip the MVs, but pixels that lie outside the frame probably shouldn't
* be counted. */
if (cux < m_8x8Width - 1 && cuy < m_8x8Height - 1 && cux >= 0 && cuy >= 0)
{
CLIP_ADD(refCosts[list][idx0], (listamount * idx0weight + 512) >> 10);//累加相应块的传播cost:refCosts[list][idx0]+=(listamount * idx0weight + 512) /1024
CLIP_ADD(refCosts[list][idx1], (listamount * idx1weight + 512) >> 10);//累加相应块的传播cost:refCosts[list][idx1]+=(listamount * idx1weight + 512) /1024
CLIP_ADD(refCosts[list][idx2], (listamount * idx2weight + 512) >> 10);//累加相应块的传播cost:refCosts[list][idx2]+=(listamount * idx2weight + 512) /1024
CLIP_ADD(refCosts[list][idx3], (listamount * idx3weight + 512) >> 10);//累加相应块的传播cost:refCosts[list][idx3]+=(listamount * idx3weight + 512) /1024
}
else /* Check offsets individually */
{
if (cux < m_8x8Width && cuy < m_8x8Height && cux >= 0 && cuy >= 0)
CLIP_ADD(refCosts[list][idx0], (listamount * idx0weight + 512) >> 10);  //超出右边界或下边界1个以内8x8块情况 可以选择idx0
if (cux + 1 < m_8x8Width && cuy < m_8x8Height && cux + 1 >= 0 && cuy >= 0)
CLIP_ADD(refCosts[list][idx1], (listamount * idx1weight + 512) >> 10);  //超出右边界2个以内8x8块情况并且超出下边界1个以内8x8块并且超出左边界1个以内8x8块情况 可以选择idx1
if (cux < m_8x8Width && cuy + 1 < m_8x8Height && cux >= 0 && cuy + 1 >= 0)
CLIP_ADD(refCosts[list][idx2], (listamount * idx2weight + 512) >> 10);  //超出下边界2个以内8x8块情况并且超出右边界1个以内8x8块并且超出下边界1个以内8x8块情况 可以选择idx2
if (cux + 1 < m_8x8Width && cuy + 1 < m_8x8Height && cux + 1 >= 0 && cuy + 1 >= 0) 
CLIP_ADD(refCosts[list][idx3], (listamount * idx3weight + 512) >> 10);  //超出边界2个以内8x8块情况,可以选择idx3
}
}
}
}
}
}
if (m_param->rc.vbvBufferSize && m_param->lookaheadDepth && referenced) //如果当前是参考帧
cuTreeFinish(frames[b], averageDuration, b == p1 ? b - p0 : 0);     //计算参考帧qpCuTreeOffset值
}
/** 函数功能             : 计算参考帧qpCuTreeOffset值
/*  调用范围             : 只在Lookahead::cuTree和Lookahead::estimateCUPropagate函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 averageDuration  : 平均每帧的播放时长 单位秒
* \参数 ref0Distance     : 如果是双向参考则为0,如果当前为单向参考则为当前帧号-前向参考帧号
* \返回                  : null * */
void Lookahead::cuTreeFinish(Lowres *frame, double averageDuration, int ref0Distance)
{
int fpsFactor = (int)(CLIP_DURATION(averageDuration) / CLIP_DURATION((double)m_param->fpsDenom / m_param->fpsNum) * 256); //fps因子:配置每帧播放时间*256/传入的实际每帧播放时间   一般为256.0
double weightdelta = 0.0; //初始化加权值
if (ref0Distance && frame->weightedCostDelta[ref0Distance - 1] > 0)  //如果当前是单向预测并且是加权参考   
weightdelta = (1.0 - frame->weightedCostDelta[ref0Distance - 1]); //值为 1- 加权的SATD/不加权的SATD 
/* Allow the strength to be adjusted via qcompress, since the two concepts
* are very similar. */
int cuCount = m_8x8Width * m_8x8Height;                //8x8块个数
double strength = 5.0 * (1.0 - m_param->rc.qCompress); //强度系数
for (int cuIndex = 0; cuIndex < cuCount; cuIndex++)
{
int intracost = (frame->intraCost[cuIndex] * frame->invQscaleFactor[cuIndex] + 128) >> 8; //当前8x8块的加权intracost
if (intracost)
{
int propagateCost = (frame->propagateCost[cuIndex] * fpsFactor + 128) >> 8;          //当前是被参考帧,获取其传播cost*帧率加权
double log2_ratio = X265_LOG2(intracost + propagateCost) - X265_LOG2(intracost) + weightdelta; //获取比例因子
frame->qpCuTreeOffset[cuIndex] = frame->qpAqOffset[cuIndex] - strength * log2_ratio;  // 调整qpCuTreeOffset值, 传播cost越大  qpCuTreeOffset 越小
}
}
}
/** 函数功能             : 如果当前是B帧直接返回其framecost:costEstAq (qpAqOffset加权)否则,重新计算framecost  经过qpCuTreeOffset加权后的数据
/*  调用范围             : 只在Lookahead::getEstimatedPictureCost和Lookahead::vbvFrameCost函数中被调用
* \参数 frames           : 当前搜索的frames列表
* \参数 p0               : 前向帧
* \参数 p1               : 后向帧
* \参数 b                : 当前帧号
* \返回                  : 返回重新计算的framecost* */
/* If MB-tree changes the quantizers, we need to recalculate the frame cost without
* re-running lookahead. */
int64_t Lookahead::frameCostRecalculate(Lowres** frames, int p0, int p1, int b)
{
if (frames[b]->sliceType == X265_TYPE_B)
return frames[b]->costEstAq[b - p0][p1 - b]; //如果是B帧,直接返回framecost,无须重新计算
int64_t score = 0;//用于累加framecost
int *rowSatd = frames[b]->rowSatds[b - p0][p1 - b];//获取每行8x8块 经过invQscaleFactor加权的存储地址
double *qp_offset = frames[b]->qpCuTreeOffset;//获取当前的qpCuTreeOffset(已经在cuTreeFinish经过修正)
x265_emms();//清除MMX寄存器中的内容,即初始化(以避免和浮点数操作发生冲突)。
for (int cuy = m_8x8Height - 1; cuy >= 0; cuy--)//遍历每行的8x8
{
rowSatd[cuy] = 0;//初始化为0
for (int cux = m_8x8Width - 1; cux >= 0; cux--)//遍历每个8x8块
{
int cuxy = cux + cuy * m_8x8Width;//8x8块所在标号
int cuCost = frames[b]->lowresCosts[b - p0][p1 - b][cuxy] & LOWRES_COST_MASK;//获取最优的cost
double qp_adj = qp_offset[cuxy];//获取qpCuTreeOffset
cuCost = (cuCost * x265_exp2fix8(qp_adj) + 128) >> 8;//对最优cost 重新进行invQscaleFactor加权
rowSatd[cuy] += cuCost;//累加当前行的cost
if ((cuy > 0 && cuy < m_8x8Height - 1 &&
cux > 0 && cux < m_8x8Width - 1) ||
m_8x8Width <= 2 || m_8x8Height <= 2)
{
score += cuCost;//只累加边界以内的8x8块
}
}
}
return score;//返回重新计算的framecost
}
/** 函数功能             :单线程计算当前帧与前后参考帧之间的最优frame cost
/*  调用范围             :只在slicetypeDecide()、vbvFrameCost、slicetypeAnalyse、scenecutInternal、slicetypePathCost和cuTree函数中被调用
* \参数 p0               :前向帧
* \参数 p1               :后向帧
* \参数 b                :当前帧号
* \参数 bIntraPenalty    :会加上intra带来的编码代价;其中在slicetypeAnalyse会传入ture(也可能为false)并且bFrameAdaptive == X265_B_ADAPT_FAST,其它为false 
* \返回                  :当前帧与前后参考帧之间的最优frame cost* */
int64_t CostEstimateGroup::singleCost(int p0, int p1, int b, bool intraPenalty)
{
LookaheadTLD& tld = m_lookahead.m_tld[m_lookahead.m_pool ? m_lookahead.m_pool->m_numWorkers : 0];//m_tld空间大小为内核数+1  numTLD = 1 + (m_pool ? m_pool->m_numWorkers : 0); 例如当前为4核,则申请空间大小为5
//当前在当前线程完成,顾取最后一个(0,1,2,3分别表示具体某个核,4表示当前线程)
return estimateFrameCost(tld, p0, p1, b, intraPenalty); //获取取每个8x8块的帧间cost(SATD + mvcost + 4) 并 获取当前b帧在p0、p1参考帧下的最优帧cost inter(SATD+mvcost+4)*10/(13 +b)或者 intra (SATD+5+4)                                 
}
/** 函数功能             : 添加任务,为后面并发执行做准备
/*  调用范围             : 只在Lookahead::slicetypeAnalyse函数中被调用
* \参数 p0               : 前向帧
* \参数 p1               : 后向帧
* \参数 b                : 当前帧号
* \返回                  : null * */
void CostEstimateGroup::add(int p0, int p1, int b)
{
X265_CHECK(m_batchMode || !m_jobTotal, "single CostEstimateGroup instance cannot mix batch modes\n");
m_batchMode = true;                      //设置m_batchMode为true 准备多线程并发
Estimate& e = m_estimates[m_jobTotal++]; //添加新的任务
e.p0 = p0;                              //p0 前向帧  p1 后向帧  b当前帧 (前向帧、后向帧并不是单指前一帧后一帧),如(p0,b,p1)= (5,6,7),(p0,b,p1)= (8,10,11)
e.p1 = p1;
e.b = b;
if (m_jobTotal == MAX_BATCH_SIZE)       //如果当前任务量超过阈值,先在此全部执行完毕
finishBatch();
}
/** 函数功能             : 触发并发执行并一直等到所有任务执行完毕:计算每帧与其对应参考帧之间的帧间cost
/*  调用范围             : 只在Lookahead::slicetypeAnalyse和CostEstimateGroup::add函数中被调用
* \返回                  : null * */
void CostEstimateGroup::finishBatch()
{
if (m_lookahead.m_pool)
tryBondPeers(*m_lookahead.m_pool, m_jobTotal);//在threadmain中触发相应processtask,只要是sleep状态的核都可以触发
processTasks(-1);                                 //在当前线程中计算cost
waitForExit();                                    //等待全部任务完成
m_jobTotal = m_jobAcquired = 0;                   //全部完成将job数目记为0
}
/** 函数功能             : 功能分两个只能执行其中一个:
1. 获取取每个8x8块的帧间cost(SATD + mvcost + 4) 并 获取当前b帧在p0、p1参考帧下的最优帧cost inter(SATD+mvcost+4)*10/(13 +b)或者 intra (SATD+5+4) 
2. Lookachead 多slice并行,执行其中一条slice的每个8x8块的帧间cost(SATD + mvcost + 4)
/*  调用范围             : 只在CostEstimateGroup::finishBatch()和CostEstimateGroup::estimateFrameCost函数中被调用 (分别执行1,2功能)
* \返回                  : null * */
void CostEstimateGroup::processTasks(int workerThreadID)
{
ThreadPool* pool = m_lookahead.m_pool;
int id = workerThreadID;
//workerThreadID为当前的内核号,-1表示在本线程中继续执行
//在WorkerThread::threadMain()中为大于0的一个内核号,在相应线程中执行
if (workerThreadID < 0)
id = pool ? pool->m_numWorkers : 0;
//如果workerThreadID<0 则将其置为最后一个id,因为申请的空间为核数+1,如四个核:0,1,2,3
//-1 则为虚拟的4,此时在本线程中继续执行,其它核号在相应线程中执行
LookaheadTLD& tld = m_lookahead.m_tld[id];
m_lock.acquire();//对临界资源加锁
while (m_jobAcquired < m_jobTotal)
{
int i = m_jobAcquired++;//执行其中一个任务
m_lock.release();//释放临界资源锁
if (m_batchMode)
{
//bFrameAdaptive = 2并且内核数大于4 时执行在此
ProfileLookaheadTime(tld.batchElapsedTime, tld.countBatches);
ProfileScopeEvent(estCostSingle);
Estimate& e = m_estimates[i];                   //获取当前需要计算的当前帧号与相应参考帧号
estimateFrameCost(tld, e.p0, e.p1, e.b, false); //获取取每个8x8块的帧间cost(SATD + mvcost + 4) 并 获取当前b帧在p0、p1参考帧下的最优帧cost inter(SATD+mvcost+4)*10/(13 +b)或者 intra (SATD+5+4)
}
else
{
// Lookachead 多 slice 并行 在内核比较少且bFrameAdaptive != 2 时执行在此  只能在CostEstimateGroup::estimateFrameCost中进入此
ProfileLookaheadTime(tld.coopSliceElapsedTime, tld.countCoopSlices);
ProfileScopeEvent(estCostCoop);
X265_CHECK(i < MAX_COOP_SLICES, "impossible number of coop slices\n");
int firstY = m_lookahead.m_numRowsPerSlice * i;//指向当前slice的第一个8x8行
int lastY = (i == m_jobTotal - 1) ? m_lookahead.m_8x8Height - 1 : m_lookahead.m_numRowsPerSlice * (i + 1) - 1; //指向当前slice的最后一个8x8行
bool lastRow = true;                       //倒序遍历,刚开始是当前slice的最后一行(目的:8x8倒序遍历,最后一行无法获取下边行的mvc)
for (int cuY = lastY; cuY >= firstY; cuY--)//倒序遍历,按照行
{
m_frames[m_coop.b]->rowSatds[m_coop.b - m_coop.p0][m_coop.p1 - m_coop.b][cuY] = 0;//将当前行的cost初始化为0
for (int cuX = m_lookahead.m_8x8Width - 1; cuX >= 0; cuX--)//倒序遍历,遍历每一个8x8
estimateCUCost(tld, cuX, cuY, m_coop.p0, m_coop.p1, m_coop.b, m_coop.bDoSearch, lastRow, i);//获取每个8x8块的帧间cost(SATD + mvcost + 4)
lastRow = false;//标记为不是最后一行,因为是从最后一行开始遍历的
}
}
m_lock.acquire();//对临界资源加锁
}
m_lock.release();//释放临界资源锁
}
/** 函数功能             :获取取每个8x8块的帧间cost(SATD + mvcost + 4) 并 获取当前b帧在p0、p1参考帧下的最优帧cost inter(SATD+mvcost+4)*10/(13 +b)或者 intra (SATD+5+4)
/*  调用范围             :只在CostEstimateGroup::singleCost和CostEstimateGroup::processTasks函数中被调用
* \参数 tld              :当前线程的tld
* \参数 p0               :前向帧
* \参数 p1               :后向帧
* \参数 b                :当前帧号
* \参数 bIntraPenalty    :会加上intra带来的编码代价;在CostEstimateGroup::processTasks中为false 在调用CostEstimateGroup::singleCost中的slicetypeAnalyse会传入ture(也可能为false) 并且bFrameAdaptive == X265_B_ADAPT_FAST
* \返回                  :当前b帧在p0、p1参考帧下的最优帧cost inter(SATD+mvcost+4)*10/(13 +b)或者 intra (SATD+5+4) * */
int64_t CostEstimateGroup::estimateFrameCost(LookaheadTLD& tld, int p0, int p1, int b, bool bIntraPenalty)
{
Lowres*     fenc  = m_frames[b];
x265_param* param = m_lookahead.m_param;
int64_t     score = 0;
if (fenc->costEst[b - p0][p1 - b] >= 0 && fenc->rowSatds[b - p0][p1 - b][0] != -1) //初始化-1.-1说明没有计算过,需要计算,如果不为-1说明已经计算过无须重新计算
score = fenc->costEst[b - p0][p1 - b];                                         //直接获取已经计算过值
else
{
X265_CHECK(p0 != b, "I frame estimates should always be pre-calculated\n");
bool bDoSearch[2];                                                        //用于标示当前是否需要进行搜索(前向、后向)
bDoSearch[0] = p0 < b && fenc->lowresMvs[0][b - p0 - 1][0].x == 0x7FFF;   //如果当前不是同一帧并且是前向帧并且没有搜索过,标记为true
bDoSearch[1] = p1 > b && fenc->lowresMvs[1][p1 - b - 1][0].x == 0x7FFF;   //如果当前不是同一帧并且是后向帧并且没有搜索过,标记为true
#if CHECKED_BUILD
X265_CHECK(!(p0 < b && fenc->lowresMvs[0][b - p0 - 1][0].x == 0x7FFE), "motion search batch duplication L0\n");
X265_CHECK(!(p1 > b && fenc->lowresMvs[1][p1 - b - 1][0].x == 0x7FFE), "motion search batch duplication L1\n");
if (bDoSearch[0]) fenc->lowresMvs[0][b - p0 - 1][0].x = 0x7FFE;           //用于检查检错,如果当前判断为ture需要搜索,则将其初始为-1 (一般情况不会出现问题)
if (bDoSearch[1]) fenc->lowresMvs[1][p1 - b - 1][0].x = 0x7FFE;           //用于检查检错,如果当前判断为ture需要搜索,则将其初始为-1 (一般情况不会出现问题)
#endif
tld.weightedRef.isWeighted = false;                    //初始标记为不加权
if (param->bEnableWeightedPred && bDoSearch[0])
tld.weightsAnalyse(*m_frames[b], *m_frames[p0]);  //判断当前两帧是否进行加权 ,结果存储在weightedRef.isWeighted
fenc->costEst[b - p0][p1 - b] = 0;    //初始化当前帧与前向帧后向帧的cost为0    第一维表示当前帧号poc-前向参考帧号poc   第二维表示后向参考帧号poc-当前帧号poc
fenc->costEstAq[b - p0][p1 - b] = 0;  //初始化当前帧与前向帧后向帧的AQcost为0  第一维表示当前帧号poc-前向参考帧号poc   第二维表示后向参考帧号poc-当前帧号poc
if (!m_batchMode && m_lookahead.m_numCoopSlices > 1 && ((p1 > b) || bDoSearch[0] || bDoSearch[1])) //如果采用lookachead多slice
{
/* Use cooperative mode if a thread pool is available and the cost estimate is
* going to need motion searches or bidir measurements */
memset(&m_slice, 0, sizeof(Slice) * m_lookahead.m_numCoopSlices);
m_lock.acquire();    //加锁,防止多线程读写冲突
X265_CHECK(!m_batchMode, "single CostEstimateGroup instance cannot mix batch modes\n");
m_coop.p0 = p0;     //设置前向帧
m_coop.p1 = p1;     //设置后向帧
m_coop.b = b;       //设置当前帧
m_coop.bDoSearch[0] = bDoSearch[0]; //p0是否需要搜索
m_coop.bDoSearch[1] = bDoSearch[1]; //p1是否需要搜索
m_jobTotal = m_lookahead.m_numCoopSlices; //当前的job个数为slice个数
m_jobAcquired = 0;                        //计数job完成量
m_lock.release();                         //解锁
tryBondPeers(*m_lookahead.m_pool, m_jobTotal);  //触发任务:只要找到一个当前正在sleep的核立即触发
processTasks(-1);                              //在当前线程先执行一个slice
waitForExit();                                 //一直等待所有任务都完成
for (int i = 0; i < m_lookahead.m_numCoopSlices; i++) //遍历当前帧的所有slice
{
fenc->costEst[b - p0][p1 - b] += m_slice[i].costEst;     //存储当前帧与参考帧之间的 satd +  mvcost
fenc->costEstAq[b - p0][p1 - b] += m_slice[i].costEstAq; //存储当前帧与参考帧之间的 加权(satd +  mvcost)
if (p1 == b)
fenc->intraMbs[b - p0] += m_slice[i].intraMbs;       //存储当前帧与参考帧之间的intra块最优个数
}
}
else
{
bool lastRow = true;  //表示当前是否为最后一行
for (int cuY = m_lookahead.m_8x8Height - 1; cuY >= 0; cuY--) //倒序遍历,按照行
{
fenc->rowSatds[b - p0][p1 - b][cuY] = 0;  //将当前行的cost初始化为0
for (int cuX = m_lookahead.m_8x8Width - 1; cuX >= 0; cuX--) //倒序遍历,遍历每一个8x8
estimateCUCost(tld, cuX, cuY, p0, p1, b, bDoSearch, lastRow, -1);//功能获取每个8x8块的帧间cost(SATD + mvcost + 4)
lastRow = false; //标记为不是最后一行,因为是从最后一行开始遍历的
}
}
score = fenc->costEst[b - p0][p1 - b]; //获取相应帧之间的最优cost(SATD+mvcost+4)
if (b != p1)
score = score * 100 / (130 + param->bFrameBias); //根据bFrameBias 调整B帧的权重系数
fenc->costEst[b - p0][p1 - b] = score;              //更新当前cost
}
if (bIntraPenalty)
// arbitrary penalty for I-blocks after B-frames
score += score * fenc->intraMbs[b - p0] / (tld.ncu * 8); //加上intra带来的编码代价,intra越多,代价值越大
return score;
}
/** 函数功能             :获取每个8x8块的帧间cost(SATD + mvcost + 4)
/*  调用范围             :只在 CostEstimateGroup::processTasks和CostEstimateGroup::estimateFrameCost函数中被调用 (一般只在CostEstimateGroup::estimateFrameCost)
* \参数 tld              :当前线程先的tld
* \参数 cuX              :当前帧的8x8 X坐标
* \参数 cuY              :当前帧的8x8 Y坐标
* \参数 p0               :前向帧
* \参数 p1               :后向帧
* \参数 b                :当前帧号
* \参数 bDoSearch        :分别表示p0\p1需不需要search
* \参数 lastRow          :是否是最后一行
* \参数 slice            :在CostEstimateGroup::processTasks为相应slice  在CostEstimateGroup::estimateFrameCost为-1
* \返回                  :null * */
void CostEstimateGroup::estimateCUCost(LookaheadTLD& tld, int cuX, int cuY, int p0, int p1, int b, bool bDoSearch[2], bool lastRow, int slice)
{
Lowres *fref0 = m_frames[p0];                                                     //获取前向帧
Lowres *fref1 = m_frames[p1];                                                     //获取后向帧
Lowres *fenc  = m_frames[b];                                                      //获取当前帧
ReferencePlanes *wfref0 = tld.weightedRef.isWeighted ? &tld.weightedRef : fref0;  //如果在estimateFrameCost中的weightsAnalyse分析为加权前向帧,则选择加权的参考帧buf
const int widthInCU = m_lookahead.m_8x8Width;                                     //lowres 8x8宽度中个数
const int heightInCU = m_lookahead.m_8x8Height;                                   //lowres 8x8高度中个数
const int bBidir = (b < p1);                                                      //判断当前是否可以搜索bi模式(后向帧在当前帧b后面时)
const int cuXY = cuX + cuY * widthInCU;                                           //计算XY
const int cuSize = X265_LOWRES_CU_SIZE;                                           //在1/2采样视频帧中采用8x8块
const intptr_t pelOffset = cuSize * cuX + cuSize * cuY * fenc->lumaStride;        //计算偏移地址
if (bBidir || bDoSearch[0] || bDoSearch[1])                                       //如果需要ME搜索,设置ME
tld.me.setSourcePU(fenc->lowresPlane[0], fenc->lumaStride, pelOffset, cuSize, cuSize); //设置me对应的asm函数,copy待搜索块数据到待搜索块的缓存
/* A small, arbitrary bias to avoid VBV problems caused by zero-residual lookahead blocks. */
int lowresPenalty = 4;                                                            //因为当前搜索的是下采样视频,预估一个编码代价
int listDist[2] = { b - p0 - 1, p1 - b - 1 };                                     //用于标记当前list下的前向/后向第几帧 (如:当前poc = 3  则fenc->lowresMvs[0][0] 表示前向第一帧 poc=2 fenc->lowresMvs[1][1] 表示后向第二帧 poc=5 )
MV mvmin, mvmax;                                                                  //最大MV和最小MV,防止越界
int bcost = tld.me.COST_MAX;                                                      //存储最优cost
int listused = 0;                                                                 //标记当前用了几个list bi模式等于3  0表示 intra
// establish search bounds that don't cross extended frame boundaries 
// 设置最大MV和最小MV,防止越界
mvmin.x = (int16_t)(-cuX * cuSize - 8);
mvmin.y = (int16_t)(-cuY * cuSize - 8);
mvmax.x = (int16_t)((widthInCU - cuX - 1) * cuSize + 8);
mvmax.y = (int16_t)((heightInCU - cuY - 1) * cuSize + 8);
for (int i = 0; i < 1 + bBidir; i++)                                             //遍历list,前向和后向
{
int& fencCost = fenc->lowresMvCosts[i][listDist[i]][cuXY];                   //获取cost的存储地址
if (!bDoSearch[i])                                                           //如果当前不用搜索(前面estimateFrameCost判定,已经搜索过),直接获取cost值
{
COPY2_IF_LT(bcost, fencCost, listused, i + 1);                           //更新最优cost 以及 list个数
continue;
}
int numc = 0;                                                               //计数mvc个数
MV mvc[4], mvp;                                                             //分别用于存储mvc 和mvp
MV* fencMV = &fenc->lowresMvs[i][listDist[i]][cuXY];                        //获取mv的存储地址
ReferencePlanes* fref = i ? fref1 : wfref0;                                 //是否获取加权帧,i=0,P帧,选择wfref(上面已经判断是否加权)i=1,选择fref1(后向帧)
/* Reverse-order MV prediction */
#define MVC(mv) mvc[numc++] = mv;                                                   //宏,用于添加MVC(候选MV)
if (cuX < widthInCU - 1)
MVC(fencMV[1]);                                                         //因为当前是倒序搜索,将当前搜索块右边块添加到MVC
if (!lastRow)
{
MVC(fencMV[widthInCU]);                                                 //因为当前是倒序搜索,将当前搜索块下边块添加到MVC
if (cuX > 0)
MVC(fencMV[widthInCU - 1]);                                         //因为当前是倒序搜索,将当前搜索块左下边块添加到MVC
if (cuX < widthInCU - 1)
MVC(fencMV[widthInCU + 1]);                                         //因为当前是倒序搜索,将当前搜索块右下边块添加到MVC
}
#undef MVC                                                                         //删除宏
if (!numc)                                                                 //如果当前mvc个数,设置mvp为(0,0)
mvp = 0;
else
{
ALIGN_VAR_32(pixel, subpelbuf[X265_LOWRES_CU_SIZE * X265_LOWRES_CU_SIZE]); //字节对齐申请空间8x8用于存储分像素插值参考块
int mvpcost = MotionEstimate::COST_MAX;                                    //用于存储当前最优cost
/* measure SATD cost of each neighbor MV (estimating merge analysis)
* and use the lowest cost MV as MVP (estimating AMVP). Since all
* mvc[] candidates are measured here, none are passed to motionEstimate */
for (int idx = 0; idx < numc; idx++)                                     //遍历当前的MVC
{
intptr_t stride = X265_LOWRES_CU_SIZE;                               //设置默认步长8,会在lowresMC更新真实步长
pixel *src = fref->lowresMC(pelOffset, mvc[idx], subpelbuf, stride); //获取当前qmv下的参考考数据块(分像素作伪插值操作)
int cost = tld.me.bufSATD(src, stride);                              //计算当前块与参考块的SATD值
COPY2_IF_LT(mvpcost, cost, mvp, mvc[idx]);                           //更新当前最优cost 与mvp
}
}
/* ME will never return a cost larger than the cost @MVP, so we do not
* have to check that ME cost is more than the estimated merge cost */
fencCost = tld.me.motionEstimate(fref, mvmin, mvmax, mvp, 0, NULL, s_merange, *fencMV);//搜索获取最优mv到fencMV,返回satd +  mvcost
COPY2_IF_LT(bcost, fencCost, listused, i + 1);                                         //更新最优cost 以及 list个数
}
if (bBidir) /* B, also consider bidir */ //Bi 模式判断
{
/* NOTE: the wfref0 (weightp) is not used for BIDIR */
/* avg(l0-mv, l1-mv) candidate */
ALIGN_VAR_32(pixel, subpelbuf0[X265_LOWRES_CU_SIZE * X265_LOWRES_CU_SIZE]); //存储1/4分像素插值数据
ALIGN_VAR_32(pixel, subpelbuf1[X265_LOWRES_CU_SIZE * X265_LOWRES_CU_SIZE]); //存储1/4分像素插值数据
intptr_t stride0 = X265_LOWRES_CU_SIZE, stride1 = X265_LOWRES_CU_SIZE;
pixel *src0 = fref0->lowresMC(pelOffset, fenc->lowresMvs[0][listDist[0]][cuXY], subpelbuf0, stride0); //获取list0 中的参考数据
pixel *src1 = fref1->lowresMC(pelOffset, fenc->lowresMvs[1][listDist[1]][cuXY], subpelbuf1, stride1); //获取list1 中的参考数据
ALIGN_VAR_32(pixel, ref[X265_LOWRES_CU_SIZE * X265_LOWRES_CU_SIZE]);                                  //用于存储list0参考块与list1参考块的平均值
primitives.pu[LUMA_8x8].pixelavg_pp(ref, X265_LOWRES_CU_SIZE, src0, stride0, src1, stride1, 32);      //获取list0参考块与list1参考块的平均值
int bicost = tld.me.bufSATD(ref, X265_LOWRES_CU_SIZE);                                                //计算当前块与参考块的SATD值
COPY2_IF_LT(bcost, bicost, listused, 3);                                                              //更新最优cost 以及 list个数
/* coloc candidate */
src0 = fref0->lowresPlane[0] + pelOffset;                                                             //尝试list0 与list1对应块作平均的cost
src1 = fref1->lowresPlane[0] + pelOffset;
primitives.pu[LUMA_8x8].pixelavg_pp(ref, X265_LOWRES_CU_SIZE, src0, fref0->lumaStride, src1, fref1->lumaStride, 32);
bicost = tld.me.bufSATD(ref, X265_LOWRES_CU_SIZE);                                                   //更新最优cost 以及 list个数
COPY2_IF_LT(bcost, bicost, listused, 3);                                                             
bcost += lowresPenalty;                                                                             //将当前最优代价加上下采样带来的预估编码代价
}
else /* P, also consider intra */
{
bcost += lowresPenalty;                                                                           //如果当前不是bi模式,将当前某个list的最优代价加上下采样带来的预估编码代价
if (fenc->intraCost[cuXY] < bcost)                                                                //如果当前的cost比intra还大,则更新最优cost为intracost
{
bcost = fenc->intraCost[cuXY];
listused = 0;
}
}
/* do not include edge blocks in the frame cost estimates, they are not very accurate */
const bool bFrameScoreCU = (cuX > 0 && cuX < widthInCU - 1 &&
cuY > 0 && cuY < heightInCU - 1) || widthInCU <= 2 || heightInCU <= 2;  //判断当前是否是边界上的CU,因为边界上的块不够准确,是边界返回false
int bcostAq = (bFrameScoreCU && fenc->invQscaleFactor) ? ((bcost * fenc->invQscaleFactor[cuXY] + 128) >> 8) : bcost;//如果是边界块:bcostAq = bcost 不是边界块:bcostAq = (bcost * fenc.invQscaleFactor[cuXY] + 128) >> 8
if (bFrameScoreCU)
{
if (slice < 0)
{
fenc->costEst[b - p0][p1 - b] += bcost;      //存储当前帧与参考帧之间的 satd +  mvcost
fenc->costEstAq[b - p0][p1 - b] += bcostAq;  //存储当前帧与参考帧之间的 加权(satd +  mvcost)
if (!listused && !bBidir)
fenc->intraMbs[b - p0]++;                //存储当前帧与参考帧之间的intra块最优个数
}
else
{
m_slice[slice].costEst += bcost;           //存储相应slice之间的(SATD+mvcost+4)的累加和
m_slice[slice].costEstAq += bcostAq;       //存储相应slice之间的(SATD+mvcost+4)加权的累加和
if (!listused && !bBidir)
m_slice[slice].intraMbs++;             //存储相应slice之间的intra块最优个数
}
}
fenc->rowSatds[b - p0][p1 - b][cuY] += bcostAq;   //存储相应帧之间的(SATD+mvcost+4)值(经过fenc.invQscaleFactor[cuXY]加权)
fenc->lowresCosts[b - p0][p1 - b][cuXY] = (uint16_t)(X265_MIN(bcost, LOWRES_COST_MASK) | (listused << LOWRES_COST_SHIFT)); //存储当前帧与相应参考帧之间的最优(SATD+mvcost+4)值  其中listused << 14 用于表示当前是intra listx 还是bi模式
}


 

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. ECCV2020 | SNE-RoadSeg:一种基于表面法向量提取的道路可行驶区域分割方法

    精选作品&#xff0c;第一时间送达 这篇文章收录于ECCV2020&#xff0c;是一篇关于无碰撞空间区域分割的文章&#xff0c;整体效果很不错。最主要的核心思想是在表面发现估计器的设计&#xff0c;在得到表面法线后将其用于分割网络的编码器环节&#xff0c;并在特征融合部分&am…...

    2024/4/20 14:25:02
  2. 双眼皮手术不开眼角

    ...

    2024/4/20 14:25:00
  3. 双眼皮手术 痒

    ...

    2024/4/20 14:24:59
  4. 双眼皮手术 眼部肌肉

    ...

    2024/5/2 14:05:58
  5. 双眼皮手术 死亡

    ...

    2024/4/28 17:46:20
  6. angualr4生命周期钩子

    理解Angular提供了生命周期钩子&#xff0c;把这些关键生命时刻暴露出来&#xff0c;赋予我们在它们发生时采取行动的能力。可以将钩子函数理解为在合适的时候做合适的事情。 钩子函数ng4主要提供了8个钩子函数: ngOnchanges input属性(输入属性)发生变化时&#xff0c;会调…...

    2024/4/21 16:26:31
  7. Angular 2 之四 组件(续)

    为了将Angular 2组件显示在DOM树中&#xff0c;组件必须和一个DOM元素关联&#xff0c;该DOM元素称为宿主元素。 组件可以和宿主元素进行以下交互&#xff1a; 监听宿主元素的事件更改宿主元素的属性调用宿主元素的方法。HostBinding标注用于将宿主元素的属性和组件属性绑定&am…...

    2024/4/21 16:26:30
  8. agular6.x 生命周期个人理解

    文章目录文章参考钩子函数介绍组件声明周期构造函数必须掌握的ngOnInitngAfterViewInitngOnDestroy需要了解的ngDoCheckngAfterContentCheckedngAfterViewCheckedngOnChange文章参考 angular——生命周期函数 钩子函数介绍 只调用一次的&#xff0c;我们通常可以用于初始化构…...

    2024/5/2 14:27:21
  9. 【前端vue——系列5】生命周期详讲(vue的生命周期、页面的生命周期)

    系列文章总结 【前端vue——系列1】vue的路由 【前端vue——系列2】vue中的data是函数而不是对象与computed的实现原理 【前端vue——系列3】vue框架的优缺点&#xff1b;vue实现双向绑定 【前端vue——系列4】vuex和angular 【前端vue——系列5】生命周期详讲&#xff08;vue…...

    2024/5/2 10:13:54
  10. 双眼皮手术 芒果

    ...

    2024/5/2 19:17:58
  11. 双眼皮手术 开车

    ...

    2024/4/21 16:26:26
  12. 双眼皮手术 红肿发痒

    ...

    2024/5/2 13:58:58
  13. Angular从零到一3.1 建立routing的步骤

    第3章 建立一个待办事项应用 这一章我们会建立一个更复杂的待办事项应用&#xff0c;当然&#xff0c;登录功能也还保留&#xff0c;这样应用就有了多个相对独立的功能模块。以往的Web应用根据不同的功能跳转到不同的功能页面。但目前前端的趋势是开发一个SPA&#xff08;Singl…...

    2024/4/21 16:26:24
  14. angular6工程有多个module, ng g component创建组件出错

    文章目录文章参考问题描述原因分析解决办法文章参考 ng c g新建组件出现More than one module matches. Use skip-import option to skip importing the component int 问题描述 在引入 Angular Material UI 组件的时候&#xff0c;创建了一个module引入了所有的UI组件&…...

    2024/5/2 9:30:26
  15. 双眼皮手术 发黄

    ...

    2024/5/2 16:34:49
  16. 双眼皮手术 动态图

    ...

    2024/5/2 19:38:31
  17. 双眼皮手术 吃辣椒

    ...

    2024/4/21 16:26:20
  18. 提交数据

    import {Http,Jsonp,Headers} from "angular/http"; private headers new Headers({Content-Type: application/json}); this.http.post(http://localhost:8008/api/test,JSON.stringify({username: admin}), {headers:this.headers}).subscribe(function(res){  …...

    2024/4/21 16:26:19
  19. 双眼皮是蛋白线

    ...

    2024/4/21 16:26:18
  20. 双眼皮什么样子好

    ...

    2024/4/21 16:26:17

最新文章

  1. 解决IDEA下springboot项目打包没有主清单属性

    1.问题出现在SpringBoot学习中 , 运行maven打包后无法运行 报错为spring_boot01_Demo-0.0.1-SNAPSHOT.jar中没有主清单属性 SpringBoot版本为 2.6.13 Java 版本用的8 解决方法 1.执行clean 删除之前的打包 2.进行打包规范设置 2.1 3.进行问题解决 (借鉴了阿里开发社区) 使用…...

    2024/5/2 20:20:35
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. WPS二次开发专题:如何获取应用签名SHA256值

    作者持续关注WPS二次开发专题系列&#xff0c;持续为大家带来更多有价值的WPS开发技术细节&#xff0c;如果能够帮助到您&#xff0c;请帮忙来个一键三连&#xff0c;更多问题请联系我&#xff08;QQ:250325397&#xff09; 在申请WPS SDK授权版时候需要开发者提供应用包名和签…...

    2024/5/1 13:07:33
  4. llama.cpp运行qwen0.5B

    编译llama.cp 参考 下载模型 05b模型下载 转化模型 创建虚拟环境 conda create --prefixD:\miniconda3\envs\llamacpp python3.10 conda activate D:\miniconda3\envs\llamacpp安装所需要的包 cd G:\Cpp\llama.cpp-master pip install -r requirements.txt python conver…...

    2024/5/1 13:25:36
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/1 17:30:59
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/2 16:16:39
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/29 2:29:43
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/2 9:28:15
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/30 9:43:09
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/2 15:04:34
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/29 20:46:55
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/30 22:21:04
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/1 4:32:01
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/30 9:42:22
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/2 9:07:46
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/30 9:42:49
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57