SVT-AV1编码器中实现WPP依赖管理核心调度
一 assign_enc_dec_segments 函数。这个函数是 SVT-AV1 编码器中实现波前并行处理(WPP) 和分段依赖管理的核心调度器之一。
//函数功能:分配编码解码段任务
//返回值Bool
//True 成功分配了一个段给当前线程,调用者应该处理这个段
//False 当前没有可立即处理的段,调用这个应该跳出循环或者进行其他操作
参数:
//segmentPtr: 指向EncDecSegments 结构的指针,包含整个图像分段的状态,依赖关系等信息
//segmentInOutIndex 输入输出参数,输入时可能是旧的段索引,输出时,如果分配成功,被设置为分配段的索引。
//taskPtr 指向EncDecTasks结构体的指针,包含任务的具体信息
srmFifoPtr 指向一个FIFO队列指针,用于在需要时向其他现线程反馈新的任务。
static Bool assign_enc_dec_segments(EncDecSegments *segmentPtr, uint16_t *segmentInOutIndex, EncDecTasks *taskPtr, EbFifo *srmFifoPtr)
{
//初始化标志位,表示是否成功分配了段以供继续处理
Bool continue_processing_flag = FALSE;
//当前段所在的行索引
uint32_t row_segment_index = 0;
//当前正在处理或者检查的段索引
uint32_t segment_index;
//当前段的右邻居 段索引
uint32_t right_segment_index;
//当前段的左下邻居段索引,在WPP依赖中非常重要
uint32_t bottom_left_segment_index;
//反馈行索引,初始化为-1表示暂无反馈需要返送
//如果不为-1 ,则表示需要向指定行反馈一个任务
int16_t feedback_row_index = -1;
//标志位,表示在当前函数调用中是否已经为当前线程自身分配了一个段
//用于防止一次调用分配多个段给同一个线程
uint32_t self_assigned = FALSE;
//根据输入任务的任务类型 input_type 进行不同的处理
switch (taskPtr->input_type)
{
//case 1 任务来自MDC, Mode Decision Configuration 过程输入
case ENCDEC_TASKS_MDC_INPUT:
//整个图像的MDC过程已经完成,提供了完整的画面信息
//因此不需要复杂的逻辑来清除输入依赖
//重置所有编码解码段的行状态:遍历每一行分段
for (uint32_t row_index = 0; row_index < segmentPtr->segment_row_count; ++row_index) {
//将美航的当前处理段索引重置为该行的起始段索引
segmentPtr->row_array[row_index].current_seg_index = segmentPtr->row_array[row_index].starting_seg_index;
}
//立即从第0行的第0个段开始处理
*segmentInOutIndex = segmentPtr->row_array[0].current_seg_index;//分配段索引
taskPtr->input_type = ENCDEC_TASKS_CONTINUE;//将任务类型标记为 继续 以便后续处理
//递增第0行的当前段索引,为下一次分配做准备
++segmentPtr->row_array[0].current_seg_index;
//设置标志位TRUE,表示成功分配了段,调用者应继续处理此段
continue_processing_flag = TRUE;
break; //跳出siwtch
//case 2:任务指定了要处理的编码解码行ENCDEC_INPUT
case ENCDEC_TASKS_ENCDEC_INPUT:
//立即从指定行enc_dec_segment_row 的当前段开始处理
*segmentInOutIndex = segmentPtr->row_array[taskPtr->enc_dec_segment_row].current_seg_index;
taskPtr->input_type = ENCDEC_TASKS_CONTINUE;//将任务类型标记为继续
//递增指定行的当前段索引
++segmentPtr->row_array[taskPtr->enc_dec_segment_row].current_seg_index;
//设置标志位为True,表示成功分配了段
continue_processing_flag = TRUE;
break;
//任务类型是继续COnitnue,表示一个段已经完成处理,需要检查并更新依赖关系,可能分配下一个段。
case ENCDEC_TASKS_CONTINUE:
//获取刚刚处理完的段索引
segment_index = *segmentInOutIndex;
//计算这个段所在的行索引 将一维段索引映射到二维的行索引
tow_segment_index = segment_idnex / segmentPtr->segment_band_count;
//计算当前段的右邻居段索引 同一行,下一个段
right_segment_index = segment_idnex + 1;
//计算当前段的左下邻居段索引,相对左下位置,这是WPP依赖的关键
bottom_left_segment_index = segment_idnex + segmentPtr->segment_band_count;
//检查并处理右邻居段的依赖
//首先检查右邻居是否存在,当前段索引是否小雨当前行的结束段索引
if (segment_index < segmentPtr->row_array[row_segment_index].assignment_mutex) {
//递减右邻居段的依赖计数器,每个段初始时间能有依赖,例如依赖于左上和上方的段
--segmentPtr->dep_map.dependency_map[right_segment_index];
//检查右邻居段的依赖计数器是否降为0, 意味着它所依赖的所有段都已经处理完成
if (segmentPtr->dep_map.dependency_map[right_segment_index] == 0) {
//依赖已满足,可以将右邻居段分配给当前线程处理
*segmentInOutIndex = segmentPtr->row_array[row_segment_index].current_seg_index;
//递增该行的当前段索引,为下次分配准备
++segmentPtr->row_array[row_segment_index].current_seg_index;
//标记当前线程已经为自己分配了一个段
self_assigned = TRUE;
//设置标志位,表示有段需要处理
continue_processing_flag = TRUE;
}
//释放当前的互斥锁
svt_release_mutex(segmentPtr->row_array[row_segment_index].assignment_mutex);
}
//检查并处理左下邻居段的依赖
//检查左下邻居是否存在1 当前行不是最后一行,2 左下邻居段索引在该下一行的有效范围内
if (row_segment_index < segmentPtr->segment_row_count - 1 && bottom_left_segment_index >= segmentPtr->row_array[row_segment_index + 1].starting_seg_index) {
//获取下一行的互斥锁,以安全的修改该行的共享依赖状态
svt_block_on_mutex(segmentPtr->row_array[row_segment_index + 1].assignment_mutex);
//递减左下邻居段的依赖计数器是是否降为0
if (segmentPtr->dep_map.dependency[bottom_left_segment_index] == 0) {
//如果当前线程在检查右邻居时已经在自身分配了一个段
if (self_assigned == TRUE)
//则无法同时处理左下段,记录需要反馈的行索引 下一行
//稍后将创建一个新任务放入队列,让其他线程处理这个就绪的段
feedback_row_index = (int16_t)row_segment_index + 1;
else {
*segmentInOutIndex = segmentPtr->row_array[row_segment_index + 1].current_seg_index;
++segmentPtr->row_array[row_segment_index + 1].current_seg_index;
continue_processing_flag = TRUE;
}
}
//释放下一行的互斥锁
svtt_release_mutex(segmentPtr->row_array[row_segment_index + 1].assignment_mutex);
}
//如果之前发现左下段就绪但是自身无法处理,因为已经分配了右段,则需要反馈任务
if (feedback_row_index > 0) {
EbObjectWrapper *wrapper_ptr;
//从FIFO队列中获取一个空的任务包装器对象
svt_get_empty_object(srmFifoPtr, &wrapper_ptr);
//获取包装器中的任务对象
EncDecTasks *feedback_task = (EncDecTasks*)wrapper_ptr->object_ptr;
//设置任务类型为ENCDEC_INPUT,并指定需要处理的行
feedback_task->input_type = ENCDEC_TASKS_ENCDEC_INPUT;
feedback_task->enc_dec_segment_row = feedback_row_index; //设置需要处理的行索引
//负值必要的上下文信息
feedback_task->pcs_wrapper = taskPtr->pcs_wrapper;
feedback_task->file_group_index = taskPtr->tile_group_index;
//将填充好的任务对象发布到FIFO队列中,等待其他工作线程获取并处理
svt_post_full_object(wrapper_ptr);
}
break;//跳出switch
}
default: break;
}
//返回标志位,告知调用者是否分配了段以供处理
return continue_processing_flag;
}
1 依赖管理,此函数的核心是管理图像分段间的空间依赖关系,在视频编码中,处理一个编码块通常需要上方,左上方和右上方的块信息。
assign_enc_dec_segments 通过一个依赖映射表dependency_map 来跟踪每个分段对其前置分段的依赖。当一个分段处理完成是,会递减其右邻居和左下邻居的依赖计数。 只有当依赖计数降为0时,该分段被认为就绪,可以背分配处理。
2 实现波前并行处理:这个函数时SVT-AV1 实现Wavefront Parallel Processing 的关键。WPP允许编码器在处理第N行的几个块之后,可以开始处理第N+1行,不必等待整行N处理完毕,这极大的提高了多核CPU的利用率和编码并行度。函数中bottom_left_segment_index检查和反馈机制正是为了实现这种波浪式的推进。
3 任务调度与负载均衡:函数作为一个调度器,动态地将就绪的分配给工作现场,痛殴互斥锁,保护共享状态current_seg_index。确保了多线程环境下的正确性。当某个现场完成分段后,会立即尝试获取下一个就绪的分段,有助于保持所有CPU核心的繁忙状态。
4 通信与同步:函数通过FIFO队列进行现场间的通信。当一个现场发现自己触发了另一个分段就绪,但是又无法立即处理时
self_assigned == TRUE的情况,会创建一个新的任务并放入队列,通知其他工作线程有新的工作可用,这是一种高效的工作窃取Work Stealing和协同机制。