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]

Reply via email to