PR #23720 opened by charlymp URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23720 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23720.patch
Each tile of a whole-frame single-slice picture is an independent substream; decode them concurrently, then filter the frame serially. Around 2.3x faster (up to 3.5x) at 8 threads on tiled streams. Signed-off-by: Charly Morgand-Poyac <[email protected]> # Summary of changes Briefly describe what this PR does and why. <!-- If this PR requires new FATE test samples, attach them to the PR and list their target paths below (relative to the fate-suite root). Attached filenames must match the sample's filename: ```fate-samples # e.g. vorbis/new-sample.ogg ``` --> >From d7e2bce117f8f542529cad21b9849879d6de6743 Mon Sep 17 00:00:00 2001 From: Charly Morgand-Poyac <[email protected]> Date: Mon, 6 Jul 2026 16:31:07 +0200 Subject: [PATCH] lavc/hevcdec: decode tiles in parallel under slice threading Each tile of a whole-frame single-slice picture is an independent substream; decode them concurrently, then filter the frame serially. Around 2.3x faster (up to 3.5x) at 8 threads on tiled streams. Signed-off-by: Charly Morgand-Poyac <[email protected]> --- libavcodec/hevc/filter.c | 68 +++++++++++++- libavcodec/hevc/hevcdec.c | 184 +++++++++++++++++++++++++++++++++++++- libavcodec/hevc/hevcdec.h | 6 ++ tests/fate/hevc.mak | 5 ++ 4 files changed, 260 insertions(+), 3 deletions(-) diff --git a/libavcodec/hevc/filter.c b/libavcodec/hevc/filter.c index e897ad5d58..2c00886f65 100644 --- a/libavcodec/hevc/filter.c +++ b/libavcodec/hevc/filter.c @@ -760,7 +760,7 @@ void ff_hevc_deblocking_boundary_strengths(HEVCLocalContext *lc, const HEVCLayer ((!s->sh.slice_loop_filter_across_slices_enabled_flag && lc->boundary_flags & BOUNDARY_UPPER_SLICE && (y0 % (1 << sps->log2_ctb_size)) == 0) || - (!pps->loop_filter_across_tiles_enabled_flag && + ((!pps->loop_filter_across_tiles_enabled_flag || lc->tile_bs_defer) && lc->boundary_flags & BOUNDARY_UPPER_TILE && (y0 % (1 << sps->log2_ctb_size)) == 0))) boundary_upper = 0; @@ -798,7 +798,7 @@ void ff_hevc_deblocking_boundary_strengths(HEVCLocalContext *lc, const HEVCLayer ((!s->sh.slice_loop_filter_across_slices_enabled_flag && lc->boundary_flags & BOUNDARY_LEFT_SLICE && (x0 % (1 << sps->log2_ctb_size)) == 0) || - (!pps->loop_filter_across_tiles_enabled_flag && + ((!pps->loop_filter_across_tiles_enabled_flag || lc->tile_bs_defer) && lc->boundary_flags & BOUNDARY_LEFT_TILE && (x0 % (1 << sps->log2_ctb_size)) == 0))) boundary_left = 0; @@ -865,6 +865,70 @@ void ff_hevc_deblocking_boundary_strengths(HEVCLocalContext *lc, const HEVCLayer } } +/* boundary strengths for a CTB's tile-boundary edges only (left/top), clamped to + * the frame; deferred by the tile-parallel decoder until all tiles are decoded */ +void ff_hevc_tile_boundary_bs(HEVCLocalContext *lc, const HEVCLayerContext *l, + const HEVCPPS *pps, int x0, int y0) +{ + const HEVCSPS *const sps = pps->sps; + const HEVCContext *s = lc->parent; + const MvField *tab_mvf = s->cur_frame->tab_mvf; + const int log2_min_pu_size = sps->log2_min_pu_size; + const int log2_min_tu_size = sps->log2_min_tb_size; + const int min_pu_width = sps->min_pu_width; + const int min_tu_width = sps->min_tb_width; + const int ctb_size = 1 << sps->log2_ctb_size; + int i, bs; + + if ((lc->boundary_flags & BOUNDARY_UPPER_TILE) && y0 > 0) { + const RefPicList *rpl_top = (lc->boundary_flags & BOUNDARY_UPPER_SLICE) ? + ff_hevc_get_ref_list(s->cur_frame, x0, y0 - 1) : + s->cur_frame->refPicList; + int yp_pu = (y0 - 1) >> log2_min_pu_size, yq_pu = y0 >> log2_min_pu_size; + int yp_tu = (y0 - 1) >> log2_min_tu_size, yq_tu = y0 >> log2_min_tu_size; + int len = FFMIN(ctb_size, sps->width - x0); + for (i = 0; i < len; i += 4) { + int x_pu = (x0 + i) >> log2_min_pu_size; + int x_tu = (x0 + i) >> log2_min_tu_size; + const MvField *top = &tab_mvf[yp_pu * min_pu_width + x_pu]; + const MvField *curr = &tab_mvf[yq_pu * min_pu_width + x_pu]; + uint8_t top_cbf = l->cbf_luma[yp_tu * min_tu_width + x_tu]; + uint8_t curr_cbf = l->cbf_luma[yq_tu * min_tu_width + x_tu]; + if (curr->pred_flag == PF_INTRA || top->pred_flag == PF_INTRA) + bs = 2; + else if (curr_cbf || top_cbf) + bs = 1; + else + bs = boundary_strength(s, curr, top, rpl_top); + l->horizontal_bs[((x0 + i) + y0 * l->bs_width) >> 2] = bs; + } + } + + if ((lc->boundary_flags & BOUNDARY_LEFT_TILE) && x0 > 0) { + const RefPicList *rpl_left = (lc->boundary_flags & BOUNDARY_LEFT_SLICE) ? + ff_hevc_get_ref_list(s->cur_frame, x0 - 1, y0) : + s->cur_frame->refPicList; + int xp_pu = (x0 - 1) >> log2_min_pu_size, xq_pu = x0 >> log2_min_pu_size; + int xp_tu = (x0 - 1) >> log2_min_tu_size, xq_tu = x0 >> log2_min_tu_size; + int len = FFMIN(ctb_size, sps->height - y0); + for (i = 0; i < len; i += 4) { + int y_pu = (y0 + i) >> log2_min_pu_size; + int y_tu = (y0 + i) >> log2_min_tu_size; + const MvField *left = &tab_mvf[y_pu * min_pu_width + xp_pu]; + const MvField *curr = &tab_mvf[y_pu * min_pu_width + xq_pu]; + uint8_t left_cbf = l->cbf_luma[y_tu * min_tu_width + xp_tu]; + uint8_t curr_cbf = l->cbf_luma[y_tu * min_tu_width + xq_tu]; + if (curr->pred_flag == PF_INTRA || left->pred_flag == PF_INTRA) + bs = 2; + else if (curr_cbf || left_cbf) + bs = 1; + else + bs = boundary_strength(s, curr, left, rpl_left); + l->vertical_bs[(x0 + (y0 + i) * l->bs_width) >> 2] = bs; + } + } +} + #undef LUMA #undef CB #undef CR diff --git a/libavcodec/hevc/hevcdec.c b/libavcodec/hevc/hevcdec.c index b4c2d82e8d..8cc8b6ffbf 100644 --- a/libavcodec/hevc/hevcdec.c +++ b/libavcodec/hevc/hevcdec.c @@ -2693,7 +2693,10 @@ static void hls_decode_neighbour(HEVCLocalContext *lc, int ctb_addr_rs = pps->ctb_addr_ts_to_rs[ctb_addr_ts]; int ctb_addr_in_slice = ctb_addr_rs - s->sh.slice_addr; - l->tab_slice_address[ctb_addr_rs] = s->sh.slice_addr; + /* the tile-parallel path pre-fills this serially; skip the write to avoid + * racing a neighbour tile's slice-boundary read below */ + if (!lc->tile_bs_defer) + l->tab_slice_address[ctb_addr_rs] = s->sh.slice_addr; if (pps->entropy_coding_sync_enabled_flag) { if (x_ctb == 0 && (y_ctb & (ctb_size - 1)) == 0) @@ -2749,6 +2752,8 @@ static int hls_decode_entry(HEVCContext *s, GetBitContext *gb) int ctb_addr_ts = pps->ctb_addr_rs_to_ts[s->sh.slice_ctb_addr_rs]; int ret; + lc->tile_bs_defer = 0; /* guard against a stale flag from an aborted tile frame */ + while (more_data && ctb_addr_ts < sps->ctb_size) { int ctb_addr_rs = pps->ctb_addr_ts_to_rs[ctb_addr_ts]; @@ -3011,6 +3016,173 @@ static int hls_slice_data_wpp(HEVCContext *s, const H2645NAL *nal) return res; } +/* decode one tile (substream job), reconstruction only; in-loop filtering is + * deferred to a serial pass so cross-tile edges read valid neighbours */ +static int hls_decode_entry_tile(AVCodecContext *avctx, void *hevc_lclist, + int job, int thread) +{ + HEVCLocalContext *lc = &((HEVCLocalContext*)hevc_lclist)[thread]; + const HEVCContext *const s = lc->parent; + const HEVCLayerContext *const l = &s->layers[s->cur_layer]; + const HEVCPPS *const pps = s->pps; + const HEVCSPS *const sps = pps->sps; + const int ctb_size = 1 << sps->log2_ctb_size; + const int ctb_width = (sps->width + ctb_size - 1) >> sps->log2_ctb_size; + const uint8_t *data = s->data + s->sh.offset[job]; + const size_t data_size = s->sh.size[job]; + int more_data = 1, ret, my_tile, seen; + int ctb_addr_ts = pps->ctb_addr_rs_to_ts[s->sh.slice_ctb_addr_rs]; + + lc->tile_bs_defer = 1; + + /* walk to the first CTB (tile-scan order) of the job-th tile */ + for (seen = 0; seen < job && ctb_addr_ts < sps->ctb_size; ) { + ctb_addr_ts++; + if (ctb_addr_ts < sps->ctb_size && + pps->tile_id[ctb_addr_ts] != pps->tile_id[ctb_addr_ts - 1]) + seen++; + } + my_tile = pps->tile_id[ctb_addr_ts]; + + while (more_data && ctb_addr_ts < sps->ctb_size && + pps->tile_id[ctb_addr_ts] == my_tile) { + int ctb_addr_rs = pps->ctb_addr_ts_to_rs[ctb_addr_ts]; + int x_ctb = (ctb_addr_rs % ctb_width) << sps->log2_ctb_size; + int y_ctb = (ctb_addr_rs / ctb_width) << sps->log2_ctb_size; + + hls_decode_neighbour(lc, l, pps, sps, x_ctb, y_ctb, ctb_addr_ts); + + ret = ff_hevc_cabac_init(lc, pps, ctb_addr_ts, data, data_size, 1); + if (ret < 0) { + l->tab_slice_address[ctb_addr_rs] = -1; + return ret; + } + + hls_sao_param(lc, l, pps, sps, + x_ctb >> sps->log2_ctb_size, y_ctb >> sps->log2_ctb_size); + + l->deblock[ctb_addr_rs].beta_offset = s->sh.beta_offset; + l->deblock[ctb_addr_rs].tc_offset = s->sh.tc_offset; + l->filter_slice_edges[ctb_addr_rs] = s->sh.slice_loop_filter_across_slices_enabled_flag; + + more_data = hls_coding_quadtree(lc, l, pps, sps, x_ctb, y_ctb, sps->log2_ctb_size, 0); + if (more_data < 0) { + l->tab_slice_address[ctb_addr_rs] = -1; + return more_data; + } + ctb_addr_ts++; + } + return ctb_addr_ts; +} + +/* tile-parallel slice decode: each tile is an independent substream, decoded + * concurrently, then the frame is filtered serially (correct even with + * loop_filter_across_tiles, all samples and boundary strengths being ready by then) */ +static int hls_slice_data_tiles(HEVCContext *s, const H2645NAL *nal) +{ + const HEVCPPS *const pps = s->pps; + const HEVCSPS *const sps = pps->sps; + const HEVCLayerContext *const l = &s->layers[s->cur_layer]; + const uint8_t *data = nal->data; + int length = nal->size; + const int ctb_size = 1 << sps->log2_ctb_size; + const int ctb_width = (sps->width + ctb_size - 1) >> sps->log2_ctb_size; + const int nb_tiles = s->sh.num_entry_point_offsets + 1; + const int start_ts = pps->ctb_addr_rs_to_ts[s->sh.slice_ctb_addr_rs]; + int64_t offset, startheader, cmpt = 0; + int j, res = 0, ctb_addr_ts, x_ctb = 0, y_ctb = 0; + int *ret; + + /* grow the per-thread local context array (as WPP does) */ + if (s->avctx->thread_count > s->nb_local_ctx) { + HEVCLocalContext *tmp = av_malloc_array(s->avctx->thread_count, sizeof(*s->local_ctx)); + if (!tmp) + return AVERROR(ENOMEM); + memcpy(tmp, s->local_ctx, sizeof(*s->local_ctx) * s->nb_local_ctx); + av_free(s->local_ctx); + s->local_ctx = tmp; + for (unsigned i = s->nb_local_ctx; i < s->avctx->thread_count; i++) { + tmp = &s->local_ctx[i]; + memset(tmp, 0, sizeof(*tmp)); + tmp->logctx = s->avctx; + tmp->parent = s; + tmp->common_cabac_state = &s->cabac; + } + s->nb_local_ctx = s->avctx->thread_count; + } + + /* substream byte offsets/sizes (entry points delimit tiles, same as WPP rows) */ + offset = s->sh.data_offset; + for (j = 0, cmpt = 0, startheader = offset + s->sh.entry_point_offset[0]; j < nal->skipped_bytes; j++) + if (nal->skipped_bytes_pos[j] >= offset && nal->skipped_bytes_pos[j] < startheader) { startheader--; cmpt++; } + for (int i = 1; i < s->sh.num_entry_point_offsets; i++) { + offset += (s->sh.entry_point_offset[i - 1] - cmpt); + for (j = 0, cmpt = 0, startheader = offset + s->sh.entry_point_offset[i]; j < nal->skipped_bytes; j++) + if (nal->skipped_bytes_pos[j] >= offset && nal->skipped_bytes_pos[j] < startheader) { startheader--; cmpt++; } + s->sh.size[i] = s->sh.entry_point_offset[i] - cmpt; + s->sh.offset[i] = offset; + } + offset += s->sh.entry_point_offset[s->sh.num_entry_point_offsets - 1] - cmpt; + if (length < offset) { + av_log(s->avctx, AV_LOG_ERROR, "entry_point_offset table is corrupted\n"); + return AVERROR_INVALIDDATA; + } + s->sh.size [s->sh.num_entry_point_offsets] = length - offset; + s->sh.offset[s->sh.num_entry_point_offsets] = offset; + s->sh.offset[0] = s->sh.data_offset; + s->sh.size[0] = s->sh.offset[1] - s->sh.offset[0]; + s->data = data; + + /* propagate QP init to every worker context */ + for (unsigned i = 1; i < s->nb_local_ctx; i++) { + s->local_ctx[i].first_qp_group = 1; + s->local_ctx[i].qp_y = s->local_ctx[0].qp_y; + s->local_ctx[i].tu.cu_qp_offset_cb = 0; + s->local_ctx[i].tu.cu_qp_offset_cr = 0; + } + + /* pre-fill the slice-address table serially so the workers read it race-free + * (whole-frame single slice -> every CTB resolves to the same slice_addr) */ + for (ctb_addr_ts = start_ts; ctb_addr_ts < sps->ctb_size; ctb_addr_ts++) + l->tab_slice_address[pps->ctb_addr_ts_to_rs[ctb_addr_ts]] = s->sh.slice_addr; + + ret = av_calloc(nb_tiles, sizeof(*ret)); + if (!ret) + return AVERROR(ENOMEM); + s->avctx->execute2(s->avctx, hls_decode_entry_tile, s->local_ctx, ret, nb_tiles); + for (int i = 0; i < nb_tiles; i++) + if (ret[i] < 0) + res = ret[i]; + av_free(ret); + if (res < 0) + return res; + + s->local_ctx[0].tile_bs_defer = 0; + if (pps->loop_filter_across_tiles_enabled_flag) { + for (ctb_addr_ts = start_ts; ctb_addr_ts < sps->ctb_size; ctb_addr_ts++) { + int ctb_addr_rs = pps->ctb_addr_ts_to_rs[ctb_addr_ts]; + x_ctb = (ctb_addr_rs % ctb_width) << sps->log2_ctb_size; + y_ctb = (ctb_addr_rs / ctb_width) << sps->log2_ctb_size; + hls_decode_neighbour(&s->local_ctx[0], l, pps, sps, x_ctb, y_ctb, ctb_addr_ts); + if (s->local_ctx[0].boundary_flags & (BOUNDARY_LEFT_TILE | BOUNDARY_UPPER_TILE)) + ff_hevc_tile_boundary_bs(&s->local_ctx[0], l, pps, x_ctb, y_ctb); + } + } + + /* serial in-loop filtering, same lagged order as hls_decode_entry */ + for (ctb_addr_ts = start_ts; ctb_addr_ts < sps->ctb_size; ctb_addr_ts++) { + int ctb_addr_rs = pps->ctb_addr_ts_to_rs[ctb_addr_ts]; + x_ctb = (ctb_addr_rs % ctb_width) << sps->log2_ctb_size; + y_ctb = (ctb_addr_rs / ctb_width) << sps->log2_ctb_size; + hls_decode_neighbour(&s->local_ctx[0], l, pps, sps, x_ctb, y_ctb, ctb_addr_ts); + ff_hevc_hls_filters(&s->local_ctx[0], l, pps, x_ctb, y_ctb, ctb_size); + } + if (x_ctb + ctb_size >= sps->width && y_ctb + ctb_size >= sps->height) + ff_hevc_hls_filter(&s->local_ctx[0], l, pps, x_ctb, y_ctb, ctb_size); + + return sps->ctb_size; +} + static int decode_slice_data(HEVCContext *s, const HEVCLayerContext *l, const H2645NAL *nal, GetBitContext *gb) { @@ -3062,6 +3234,16 @@ static int decode_slice_data(HEVCContext *s, const HEVCLayerContext *l, pps->num_tile_rows == 1 && pps->num_tile_columns == 1) return hls_slice_data_wpp(s, nal); + /* tile-parallel decode: one slice spanning the whole frame, its entry points + * delimiting every tile; anything else (multi-slice, no entry points) falls back */ + if (s->avctx->active_thread_type == FF_THREAD_SLICE && + s->sh.num_entry_point_offsets > 0 && + pps->tiles_enabled_flag && + !pps->entropy_coding_sync_enabled_flag && + s->sh.first_slice_in_pic_flag && + s->sh.num_entry_point_offsets + 1 == pps->num_tile_rows * pps->num_tile_columns) + return hls_slice_data_tiles(s, nal); + return hls_decode_entry(s, gb); } diff --git a/libavcodec/hevc/hevcdec.h b/libavcodec/hevc/hevcdec.h index 8394740c4b..8d630473f2 100644 --- a/libavcodec/hevc/hevcdec.h +++ b/libavcodec/hevc/hevcdec.h @@ -444,6 +444,10 @@ typedef struct HEVCLocalContext { * of the deblocking filter */ int boundary_flags; + /* set while decoding tiles in parallel: defer tile-boundary strengths + * (neighbour tile races), recomputed serially afterwards */ + int tile_bs_defer; + // an array of these structs is used for per-thread state - pad its size // to avoid false sharing char padding[128]; @@ -706,6 +710,8 @@ void ff_hevc_set_qPy(HEVCLocalContext *lc, void ff_hevc_deblocking_boundary_strengths(HEVCLocalContext *lc, const HEVCLayerContext *l, const HEVCPPS *pps, int x0, int y0, int log2_trafo_size); +void ff_hevc_tile_boundary_bs(HEVCLocalContext *lc, const HEVCLayerContext *l, + const HEVCPPS *pps, int x0, int y0); int ff_hevc_cu_qp_delta_sign_flag(HEVCLocalContext *lc); int ff_hevc_cu_qp_delta_abs(HEVCLocalContext *lc); int ff_hevc_cu_chroma_qp_offset_flag(HEVCLocalContext *lc); diff --git a/tests/fate/hevc.mak b/tests/fate/hevc.mak index 697618e2d9..86fcf37668 100644 --- a/tests/fate/hevc.mak +++ b/tests/fate/hevc.mak @@ -290,6 +290,11 @@ FATE_HEVC_FFPROBE-$(call DEMDEC, MOV, HEVC) += fate-hevc-dv-rpu fate-hevc-two-first-slice: CMD = threads=2 framemd5 -i $(TARGET_SAMPLES)/hevc/two_first_slice.mp4 -sws_flags bitexact -t 00:02.00 -an FATE_HEVC-$(call FRAMEMD5, MOV, HEVC) += fate-hevc-two-first-slice +# TILES_A conformance picture through the tile-parallel slice path, same output +fate-hevc-tiles-slice-threads: CMD = thread_type=slice threads=4 framecrc -flags output_corrupt -i $(TARGET_SAMPLES)/hevc-conformance/TILES_A_Cisco_2.bit -pix_fmt yuv420p +fate-hevc-tiles-slice-threads: REF = $(SRC_PATH)/tests/ref/fate/hevc-conformance-TILES_A_Cisco_2 +FATE_HEVC-$(call FRAMECRC, HEVC, HEVC) += fate-hevc-tiles-slice-threads + fate-hevc-cabac-tudepth: CMD = framecrc -i $(TARGET_SAMPLES)/hevc/cbf_cr_cb_TUDepth_4_circle.h265 -pix_fmt yuv444p FATE_HEVC-$(call FRAMECRC, HEVC, HEVC) += fate-hevc-cabac-tudepth -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
