From b06279b7725dbc77994d2e971f4d97683bbc85e9 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:28:34 +0300 Subject: [PATCH 1/2] svac3: pictures decoded side by side With N threads, N pictures are decoded at a time. The caller's thread keeps the picture buffer and the reference lists in decoding order: at a picture's first patch it works out the references and a slot, and once the patches are all in it hands the picture, with a copy of its headers and patch data, to a picture thread. Slots that a picture in flight uses are held until it is output. A P picture waits only for the reference rows its motion reaches. Before each prediction it asks for the rows the 8-tap luma and 4-tap chroma interpolation read, and before co-located motion it asks for that row of 16x16 temporal motion. The patch decoder now reports how far a picture is: the rows no later filter step can change (8 above the last SAO band, 6 above the last deblocked one without SAO) and the temporal-motion rows, stored per LCU row. Each picture thread also runs its picture's reconstruction and filters behind its parser, as one thread with a pool of 3 did, which is what speeds up I pictures. Pictures come out in decoding order, with the same bits for any number of threads, through an output function, up to N pictures late. svac3d_flush() returns the rest at the end of the stream, and svac3d_decode_tag() tags each picture with the buffer its first patch came in. A picture that fails reports its error when it is output, and leaves the buffer; the pictures made from it fail too. One thread decodes as before. The FFmpeg wrapper drains at EOF (AV_CODEC_CAP_DELAY) and takes each frame's pts, dts and duration from its own packet by tag. The library version is 0.3.0. On a six-P-core Core Ultra 5 245K, at 4 threads: 1080p CBR 0.24 s -> 0.13 s, 2560x1440 1.56 s -> 0.79 s, QP 22 (an I picture in six) 0.95 s -> 0.67 s. --- .github/workflows/ci.yml | 14 +- README.md | 27 +- ...-add-SVAC-3.0-decoding-via-libsvac3d.patch | 72 +- source/svac3/svac3_dec.c | 648 +++++++++++++++--- source/svac3/svac3_dec.h | 41 +- source/svac3/svac3_patch.c | 83 ++- source/svac3/svac3_patch.h | 18 + source/svac3/svac3d.c | 84 ++- source/svac3/svac3d.h | 40 +- tests/fuzz/fuzz_patch.c | 8 +- tests/test_api.c | 54 +- tools/svac3dec.c | 63 +- 12 files changed, 996 insertions(+), 156 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea337f7..9c43fb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,14 +42,16 @@ jobs: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_COMPILER=clang \ -DCMAKE_C_FLAGS="-fsanitize=thread -g" cmake --build build -j"$(nproc)" - - name: Decode every stream on 4 threads + - name: Decode every stream on 2 and 4 threads env: TSAN_OPTIONS: halt_on_error=1 run: | for s in tests/streams/*.svac3; do + build/svac3dec -t 2 "$s" /dev/null build/svac3dec -t 4 "$s" /dev/null done build/test_api tests/streams/cv608_synth_1080p_cbr2m.svac3 + build/test_api -lost tests/streams/cv608_patch4_1080p.svac3 fuzz: runs-on: ubuntu-24.04 @@ -64,6 +66,11 @@ jobs: source/svac3/svac3_inter.c source/svac3/svac3_dec.c source/svac3/svac3_dsp.c \ source/svac3/svac3_pool.c -lpthread \ -o fuzz_patch + clang $F -DFUZZ_THREADS=3 tests/fuzz/fuzz_patch.c source/svac3/svac3_hls.c \ + source/svac3/svac3_aec.c source/svac3/svac3_patch.c source/svac3/svac3_recon.c \ + source/svac3/svac3_loopfilter.c source/svac3/svac3_inter.c source/svac3/svac3_dec.c \ + source/svac3/svac3_dsp.c source/svac3/svac3_pool.c -lpthread \ + -o fuzz_frames - name: Fuzz the header parser for 60 s run: | mkdir -p corpus @@ -76,6 +83,11 @@ jobs: head -c 60000 tests/streams/cv608_smartp_1080p.svac3 > corpus_patch/library.svac3 head -c 60000 tests/streams/cv608_patch4_1080p.svac3 > corpus_patch/patches.svac3 ./fuzz_patch -max_total_time=60 -max_len=65536 corpus_patch + - name: Fuzz the picture threads for 60 s + run: | + mkdir -p corpus_frames + cp corpus_patch/*.svac3 corpus_frames/ + ./fuzz_frames -max_total_time=60 -max_len=65536 corpus_frames ffmpeg: runs-on: ubuntu-24.04 diff --git a/README.md b/README.md index ac8b788..4fbc8e5 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,25 @@ decoded pictures. On the 1080p CBR corpus stream, one core decodes about 200 pictures a second on a Cortex-X925 and about 110 on a Core i9-13900; at high bit rates the entropy decoder, which is serial, sets the pace. -`svac3d_set_threads()` (`svac3dec -t N`, FFmpeg's `-threads`) decodes on -several threads: one parses while another reconstructs, and the loop -filters run in bands on all of them. Pictures and their order are the same -whatever the number; on 2 threads a picture takes about 0.65 of the time. +`svac3d_set_threads()` (`svac3dec -t N`, FFmpeg's `-threads`) decodes N +pictures at a time. A P picture waits only for the rows of its reference +that its motion reaches, as the reference's thread finishes them, and +each picture has its reconstruction and loop filters follow its parser on +two threads of its own, so up to three threads per picture are busy. +Pictures come out in the same order and with the same bits whatever the +number, up to N pictures later than with one thread; `svac3d_flush()` +returns the last of them at the end of the stream. + +On a Core Ultra 5 245K (six P-cores): + +| stream | 1 thread | 2 threads | 4 threads | +|---|---|---|---| +| 1080p CBR, 90 pictures | 0.45 s | 0.13 s | 0.13 s | +| 2560x1440, 295 pictures | 3.25 s | 0.89 s | 0.79 s | +| 1080p QP 22, 120 pictures, an I picture in six | 1.58 s | 0.76 s | 0.68 s | + +Threads cost 10-30% more CPU time than one thread does. Streams with many I +pictures gain least: nothing overlaps an I picture's entropy decoding. ## API @@ -65,10 +80,14 @@ whatever the number; on 2 threads a picture takes about 0.65 of the time. svac3d_t *d = svac3d_open(); svac3d_set_threads(d, 4); /* optional */ svac3d_decode(d, buf, len, on_picture, opaque); /* Annex-B, any whole NAL units */ +svac3d_flush(d, on_picture, opaque); /* at the end, with threads */ svac3d_close(d); ``` `on_picture` receives each decoded picture, cropped to the display size. +`svac3d_decode_tag()` gives each picture a value from the buffer its first +patch came in, which is how the FFmpeg wrapper keeps packet timestamps on +pictures that come out later. ## FFmpeg diff --git a/ffmpeg/0001-avcodec-add-SVAC-3.0-decoding-via-libsvac3d.patch b/ffmpeg/0001-avcodec-add-SVAC-3.0-decoding-via-libsvac3d.patch index ed8b5a7..19aa81f 100644 --- a/ffmpeg/0001-avcodec-add-SVAC-3.0-decoding-via-libsvac3d.patch +++ b/ffmpeg/0001-avcodec-add-SVAC-3.0-decoding-via-libsvac3d.patch @@ -1,4 +1,4 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From 27876dcaf30a7bbb10f3e603374c2a42c5270227 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin Date: Wed, 28 Oct 2026 23:38:39 +0300 Subject: [PATCH] avcodec: add SVAC 3.0 decoding via libsvac3d @@ -13,21 +13,21 @@ tool set of the GB/T SVAC 3.0 draft. libavcodec/allcodecs.c | 1 + libavcodec/codec_desc.c | 7 ++ libavcodec/codec_id.h | 1 + - libavcodec/libsvac3d.c | 206 ++++++++++++++++++++++++++++++++++++++ + libavcodec/libsvac3d.c | 244 ++++++++++++++++++++++++++++++++++++++ libavcodec/parsers.c | 1 + - libavcodec/svac3.h | 56 +++++++++++ - libavcodec/svac3_parser.c | 121 ++++++++++++++++++++++ + libavcodec/svac3.h | 56 +++++++++ + libavcodec/svac3_parser.c | 121 +++++++++++++++++++ libavformat/Makefile | 1 + libavformat/allformats.c | 1 + - libavformat/svac3dec.c | 64 ++++++++++++ - 12 files changed, 466 insertions(+) + libavformat/svac3dec.c | 64 ++++++++++ + 12 files changed, 504 insertions(+) create mode 100644 libavcodec/libsvac3d.c create mode 100644 libavcodec/svac3.h create mode 100644 libavcodec/svac3_parser.c create mode 100644 libavformat/svac3dec.c diff --git a/configure b/configure -index 374bd5c..6fd5725 100755 +index 374bd5c..61c7b77 100755 --- a/configure +++ b/configure @@ -290,6 +290,7 @@ External library support: @@ -59,7 +59,7 @@ index 374bd5c..6fd5725 100755 enabled libtwolame && require libtwolame twolame.h twolame_init -ltwolame && { check_lib libtwolame twolame.h twolame_encode_buffer_float32_interleaved -ltwolame || die "ERROR: libtwolame must be installed and version must be >= 0.3.10"; } -+enabled libsvac3d && require_pkg_config libsvac3d "libsvac3d >= 0.2.0" svac3d.h svac3d_set_threads ++enabled libsvac3d && require_pkg_config libsvac3d "libsvac3d >= 0.3.0" svac3d.h svac3d_flush enabled libuavs3d && require_pkg_config libuavs3d "uavs3d >= 1.1.41" uavs3d.h uavs3d_decode enabled libv4l2 && require_pkg_config libv4l2 libv4l2 libv4l2.h v4l2_ioctl enabled libvidstab && require_pkg_config libvidstab "vidstab >= 0.98" vid.stab/libvidstab.h vsMotionDetectInit @@ -127,10 +127,10 @@ index 668a068..a089876 100644 AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs diff --git a/libavcodec/libsvac3d.c b/libavcodec/libsvac3d.c new file mode 100644 -index 0000000..e69d957 +index 0000000..ff4110c --- /dev/null +++ b/libavcodec/libsvac3d.c -@@ -0,0 +1,206 @@ +@@ -0,0 +1,244 @@ +/* + * This file is part of FFmpeg. + * @@ -163,11 +163,20 @@ index 0000000..e69d957 +#include "codec_internal.h" +#include "decode.h" + ++/* the timing of a packet, found again by the tag its pictures carry */ ++typedef struct PacketProps { ++ int64_t tag; ++ int64_t pts, dts, duration; ++} PacketProps; ++ +typedef struct LibSVAC3DContext { + svac3d_t *dec; + AVCodecContext *avctx; + AVPacket *pkt; + AVFifo *frames; ///< decoded pictures not yet returned, AVFrame * ++ AVFifo *props; ///< PacketProps of the packets pictures may still come from ++ int64_t tag; ///< of the last packet decoded ++ int drained; ///< the pictures still in flight at EOF are out + int err; ///< error raised inside the output callback + int pending_err; ///< error to return once the queued pictures are out +} LibSVAC3DContext; @@ -221,6 +230,18 @@ index 0000000..e69d957 + av_image_copy_plane(frame->data[i], frame->linesize[i], + pic->data[i], pic->linesize[i], w, h); + } ++ /* with threads a picture comes out some packets after its own: its ++ * timing is that of the packet its tag names, not the last one */ ++ for (PacketProps p; av_fifo_peek(s->props, &p, 1, 0) >= 0; av_fifo_drain2(s->props, 1)) { ++ if (p.tag < pic->tag) ++ continue; ++ if (p.tag == pic->tag) { ++ frame->pts = p.pts; ++ frame->pkt_dts = p.dts; ++ frame->duration = p.duration; ++ } ++ break; ++ } + frame->pict_type = pic->pict_type == 1 ? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_P; + if (pic->pict_type == 1) + frame->flags |= AV_FRAME_FLAG_KEY; @@ -248,8 +269,9 @@ index 0000000..e69d957 + s->avctx = avctx; + s->pkt = av_packet_alloc(); + s->frames = av_fifo_alloc2(2, sizeof(AVFrame *), AV_FIFO_FLAG_AUTO_GROW); ++ s->props = av_fifo_alloc2(8, sizeof(PacketProps), AV_FIFO_FLAG_AUTO_GROW); + s->dec = svac3d_open(); -+ if (!s->pkt || !s->frames || !s->dec) ++ if (!s->pkt || !s->frames || !s->props || !s->dec) + return AVERROR(ENOMEM); + avctx->pix_fmt = AV_PIX_FMT_YUV420P; + /* libsvac3d runs its own threads: 0 asks for one per CPU */ @@ -273,6 +295,7 @@ index 0000000..e69d957 + + drop_frames(s); + av_fifo_freep2(&s->frames); ++ av_fifo_freep2(&s->props); + av_packet_free(&s->pkt); + svac3d_close(s->dec); + s->dec = NULL; @@ -284,7 +307,9 @@ index 0000000..e69d957 + LibSVAC3DContext *s = avctx->priv_data; + + drop_frames(s); ++ av_fifo_reset2(s->props); + s->pending_err = 0; ++ s->drained = 0; + svac3d_reset(s->dec); +} + @@ -308,11 +333,24 @@ index 0000000..e69d957 + } + + ret = ff_decode_get_packet(avctx, s->pkt); -+ if (ret < 0) -+ return ret; /* EAGAIN, or EOF: no pictures are held back */ -+ s->err = 0; -+ ret = svac3d_decode(s->dec, s->pkt->data, s->pkt->size, svac3d_output, s); -+ av_packet_unref(s->pkt); ++ if (ret == AVERROR_EOF && !s->drained) { ++ /* the pictures other threads are still decoding */ ++ s->drained = 1; ++ s->err = 0; ++ ret = svac3d_flush(s->dec, svac3d_output, s); ++ } else if (ret < 0) { ++ return ret; ++ } else { ++ const PacketProps p = { ++ .tag = ++s->tag, .pts = s->pkt->pts, .dts = s->pkt->dts, ++ .duration = s->pkt->duration, ++ }; ++ s->err = av_fifo_write(s->props, &p, 1); ++ if (s->err >= 0) ++ ret = svac3d_decode_tag(s->dec, s->pkt->data, s->pkt->size, p.tag, ++ svac3d_output, s); ++ av_packet_unref(s->pkt); ++ } + if (s->err < 0) { + s->pending_err = s->err; + } else if (ret < 0) { @@ -333,7 +371,7 @@ index 0000000..e69d957 + .close = libsvac3d_close, + .flush = libsvac3d_flush, + FF_CODEC_RECEIVE_FRAME_CB(libsvac3d_receive_frame), -+ .p.capabilities = AV_CODEC_CAP_DR1 | AV_CODEC_CAP_OTHER_THREADS, ++ .p.capabilities = AV_CODEC_CAP_DR1 | AV_CODEC_CAP_DELAY | AV_CODEC_CAP_OTHER_THREADS, + .caps_internal = FF_CODEC_CAP_INIT_CLEANUP | FF_CODEC_CAP_AUTO_THREADS, + .p.wrapper_name = "libsvac3d", +}; diff --git a/source/svac3/svac3_dec.c b/source/svac3/svac3_dec.c index 596195b..bb62890 100644 --- a/source/svac3/svac3_dec.c +++ b/source/svac3/svac3_dec.c @@ -4,28 +4,83 @@ * SVAC3 picture decoder around the patch decoder: 5.4.3.2 (DOI and POI), * 5.4.3.3 (reference picture lists), the decoded picture buffer, and the * library pictures that display pictures may reference. + * + * With one thread a picture is decoded as its patches arrive. With more, + * pictures are decoded side by side, one per picture thread: the caller's + * thread keeps the buffer and the reference lists, in decoding order, and + * hands each picture, once its patches are in, to a picture thread; a + * picture waits for the rows of its references that its motion reaches, + * as their threads report them final. Each picture thread also has the + * reconstruction and the filters of its picture follow its parser on + * threads of their own: that is what makes an I picture, which nothing + * else can overlap much, quicker. */ #include "svac3_dec.h" +#include #include #include -/* every reference a list may name, plus the picture being decoded */ -#define DPB_SLOTS (2 * SVAC3_MAX_REFS + 1) +/* every reference a list may name, the picture being decoded, and one for + * each picture another thread may still be decoding or about to output */ +#define DPB_SLOTS (2 * SVAC3_MAX_REFS + 2 + SVAC3_MAX_THREADS) /* the library picture kept (the Main profile allows one in the buffer, - * F.2; a new one replaces it, 5.4.3.4), and one being decoded */ -#define LIB_SLOTS 2 + * F.2; a new one replaces it, 5.4.3.4), one being decoded, and the same + * for pictures in flight */ +#define LIB_SLOTS (2 + SVAC3_MAX_THREADS) typedef struct { uint8_t *buf; svac3_frame_t f; svac3_tmv_t *tmv; + int w, h; int64_t doi; /* DOI + 256 * DOICycleCnt */ int64_t poi; int used; uint32_t lib_index; /* library pictures: library_picture_index */ + + /* pictures in flight */ + int holds; /* pictures in flight using it (the caller's thread's) */ + svac3_dec_t *dec; + int rows, tmv_rows; /* final luma rows, temporal motion rows; atomic */ + int failed; /* written before the last rows */ } pic_t; +/* What the reference lists make of a picture at its first patch */ +typedef struct { + pic_t *slot; + int lib; + uint32_t index; /* library pictures: library_picture_index */ + int64_t doi, poi; + int64_t tag; + int keep[DPB_SLOTS]; + svac3_refs_t refs; + pic_t *ref[SVAC3_MAX_REFS]; +} plan_t; + +typedef struct { + svac3_patch_header_t hdr; + uint8_t *data; + size_t len, cap; +} patch_buf_t; + +/* A picture for a picture thread */ +typedef struct { + svac3_hls_t hls; /* as at its first patch */ + patch_buf_t *patch; + int npatch, num_patches, patch_cap; + plan_t plan; + int ret, done; + svac3_patch_stats_t st; +} task_t; + +typedef struct { + pthread_t th; + svac3_dec_t *d; + svac3_picture_cache_t cache; + svac3_pool_t *pool; +} worker_t; + struct svac3_dec { svac3_hls_t hls; svac3_patch_stats_t st; @@ -37,20 +92,31 @@ struct svac3_dec { int64_t cycle; int doi_prev, have_prev; int64_t last_poi; + int64_t tag, last_tag; /* the picture whose patches are being decoded */ svac3_picture_t *open; - pic_t *open_slot; - int open_lib; - int open_keep[DPB_SLOTS]; - svac3_refs_t open_refs; - svac3_pool_t *pool; + plan_t open_plan; svac3_picture_cache_t cache; - int64_t open_doi, open_poi; - uint32_t open_index; + + /* picture threads: tasks in decoding order, ring[head] the oldest; + * the first taken of them are with a thread or done */ + int nworkers; + worker_t *workers; + pthread_mutex_t lock; + pthread_cond_t work, cond; /* a task queued; progress or a task done */ + int stop; + task_t *ring[SVAC3_MAX_THREADS]; + int head, n, taken; + task_t *collect; /* the picture whose patches are arriving */ + task_t *spare[SVAC3_MAX_THREADS + 1]; + int nspare; + svac3_dec_out_fn out_fn; + void *out_ctx; }; static void abandon(svac3_dec_t *d); +static void stop_workers(svac3_dec_t *d); svac3_dec_t *svac3_dec_create(void) { @@ -64,12 +130,14 @@ static void free_pic(pic_t *p) memset(p, 0, sizeof(*p)); } -static void free_pics(svac3_dec_t *d) +static void free_task(task_t *t) { - for (int i = 0; i < DPB_SLOTS; i++) - free_pic(&d->pic[i]); - for (int i = 0; i < LIB_SLOTS; i++) - free_pic(&d->lib[i]); + if (!t) + return; + for (int i = 0; i < t->patch_cap; i++) + free(t->patch[i].data); + free(t->patch); + free(t); } void svac3_dec_destroy(svac3_dec_t *d) @@ -77,34 +145,31 @@ void svac3_dec_destroy(svac3_dec_t *d) if (!d) return; abandon(d); - free_pics(d); + stop_workers(d); + for (int i = 0; i < d->nspare; i++) + free_task(d->spare[i]); + for (int i = 0; i < DPB_SLOTS; i++) + free_pic(&d->pic[i]); + for (int i = 0; i < LIB_SLOTS; i++) + free_pic(&d->lib[i]); svac3_picture_cache_free(&d->cache); - svac3_pool_destroy(d->pool); free(d->rbsp); free(d); } -int svac3_dec_set_threads(svac3_dec_t *d, int threads) +int64_t svac3_dec_poi(const svac3_dec_t *d) { - abandon(d); /* a picture keeps the pool it began with */ - svac3_pool_destroy(d->pool); - d->pool = svac3_pool_create(threads); - return svac3_pool_threads(d->pool); + return d->last_poi; } -void svac3_dec_reset(svac3_dec_t *d) +void svac3_dec_set_tag(svac3_dec_t *d, int64_t tag) { - abandon(d); - for (int i = 0; i < DPB_SLOTS; i++) - d->pic[i].used = 0; - for (int i = 0; i < LIB_SLOTS; i++) - d->lib[i].used = 0; - d->have_prev = 0; + d->tag = tag; } -int64_t svac3_dec_poi(const svac3_dec_t *d) +int64_t svac3_dec_tag(const svac3_dec_t *d) { - return d->last_poi; + return d->last_tag; } const svac3_hls_t *svac3_dec_hls(const svac3_dec_t *d) @@ -117,8 +182,19 @@ const svac3_patch_stats_t *svac3_dec_stats(const svac3_dec_t *d) return &d->st; } +void svac3_dec_set_output(svac3_dec_t *d, svac3_dec_out_fn fn, void *ctx) +{ + d->out_fn = fn; + d->out_ctx = ctx; +} + +/* A slot holding a w x h picture: a picture of another size gives way */ static int alloc_pic(pic_t *p, int w, int h) { + if (p->buf && (p->w != w || p->h != h)) + free_pic(p); + if (p->buf) + return SVAC3_OK; const size_t ysz = (size_t)w * h; p->buf = malloc(ysz * 3 / 2); p->tmv = malloc(svac3_tmv_count(w, h) * sizeof(*p->tmv)); @@ -129,6 +205,8 @@ static int alloc_pic(pic_t *p, int w, int h) p->tmv = NULL; return SVAC3_ERR_NOMEM; } + p->w = w; + p->h = h; p->f.plane[0] = p->buf; p->f.plane[1] = p->buf + ysz; p->f.plane[2] = p->f.plane[1] + ysz / 4; @@ -207,38 +285,45 @@ static void abandon(svac3_dec_t *d) * library_picture_index, for display pictures to reference, and output * since only display library patches (type 17) are accepted. A display * picture naming another index finds no reference. It is decoded into - * the slot not holding the current library picture, which gives way only + * a slot not holding the current library picture, which gives way only * once the new one has decoded. */ -static int start_library(svac3_dec_t *d) +static int plan_library(svac3_dec_t *d, plan_t *pl) { svac3_hls_t *hls = &d->hls; - int ret; if (hls->ph.picture_coding_type != 1) return SVAC3_ERR_UNSUPPORTED; /* inter-coded library pictures */ - pic_t *cur = d->lib[0].used ? &d->lib[1] : &d->lib[0]; - cur->used = 0; - if (!cur->buf && (ret = alloc_pic(cur, d->w, d->h))) - return ret; - if ((ret = svac3_picture_begin_cached(hls, &cur->f, NULL, cur->tmv, &d->cache, &d->open))) + pic_t *cur = NULL; + for (int i = 0; i < LIB_SLOTS && !cur; i++) + if (!d->lib[i].used && !d->lib[i].holds) + cur = &d->lib[i]; + if (!cur) + return SVAC3_ERR_CORRUPT; + const int ret = alloc_pic(cur, d->w, d->h); + if (ret) return ret; - svac3_picture_set_pool(d->open, d->pool); - d->open_slot = cur; - d->open_lib = 1; - d->open_index = hls->ph.library_picture_index; + pl->slot = cur; + pl->lib = 1; + pl->index = hls->ph.library_picture_index; return SVAC3_OK; } -/* Begin a picture at its first patch: DOI and POI, reference lists, a slot */ -static int start_picture(svac3_dec_t *d) +/* A picture at its first patch: DOI and POI, reference lists, a slot */ +static int plan_picture(svac3_dec_t *d, plan_t *pl) { svac3_hls_t *hls = &d->hls; const svac3_sps_t *sps = &hls->sps; int ret; + memset(pl, 0, sizeof(*pl)); const int w = sps->pic_width_in_luma, h = sps->pic_height_in_luma; if (w != d->w || h != d->h) { - free_pics(d); + /* a new size: no picture of the old one is referenced again (the + * slots are reallocated as they are taken) */ + for (int i = 0; i < DPB_SLOTS; i++) + d->pic[i].used = 0; + for (int i = 0; i < LIB_SLOTS; i++) + d->lib[i].used = 0; d->w = w; d->h = h; } @@ -253,10 +338,11 @@ static int start_picture(svac3_dec_t *d) } const int64_t doi = doi7 + 256 * d->cycle; const int64_t poi = doi + hls->ph.picture_output_delay; - d->open_doi = doi; - d->open_poi = poi; + pl->doi = doi; + pl->poi = poi; + pl->tag = d->tag; if (sps->library_stream_flag) - return start_library(d); + return plan_library(d, pl); /* reference lists, and the pictures either of them keeps */ ref_t list[2][SVAC3_MAX_REFS]; @@ -269,7 +355,7 @@ static int start_picture(svac3_dec_t *d) } /* which pictures the lists keep; the buffer is pruned only once the * picture has decoded, so a rejected one costs no reference */ - int *keep = d->open_keep; + int *keep = pl->keep; for (int i = 0; i < DPB_SLOTS; i++) { const pic_t *p = &d->pic[i]; keep[i] = 0; @@ -278,15 +364,15 @@ static int start_picture(svac3_dec_t *d) keep[i] |= p->used && !list[l][j].lib && p->doi == list[l][j].key; } - svac3_refs_t *refs = &d->open_refs; - memset(refs, 0, sizeof(*refs)); + svac3_refs_t *refs = &pl->refs; if (hls->ph.picture_coding_type == 2) { refs->num = nlist[0]; for (int j = 0; j < nlist[0]; j++) { const ref_t *e = &list[0][j]; - const pic_t *r = e->lib ? find_lib(d, e->key) : find_doi(d, e->key); + pic_t *r = e->lib ? find_lib(d, e->key) : find_doi(d, e->key); if (!r) return SVAC3_ERR_CORRUPT; /* reference not decoded */ + pl->ref[j] = r; refs->ref[j] = &r->f; /* a library reference's DistanceIndex is 2 * (POI - 1) (5.4.3.3) */ refs->dist[j] = (int)clamp_dist(e->lib ? 2 : 2 * (poi - r->poi)); @@ -296,44 +382,51 @@ static int start_picture(svac3_dec_t *d) } /* a free slot, or failing that one the lists let go: at most - * 2 * SVAC3_MAX_REFS are kept, so one always exists */ + * 2 * SVAC3_MAX_REFS are kept, so one always exists. A slot a picture + * in flight uses is not free. */ pic_t *cur = NULL; for (int i = 0; i < DPB_SLOTS && !cur; i++) - if (!d->pic[i].used) + if (!d->pic[i].used && !d->pic[i].holds) cur = &d->pic[i]; for (int i = 0; i < DPB_SLOTS && !cur; i++) - if (!keep[i]) + if (!keep[i] && !d->pic[i].holds) cur = &d->pic[i]; if (!cur) return SVAC3_ERR_CORRUPT; cur->used = 0; /* its picture is about to be overwritten */ - if (!cur->buf && (ret = alloc_pic(cur, w, h))) - return ret; - if ((ret = svac3_picture_begin_cached(hls, &cur->f, refs, cur->tmv, &d->cache, &d->open))) + if ((ret = alloc_pic(cur, w, h))) return ret; - svac3_picture_set_pool(d->open, d->pool); - d->open_slot = cur; - d->open_lib = 0; + pl->slot = cur; + pl->lib = 0; return SVAC3_OK; } -/* The picture's last patch has decoded: the buffer takes it */ -static void commit(svac3_dec_t *d, const svac3_frame_t **out) +static int start_picture(svac3_dec_t *d) +{ + plan_t *pl = &d->open_plan; + int ret = plan_picture(d, pl); + if (ret) + return ret; + pic_t *cur = pl->slot; + return svac3_picture_begin_cached(&d->hls, &cur->f, pl->lib ? NULL : &pl->refs, cur->tmv, + &d->cache, &d->open); +} + +/* The buffer takes the picture */ +static void commit(svac3_dec_t *d, const plan_t *pl) { - pic_t *cur = d->open_slot; - if (d->open_lib) { + pic_t *cur = pl->slot; + if (pl->lib) { for (int i = 0; i < LIB_SLOTS; i++) d->lib[i].used = 0; - cur->lib_index = d->open_index; + cur->lib_index = pl->index; } else { for (int i = 0; i < DPB_SLOTS; i++) - d->pic[i].used = d->open_keep[i]; + d->pic[i].used = pl->keep[i]; } cur->used = 1; - cur->doi = d->open_doi; - cur->poi = d->open_poi; - d->last_poi = d->open_poi; - *out = &cur->f; + cur->doi = pl->doi; + cur->poi = pl->poi; } /* One patch NAL unit. A picture that loses patches is dropped, and the @@ -368,11 +461,404 @@ static int decode_patch_nal(svac3_dec_t *d, const uint8_t *rbsp, size_t rl, d->open = NULL; if ((ret = svac3_picture_end_cached(pic, hls, &d->cache))) return ret; - commit(d, out); + commit(d, &d->open_plan); + d->last_poi = d->open_plan.poi; + d->last_tag = d->open_plan.tag; + *out = &d->open_plan.slot->f; + } + return lost ? SVAC3_ERR_CORRUPT : SVAC3_OK; +} + +/* ------------------------------------------------------------------------ */ +/* Pictures on picture threads */ + +static int load(const int *v) +{ + return __atomic_load_n(v, __ATOMIC_ACQUIRE); +} + +/* The rows of slot s that are final, and of its temporal motion; a + * negative number, or one below what is already known, changes nothing */ +static void set_progress(pic_t *s, int rows, int tmv_rows) +{ + svac3_dec_t *d = s->dec; + pthread_mutex_lock(&d->lock); + if (rows > s->rows) + __atomic_store_n(&s->rows, rows, __ATOMIC_RELEASE); + if (tmv_rows > s->tmv_rows) + __atomic_store_n(&s->tmv_rows, tmv_rows, __ATOMIC_RELEASE); + pthread_cond_broadcast(&d->cond); + pthread_mutex_unlock(&d->lock); +} + +/* From the patch decoder's threads. The picture's last report waits for + * its picture thread to know whether it failed: a picture referencing it + * reads that first. */ +static void on_progress(void *ctx, int rows, int tmv_rows) +{ + pic_t *s = ctx; + if (rows >= s->h) + return; + if (rows > load(&s->rows) || tmv_rows > load(&s->tmv_rows)) + set_progress(s, rows, tmv_rows); +} + +static void wait_on(pic_t *s, const int *v, int rows) +{ + if (load(v) >= rows) + return; + svac3_dec_t *d = s->dec; + pthread_mutex_lock(&d->lock); + while (load(v) < rows) + pthread_cond_wait(&d->cond, &d->lock); + pthread_mutex_unlock(&d->lock); +} + +static void wait_rows(void *ctx, int rows) +{ + pic_t *s = ctx; + wait_on(s, &s->rows, rows); +} + +static void wait_tmv(void *ctx, int rows) +{ + pic_t *s = ctx; + wait_on(s, &s->tmv_rows, rows); +} + +static void run_task(task_t *t, svac3_picture_cache_t *cache, svac3_pool_t *pool) +{ + plan_t *pl = &t->plan; + pic_t *s = pl->slot; + const int inter = !pl->lib && t->hls.ph.picture_coding_type == 2; + svac3_picture_t *pic; + memset(&t->st, 0, sizeof(t->st)); + int ret = svac3_picture_begin_cached(&t->hls, &s->f, inter ? &pl->refs : NULL, s->tmv, + cache, &pic); + if (!ret) { + svac3_picture_set_progress(pic, on_progress, s); + if (pool) + svac3_picture_set_pool(pic, pool); + for (int i = 0; i < t->npatch && !ret; i++) { + t->hls.patch = t->patch[i].hdr; + const int r = svac3_picture_patch(pic, &t->hls, t->patch[i].data, t->patch[i].len, + &t->st); + if (r < 0) + ret = r; + } + if (ret) + svac3_picture_release(pic, cache); + else + ret = svac3_picture_end_cached(pic, &t->hls, cache); + } + /* a picture made from a reference that failed failed too */ + for (int j = 0; inter && j < pl->refs.num; j++) { + wait_rows(pl->ref[j], pl->ref[j]->h); + if (!ret && load(&pl->ref[j]->failed)) + ret = SVAC3_ERR_CORRUPT; + } + t->ret = ret; + if (ret) + __atomic_store_n(&s->failed, 1, __ATOMIC_RELEASE); + set_progress(s, s->h, (s->h + 15) >> 4); +} + +static void *worker_main(void *arg) +{ + worker_t *w = arg; + svac3_dec_t *d = w->d; + pthread_mutex_lock(&d->lock); + for (;;) { + while (!d->stop && d->taken == d->n) + pthread_cond_wait(&d->work, &d->lock); + if (d->taken == d->n) + break; /* stopping, nothing queued */ + task_t *t = d->ring[(d->head + d->taken++) % SVAC3_MAX_THREADS]; + pthread_mutex_unlock(&d->lock); + run_task(t, &w->cache, w->pool); + pthread_mutex_lock(&d->lock); + t->done = 1; + pthread_cond_broadcast(&d->cond); + } + pthread_mutex_unlock(&d->lock); + return NULL; +} + +/* The slots a planned picture uses are held while it is in flight */ +static void hold(plan_t *pl, int by) +{ + pl->slot->holds += by; + for (int j = 0; j < SVAC3_MAX_REFS; j++) + if (pl->ref[j]) + pl->ref[j]->holds += by; +} + +static void spare_task(svac3_dec_t *d, task_t *t) +{ + hold(&t->plan, -1); + d->spare[d->nspare++] = t; +} + +static void drop_collect(svac3_dec_t *d) +{ + if (d->collect) + spare_task(d, d->collect); + d->collect = NULL; +} + +/* The oldest picture in flight leaves, once done: to the output function + * when emit is set. A picture that failed leaves the buffer, as one that + * fails on the caller's thread never enters it. */ +static int retire(svac3_dec_t *d, int wait, int emit) +{ + if (!d->n) + return 0; + task_t *t = d->ring[d->head]; + pthread_mutex_lock(&d->lock); + while (wait && !t->done) + pthread_cond_wait(&d->cond, &d->lock); + const int done = t->done; + if (done) { + d->head = (d->head + 1) % SVAC3_MAX_THREADS; + d->n--; + d->taken--; + } + pthread_mutex_unlock(&d->lock); + if (!done) + return 0; + pic_t *s = t->plan.slot; + if (t->ret) + s->used = 0; + d->st = t->st; + d->last_poi = t->plan.poi; + d->last_tag = t->plan.tag; + if (emit && d->out_fn) { + const svac3_dec_pic_t o = { + .frame = t->ret ? NULL : &s->f, + .width = (int)t->hls.sps.horizontal_size, + .height = (int)t->hls.sps.vertical_size, + .pict_type = t->hls.ph.picture_coding_type, + .poi = t->plan.poi, + .tag = t->plan.tag, + .err = t->ret, + }; + d->out_fn(d->out_ctx, &o); + } + spare_task(d, t); + return 1; +} + +/* Every picture in flight, done, out (with emit) */ +static void drain(svac3_dec_t *d, int emit) +{ + drop_collect(d); + while (retire(d, 1, emit)) + ; +} + +/* The picture's patches are in: the buffer takes it, a thread decodes it */ +static void dispatch(svac3_dec_t *d, task_t *t) +{ + plan_t *pl = &t->plan; + commit(d, pl); + pic_t *s = pl->slot; + s->dec = d; + s->rows = s->tmv_rows = s->failed = 0; /* no thread uses the slot */ + pl->refs.wait = wait_rows; + pl->refs.wait_tmv = wait_tmv; + for (int j = 0; j < SVAC3_MAX_REFS; j++) + pl->refs.ctx[j] = pl->ref[j]; + pl->refs.col_ctx = pl->ref[0]; + t->done = 0; + t->ret = 0; + while (retire(d, 0, 1)) /* what is done, out first */ + ; + while (d->n >= d->nworkers) /* as many in flight as threads */ + retire(d, 1, 1); + pthread_mutex_lock(&d->lock); + d->ring[(d->head + d->n++) % SVAC3_MAX_THREADS] = t; + pthread_cond_signal(&d->work); + pthread_mutex_unlock(&d->lock); +} + +static task_t *take_task(svac3_dec_t *d) +{ + if (d->nspare) + return d->spare[--d->nspare]; + return calloc(1, sizeof(task_t)); +} + +/* A patch NAL unit for a picture thread: kept until its picture's patches + * are all in, with the same checks of their order as the caller's thread + * makes; what is in the patches is checked by the picture's thread, and + * its errors come with the picture. */ +static int queue_patch(svac3_dec_t *d, const uint8_t *nal, size_t len) +{ + svac3_hls_t *hls = &d->hls; + int ret = svac3_patch_supported(hls), lost = 0; + if (ret) + return ret; + + if (hls->patch.patch_index == 0) { + if (d->collect) { + drop_collect(d); + lost = 1; + } + const int np = svac3_picture_patches(hls); + if (np < 1) + return SVAC3_ERR_INVALID; + task_t *t = take_task(d); + if (!t) + return SVAC3_ERR_NOMEM; + if ((ret = plan_picture(d, &t->plan))) { + d->spare[d->nspare++] = t; + return ret; + } + hold(&t->plan, 1); + t->hls = *hls; + t->npatch = 0; + t->num_patches = np; + d->collect = t; + } else if (!d->collect) { + return SVAC3_ERR_CORRUPT; /* its picture's first patch was lost */ + } + + task_t *t = d->collect; + if (hls->patch.patch_index != t->npatch) { + drop_collect(d); + return SVAC3_ERR_CORRUPT; /* a patch lost or out of order */ + } + if (t->npatch == t->patch_cap) { + const int cap = t->patch_cap ? 2 * t->patch_cap : 1; + patch_buf_t *p = realloc(t->patch, (size_t)cap * sizeof(*p)); + if (!p) { + drop_collect(d); + return SVAC3_ERR_NOMEM; + } + memset(p + t->patch_cap, 0, (size_t)(cap - t->patch_cap) * sizeof(*p)); + t->patch = p; + t->patch_cap = cap; + } + patch_buf_t *pb = &t->patch[t->npatch]; + if (len - 2 > pb->cap) { + uint8_t *b = realloc(pb->data, len - 2); + if (!b) { + drop_collect(d); + return SVAC3_ERR_NOMEM; + } + pb->data = b; + pb->cap = len - 2; + } + pb->len = svac3_unescape(nal + 2, len - 2, pb->data); + pb->hdr = hls->patch; + if (++t->npatch == t->num_patches) { + d->collect = NULL; + dispatch(d, t); } return lost ? SVAC3_ERR_CORRUPT : SVAC3_OK; } +static void stop_workers(svac3_dec_t *d) +{ + if (!d->nworkers) + return; + drain(d, 0); + pthread_mutex_lock(&d->lock); + d->stop = 1; + pthread_cond_broadcast(&d->work); + pthread_mutex_unlock(&d->lock); + for (int i = 0; i < d->nworkers; i++) { + pthread_join(d->workers[i].th, NULL); + svac3_picture_cache_free(&d->workers[i].cache); + svac3_pool_destroy(d->workers[i].pool); + } + free(d->workers); + d->workers = NULL; + d->nworkers = 0; + d->stop = 0; + pthread_cond_destroy(&d->work); + pthread_cond_destroy(&d->cond); + pthread_mutex_destroy(&d->lock); +} + +/* threads picture threads; fewer when they cannot all be started, and + * none (0) unless at least two are */ +static int start_workers(svac3_dec_t *d, int threads) +{ + if (pthread_mutex_init(&d->lock, NULL)) + return 0; + if (pthread_cond_init(&d->work, NULL)) { + pthread_mutex_destroy(&d->lock); + return 0; + } + if (pthread_cond_init(&d->cond, NULL)) { + pthread_cond_destroy(&d->work); + pthread_mutex_destroy(&d->lock); + return 0; + } + d->workers = calloc((size_t)threads, sizeof(*d->workers)); + int n = 0; + while (d->workers && n < threads) { + d->workers[n].d = d; + /* a picture's own threads: reconstruction and the filters beside + * its parser, as on one thread with a pool of 3; they wait while + * the picture waits for its references */ + d->workers[n].pool = svac3_pool_create(3); + if (pthread_create(&d->workers[n].th, NULL, worker_main, &d->workers[n])) { + svac3_pool_destroy(d->workers[n].pool); + break; + } + n++; + } + d->nworkers = n; + if (n < 2) { + stop_workers(d); + if (!n) { /* stop_workers() left them be */ + free(d->workers); + d->workers = NULL; + pthread_cond_destroy(&d->work); + pthread_cond_destroy(&d->cond); + pthread_mutex_destroy(&d->lock); + } + return 0; + } + return n; +} + +int svac3_dec_set_threads(svac3_dec_t *d, int threads) +{ + abandon(d); + if (d->nworkers) + drain(d, 1); + stop_workers(d); + if (threads > SVAC3_MAX_THREADS) + threads = SVAC3_MAX_THREADS; + if (threads >= 2) { + const int n = start_workers(d, threads); + if (n) + return n; + } + return 1; +} + +void svac3_dec_flush(svac3_dec_t *d) +{ + if (d->nworkers) + drain(d, 1); +} + +void svac3_dec_reset(svac3_dec_t *d) +{ + abandon(d); + if (d->nworkers) + drain(d, 0); + for (int i = 0; i < DPB_SLOTS; i++) + d->pic[i].used = 0; + for (int i = 0; i < LIB_SLOTS; i++) + d->lib[i].used = 0; + d->have_prev = 0; +} + int svac3_dec_nal(svac3_dec_t *d, const uint8_t *nal, size_t len, const svac3_frame_t **out) { *out = NULL; @@ -382,15 +868,23 @@ int svac3_dec_nal(svac3_dec_t *d, const uint8_t *nal, size_t len, const svac3_fr * parameters arrives is lost */ const unsigned t = nal[1] & 31u; int lost = 0; - if (d->open && (t == SVAC3_NAL_SPS || t == SVAC3_NAL_PPS || t == SVAC3_NAL_PIC_HEAD)) { - abandon(d); - lost = 1; + if (t == SVAC3_NAL_SPS || t == SVAC3_NAL_PPS || t == SVAC3_NAL_PIC_HEAD) { + if (d->open) { + abandon(d); + lost = 1; + } + if (d->collect) { + drop_collect(d); + lost = 1; + } } int ret = svac3_parse_nal(&d->hls, nal, len, 0, NULL); if (ret) return ret; if (!svac3_nal_is_patch(t)) return lost ? SVAC3_ERR_CORRUPT : SVAC3_OK; + if (d->nworkers) + return queue_patch(d, nal, len); if (len - 2 > d->rbsp_cap) { uint8_t *b = realloc(d->rbsp, len - 2); if (!b) diff --git a/source/svac3/svac3_dec.h b/source/svac3/svac3_dec.h index 62768b6..8b55a45 100644 --- a/source/svac3/svac3_dec.h +++ b/source/svac3/svac3_dec.h @@ -30,19 +30,52 @@ void svac3_dec_destroy(svac3_dec_t *d); * complete a picture: *out is set alongside the error then. */ int svac3_dec_nal(svac3_dec_t *d, const uint8_t *nal, size_t len, const svac3_frame_t **out); +/* A picture decoded on a picture thread */ +typedef struct { + const svac3_frame_t *frame; /* coded size; NULL when the picture failed */ + int width, height; /* display size */ + int pict_type; /* picture_coding_type */ + int64_t poi; + int64_t tag; /* see svac3_dec_set_tag() */ + int err; /* 0, or why the picture failed */ +} svac3_dec_pic_t; + +typedef void (*svac3_dec_out_fn)(void *ctx, const svac3_dec_pic_t *pic); + /* Decode with this many threads (1, the default, decodes on the caller's). - * Returns the number in use: fewer when threads cannot be started. */ + * Returns the number in use: fewer when threads cannot be started. With + * two or more, pictures are decoded side by side, one per thread, and + * svac3_dec_nal() never sets *out: each picture goes to the output + * function instead, in decoding order, from a later call or from + * svac3_dec_flush(), with any error its decoding met. Pictures still in + * flight when the number changes are output first. */ int svac3_dec_set_threads(svac3_dec_t *d, int threads); -/* Drop every decoded picture (after a seek); parameter sets are kept */ +/* Where pictures decoded on picture threads go; frame is valid while the + * function runs. */ +void svac3_dec_set_output(svac3_dec_t *d, svac3_dec_out_fn fn, void *ctx); + +/* Wait for every picture in flight and output it */ +void svac3_dec_flush(svac3_dec_t *d); + +/* Drop every decoded picture (after a seek), those in flight too; + * parameter sets are kept */ void svac3_dec_reset(svac3_dec_t *d); const svac3_hls_t *svac3_dec_hls(const svac3_dec_t *d); -/* POI of the last picture decoded */ +/* POI of the last picture decoded (output, with picture threads) */ int64_t svac3_dec_poi(const svac3_dec_t *d); -/* Statistics of the last patch decoded */ +/* A value each picture carries from the call that gave its first patch: + * the one set when that call was made */ +void svac3_dec_set_tag(svac3_dec_t *d, int64_t tag); + +/* The tag of the last picture decoded (output, with picture threads) */ +int64_t svac3_dec_tag(const svac3_dec_t *d); + +/* Statistics of the last patch decoded (of the last picture output, with + * picture threads) */ const svac3_patch_stats_t *svac3_dec_stats(const svac3_dec_t *d); #endif /* SVAC3_DEC_H */ diff --git a/source/svac3/svac3_patch.c b/source/svac3/svac3_patch.c index d557432..4cec4a7 100644 --- a/source/svac3/svac3_patch.c +++ b/source/svac3/svac3_patch.c @@ -658,6 +658,8 @@ static void derive_skip_motion(const parser_t *p, int x0, int y0, int w, int h, if (subtype == 0) { if (!p->tmvp || !p->refs->col) return; + if (p->refs->wait_tmv) + p->refs->wait_tmv(p->refs->col_ctx, (y0 >> 4) + 1); const svac3_tmv_t *t = &p->refs->col[(y0 >> 4) * ((p->pic_w + 15) >> 4) + (x0 >> 4)]; if (t->ref < 0) return; @@ -677,10 +679,27 @@ static void derive_skip_motion(const parser_t *p, int x0, int y0, int w, int h, } } +/* A reference decoded at the same time: wait for the rows that the + * interpolation of an h-row block at y0 with vertical motion mvy reads, + * the 8-tap luma one and the 4-tap chroma one */ +static void ref_wait(const parser_t *p, int ref, int y0, int h, int mvy) +{ + if (!p->refs->wait) + return; + const int luma = (((y0 << 2) + mvy) >> 2) + h + 4; + const int chroma = 2 * (((((y0 >> 1) * 2 << 2) + mvy) >> 3) + h / 2 + 2); + int need = luma > chroma ? luma : chroma; + if (need > p->pic_h) + need = p->pic_h; + if (need > 0) + p->refs->wait(p->refs->ctx[ref], need); +} + static void recon_inter(parser_t *p, int x0, int y0, int w, int h, int ref, const int mv[2], int qp, int ctp, int chroma) { const svac3_frame_t *rf = p->refs->ref[ref]; + ref_wait(p, ref, y0, h, mv[1]); svac3_mc_luma(rf, x0, y0, w, h, mv[0], mv[1], p->pred); const int16_t *res = NULL; if (ctp & 1) { @@ -714,9 +733,11 @@ static void recon_chroma_inter(parser_t *p, int x0, int y0, int w, int h, int qp for (int x = 0; x < K; x += 2) { const scu_t *s = scu_at(p, x0 + 2 * x, y0 + 2 * y); uint8_t blk[4] = { 128, 128, 128, 128 }; - if (s && s->ref >= 0) + if (s && s->ref >= 0) { + ref_wait(p, s->ref, y0 + 2 * y, 4, s->mv[1]); svac3_mc_chroma(p->refs->ref[s->ref], c, x0 / 2 + x, y0 / 2 + y, 2, 2, s->mv[0], s->mv[1], blk); + } for (int j = 0; j < 2; j++) for (int i = 0; i < 2; i++) p->pred[(y + j) * K + x + i] = blk[j * 2 + i]; @@ -1182,10 +1203,10 @@ size_t svac3_tmv_count(int pic_w, int pic_h) /* 5.4.6.7.10.3 with EtmvpMvapEnableFlag, SbTmvpEnableFlag, * MvpRefLayerEnableFlag and AwpEnableFlag 0: the motion at the centre of * each 16x16 unit */ -static void store_tmv(const parser_t *p, svac3_tmv_t *t) +static void store_tmv_rows(const parser_t *p, svac3_tmv_t *t, int ty0, int ty1) { - const int tw = (p->pic_w + 15) >> 4, th = (p->pic_h + 15) >> 4; - for (int ty = 0; ty < th; ty++) + const int tw = (p->pic_w + 15) >> 4; + for (int ty = ty0; ty < ty1; ty++) for (int tx = 0; tx < tw; tx++) { const int x0 = tx << 4, y0 = ty << 4; const int nw = x0 + 16 >= p->pic_w ? p->pic_w - x0 : 16; @@ -1203,6 +1224,7 @@ static void store_tmv(const parser_t *p, svac3_tmv_t *t) } } + int svac3_patch_supported(const svac3_hls_t *hls) { const svac3_sps_t *s = &hls->sps; @@ -1320,6 +1342,9 @@ struct svac3_picture { * rows it reads; with patches narrower than the picture, at the end */ int wavefront; int dbk_off, sao_on; + svac3_progress_fn progress; + void *progress_ctx; + int tmv_rows; /* rows of temporal motion stored */ /* with 3 threads or more, the filters have a thread of their own */ int filter_threaded, filter_ending, rows_ready; pthread_t filter; @@ -1342,6 +1367,27 @@ struct svac3_picture { size_t scu_n, sao_n; }; +/* Tell the progress callback which rows are final: those no filter step + * still to come can change. SAO of LCU row k leaves luma rows above y1 - 4 + * final (y1 the next band's first row) and chroma rows above y1 / 2 - 4; + * deblocking alone, y1 - 3 and y1 / 2 - 3; in luma rows that is y1 - 8 + * and y1 - 6 for every plane. */ +static void report(svac3_picture_t *pic) +{ + if (!pic->progress) + return; + const parser_t *r = &pic->r; + const int L = r->lcu_size; + int rows; + if (pic->sao_on) + rows = pic->sao_done >= r->lcu_h ? r->pic_h : pic->sao_done * L - 8; + else if (!pic->dbk_off) + rows = pic->dbk_done >= r->lcu_h ? r->pic_h : pic->dbk_done * L - 6; + else + rows = pic->dbk_done >= r->lcu_h ? r->pic_h : pic->dbk_done * L; + pic->progress(pic->progress_ctx, rows < 0 ? 0 : rows, -1); +} + /* The deblocking parameters of LCU row k's units */ static void fill_units(svac3_picture_t *pic, int k) { @@ -1380,8 +1426,10 @@ static void filter_upto(svac3_picture_t *pic, int rows, int final) } pic->dbk_done = dbk_to; } - if (!pic->sao_on || pic->sao_done >= pic->dbk_done) + if (!pic->sao_on || pic->sao_done >= pic->dbk_done) { + report(pic); return; + } /* the deblocked rows the SAO of LCU rows sao_done .. dbk_done - 1 reads * that are final now: up to 4 above the next band (whose top edge * changes the 3 above it) */ @@ -1401,6 +1449,7 @@ static void filter_upto(svac3_picture_t *pic, int rows, int final) svac3_sao_rows(r->frame, pic->sao_src, (const svac3_sao_t (*)[3])r->sao, L, r->lcu_w, lh, pic->sao_done, pic->dbk_done, r->pool); pic->sao_done = pic->dbk_done; + report(pic); } /* The reconstruction has finished LCU rows 0 .. rows - 1 */ @@ -1408,6 +1457,18 @@ static void filter_rows_done(svac3_picture_t *pic, int rows) { if (!pic || !pic->wavefront) return; + /* the temporal motion of the rows: a later picture may be waiting */ + if (pic->progress && pic->p.tmv_out) { + const parser_t *r = &pic->r; + int ty1 = rows * r->lcu_size / 16; + if (ty1 > (r->pic_h + 15) >> 4) + ty1 = (r->pic_h + 15) >> 4; + if (ty1 > pic->tmv_rows) { + store_tmv_rows(r, pic->p.tmv_out, pic->tmv_rows, ty1); + pic->tmv_rows = ty1; + pic->progress(pic->progress_ctx, -1, ty1); + } + } if (!pic->filter_threaded) { filter_upto(pic, rows, 0); return; @@ -1959,6 +2020,12 @@ static int decode_patch(parser_t *p, const svac3_hls_t *hls, const uint8_t *data return SVAC3_OK; } +void svac3_picture_set_progress(svac3_picture_t *pic, svac3_progress_fn fn, void *ctx) +{ + pic->progress = fn; + pic->progress_ctx = ctx; +} + void svac3_picture_set_pool(svac3_picture_t *pic, svac3_pool_t *pool) { pic->p.pool = pic->r.pool = pool; @@ -2026,8 +2093,10 @@ int svac3_picture_end_cached(svac3_picture_t *pic, const svac3_hls_t *hls, ret = SVAC3_ERR_NOMEM; else if (p->recon) filter_upto(pic, 0, 1); /* what the rows left to filter */ - if (ret == SVAC3_OK && p->tmv_out) - store_tmv(p->recon ? &pic->r : p, p->tmv_out); + if (ret == SVAC3_OK && p->tmv_out) /* the rows not stored yet */ + store_tmv_rows(p->recon ? &pic->r : p, p->tmv_out, pic->tmv_rows, (p->pic_h + 15) >> 4); + if (pic->progress) /* all of it, even in error */ + pic->progress(pic->progress_ctx, p->pic_h, (p->pic_h + 15) >> 4); svac3_picture_release(pic, cache); return ret; } diff --git a/source/svac3/svac3_patch.h b/source/svac3/svac3_patch.h index d36b6cf..c147add 100644 --- a/source/svac3/svac3_patch.h +++ b/source/svac3/svac3_patch.h @@ -55,6 +55,16 @@ typedef struct { int dist[SVAC3_MAX_REFS]; /* BlockDistance: DistanceIndex of the current picture minus the reference's */ const svac3_tmv_t *col; /* temporal motion of ref[0] */ + + /* For pictures decoded at the same time as their references: wait() + * returns once picture ref[i] (ctx[i]) has its first rows luma rows, + * and rows / 2 chroma rows, final; wait_tmv() once the first rows rows + * of 16x16 temporal motion of ref[0] (col_ctx) are stored. NULL when + * the references are complete. */ + void (*wait)(void *ctx, int rows); + void *ctx[SVAC3_MAX_REFS]; + void (*wait_tmv)(void *ctx, int rows); + void *col_ctx; } svac3_refs_t; /* Number of svac3_tmv_t a picture of this size stores. */ @@ -101,6 +111,14 @@ int svac3_picture_end(svac3_picture_t *pic, const svac3_hls_t *hls); /* Run the picture's loop filters on pool's threads (NULL: on the caller) */ void svac3_picture_set_pool(svac3_picture_t *pic, svac3_pool_t *pool); +/* Report how far the picture is: progress(ctx, rows, tmv_rows) after each + * step, with the luma rows (rows / 2 chroma rows) that are final or the + * rows of temporal motion stored, the other -1 (they come from different + * threads); and once more, with all of both, when it ends (also when it + * ends in error). */ +typedef void (*svac3_progress_fn)(void *ctx, int rows, int tmv_rows); +void svac3_picture_set_progress(svac3_picture_t *pic, svac3_progress_fn fn, void *ctx); + /* Free a picture without finishing it. */ void svac3_picture_free(svac3_picture_t *pic); diff --git a/source/svac3/svac3d.c b/source/svac3/svac3d.c index 7437d80..a58e1d0 100644 --- a/source/svac3/svac3d.c +++ b/source/svac3/svac3d.c @@ -13,6 +13,10 @@ struct svac3d { svac3_dec_t *dec; + /* for the call under way */ + svac3d_output_fn out; + void *opaque; + int first; /* its first error */ }; /* ่กจ61 */ @@ -58,39 +62,87 @@ void svac3d_reset(svac3d_t *d) svac3_dec_reset(d->dec); } +static void emit(svac3d_t *d, const svac3_frame_t *f, int width, int height, int type, + int64_t poi, int64_t tag) +{ + if (!d->out) + return; + svac3d_picture_t pic = { + .width = width, .height = height, .pict_type = type, .poi = poi, .tag = tag, + }; + for (int c = 0; c < 3; c++) { + pic.data[c] = f->plane[c]; + pic.linesize[c] = f->stride[c]; + } + d->out(d->opaque, &pic); +} + +/* A picture from a picture thread */ +static void on_picture(void *ctx, const svac3_dec_pic_t *p) +{ + svac3d_t *d = ctx; + if (p->err && !d->first) + d->first = p->err; + if (p->frame) + emit(d, p->frame, p->width, p->height, p->pict_type, p->poi, p->tag); +} + +static void begin_call(svac3d_t *d, svac3d_output_fn out, void *opaque) +{ + d->out = out; + d->opaque = opaque; + d->first = 0; + svac3_dec_set_output(d->dec, on_picture, d); +} + +static int end_call(svac3d_t *d) +{ + svac3_dec_set_output(d->dec, NULL, NULL); + d->out = NULL; + d->opaque = NULL; + return d->first; +} + int svac3d_decode(svac3d_t *d, const uint8_t *buf, size_t len, svac3d_output_fn out, void *opaque) +{ + return svac3d_decode_tag(d, buf, len, 0, out, opaque); +} + +int svac3d_decode_tag(svac3d_t *d, const uint8_t *buf, size_t len, int64_t tag, + svac3d_output_fn out, void *opaque) { if (!d || (!buf && len)) return SVAC3D_ERR_ARG; size_t pos = 0, nal_len; const uint8_t *nal; - int first = 0; + begin_call(d, out, opaque); + svac3_dec_set_tag(d->dec, tag); while (svac3_next_nal(buf, len, &pos, &nal, &nal_len)) { if (!nal_len) continue; /* two start codes in a row */ const svac3_frame_t *f; const int ret = svac3_dec_nal(d->dec, nal, nal_len, &f); - if (ret && !first) - first = ret; + if (ret && !d->first) + d->first = ret; /* a unit in error may still complete a picture: one that reveals * an earlier picture's lost patch */ - if (!f || !out) + if (!f) continue; const svac3_hls_t *hls = svac3_dec_hls(d->dec); - svac3d_picture_t pic = { - .width = hls->sps.horizontal_size, - .height = hls->sps.vertical_size, - .pict_type = hls->ph.picture_coding_type, - .poi = svac3_dec_poi(d->dec), - }; - for (int c = 0; c < 3; c++) { - pic.data[c] = f->plane[c]; - pic.linesize[c] = f->stride[c]; - } - out(opaque, &pic); + emit(d, f, (int)hls->sps.horizontal_size, (int)hls->sps.vertical_size, + hls->ph.picture_coding_type, svac3_dec_poi(d->dec), svac3_dec_tag(d->dec)); } - return first; + return end_call(d); +} + +int svac3d_flush(svac3d_t *d, svac3d_output_fn out, void *opaque) +{ + if (!d) + return SVAC3D_ERR_ARG; + begin_call(d, out, opaque); + svac3_dec_flush(d->dec); + return end_call(d); } int svac3d_get_info(const svac3d_t *d, svac3d_info_t *info) diff --git a/source/svac3/svac3d.h b/source/svac3/svac3d.h index e573e5a..79aab95 100644 --- a/source/svac3/svac3d.h +++ b/source/svac3/svac3d.h @@ -23,7 +23,7 @@ extern "C" { #endif #define SVAC3D_VERSION_MAJOR 0 -#define SVAC3D_VERSION_MINOR 2 +#define SVAC3D_VERSION_MINOR 3 #define SVAC3D_VERSION_PATCH 0 /* Error codes; every call returns 0 or one of these. */ @@ -45,6 +45,7 @@ typedef struct { int width, height; /* luma; chroma is (width + 1) / 2 x (height + 1) / 2 */ int pict_type; /* 1 = I, 2 = P */ int64_t poi; /* picture output order index */ + int64_t tag; /* of the buffer its first patch came in (svac3d_decode_tag()) */ } svac3d_picture_t; /* What the last sequence parameter set says. */ @@ -63,9 +64,15 @@ svac3d_t *svac3d_open(void); void svac3d_close(svac3d_t *d); /* Decode with this many threads; 1, the default, decodes on the caller's - * thread. The output is the same whatever the number. Returns the number - * in use (fewer when threads cannot be started, and at most 64), or - * SVAC3D_ERR_ARG. */ + * thread. With more, that many pictures are decoded side by side, and + * each comes out up to that many pictures later than with one (see + * svac3d_flush()); the pictures are the same whatever the number. Each + * picture being decoded also runs its reconstruction and loop filters + * on two threads beside its parser, so more threads than the number + * exist; those waiting on another picture's rows use no CPU. Pictures + * still being decoded are output to nothing when the number changes, so + * set it before decoding. Returns the number in use (fewer when threads + * cannot be started, and at most 64), or SVAC3D_ERR_ARG. */ int svac3d_set_threads(svac3d_t *d, int threads); /* Forget every decoded picture, as after a seek; parameter sets are kept. @@ -74,14 +81,29 @@ void svac3d_reset(svac3d_t *d); /* Decode an Annex-B buffer (start codes, then NAL units) holding any whole * number of NAL units, typically one access unit. out is called once for - * each picture completed, before the call returns. A NAL unit in error - * does not stop the others: the first error is returned once the whole - * buffer is decoded, and every picture that did complete has been given - * to out. The decoder stays usable; pictures referencing a lost one are - * refused until the next I picture. */ + * each picture completed, in decoding order, before the call returns: + * with one thread, the pictures of this buffer; with more, those that + * have finished decoding, which may be from earlier buffers. A NAL unit + * in error does not stop the others: the first error is returned once the + * whole buffer is decoded, and every picture that did complete has been + * given to out. With threads, an error found while decoding a picture is + * returned by the call that outputs it, instead. The decoder stays + * usable; pictures referencing a lost one are refused until the next I + * picture. */ int svac3d_decode(svac3d_t *d, const uint8_t *buf, size_t len, svac3d_output_fn out, void *opaque); +/* svac3d_decode(), giving each picture whose first patch is in buf this + * tag: with threads, a way to tell which buffer a picture came from (as + * for its timestamp). svac3d_decode() gives 0. */ +int svac3d_decode_tag(svac3d_t *d, const uint8_t *buf, size_t len, int64_t tag, + svac3d_output_fn out, void *opaque); + +/* At the end of the stream: wait for the pictures still being decoded + * and give them to out, returning the first error any of them met. Does + * nothing with one thread. */ +int svac3d_flush(svac3d_t *d, svac3d_output_fn out, void *opaque); + /* 0 and *info filled once a sequence parameter set has been decoded, * SVAC3D_ERR_INVALID before. */ int svac3d_get_info(const svac3d_t *d, svac3d_info_t *info); diff --git a/tests/fuzz/fuzz_patch.c b/tests/fuzz/fuzz_patch.c index ebf060b..5ce20d4 100644 --- a/tests/fuzz/fuzz_patch.c +++ b/tests/fuzz/fuzz_patch.c @@ -7,7 +7,9 @@ * Build: clang -g -O1 -fsanitize=fuzzer,address,undefined -Isource/svac3 \ * tests/fuzz/fuzz_patch.c source/svac3/svac3_hls.c source/svac3/svac3_aec.c \ * source/svac3/svac3_patch.c source/svac3/svac3_recon.c \ - * source/svac3/svac3_loopfilter.c source/svac3/svac3_inter.c source/svac3/svac3_dec.c + * source/svac3/svac3_loopfilter.c source/svac3/svac3_inter.c source/svac3/svac3_dec.c \ + * source/svac3/svac3_dsp.c source/svac3/svac3_pool.c -lpthread + * With -DFUZZ_THREADS=n the pictures are decoded on n picture threads. */ #include #include @@ -24,6 +26,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) if (!dec) return 0; +#ifdef FUZZ_THREADS + svac3_dec_set_threads(dec, FUZZ_THREADS); +#endif memset(&peek, 0, sizeof(peek)); while (svac3_next_nal(data, size, &pos, &nal, &nal_len)) { if (nal_len < 2 || svac3_parse_nal(&peek, nal, nal_len, 0, NULL)) @@ -39,6 +44,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) const svac3_frame_t *fr; svac3_dec_nal(dec, nal, nal_len, &fr); } + svac3_dec_flush(dec); svac3_dec_destroy(dec); return 0; } diff --git a/tests/test_api.c b/tests/test_api.c index bc4794f..5c7688b 100644 --- a/tests/test_api.c +++ b/tests/test_api.c @@ -31,6 +31,7 @@ typedef struct { uint64_t hash; int pics, last_type; int64_t last_poi; + uint64_t tags; /* FNV-1a over the pictures' tags */ } sink_t; static void hash_plane(uint64_t *h, const uint8_t *p, int stride, int w, int rows) @@ -49,6 +50,7 @@ static void out(void *opaque, const svac3d_picture_t *pic) s->pics++; s->last_type = pic->pict_type; s->last_poi = pic->poi; + s->tags = (s->tags ^ (uint64_t)pic->tag) * 0x100000001b3ULL; } /* A stream of several patches per picture, with the first picture's last @@ -106,8 +108,18 @@ static void lost_patch_checks(const uint8_t *buf, size_t len) svac3d_t *d = svac3d_open(); sink_t got = { 0xcbf29ce484222325ULL, 0, 0, 0 }; CHECK(svac3d_decode(d, lost, lost_len, out, &got) == SVAC3D_ERR_CORRUPT); + CHECK(svac3d_flush(d, out, &got) == 0); CHECK(got.pics > 0 && got.pics < pictures && got.pics == want.pics && got.hash == want.hash); svac3d_close(d); + + /* with picture threads, the same pictures and the same loss */ + d = svac3d_open(); + CHECK(svac3d_set_threads(d, 4) >= 1); + sink_t thr = { 0xcbf29ce484222325ULL, 0, 0, 0 }; + CHECK(svac3d_decode(d, lost, lost_len, out, &thr) == SVAC3D_ERR_CORRUPT); + CHECK(svac3d_flush(d, out, &thr) == 0); + CHECK(thr.pics == want.pics && thr.hash == want.hash); + svac3d_close(d); free(lost); } @@ -176,7 +188,7 @@ int main(int argc, char **argv) sink_t b = { 0xcbf29ce484222325ULL, 0, 0, 0 }; size_t pos = 0, start = 0, nal_len; const uint8_t *nal; - int patch = 0; + int patch = 0, au = 0; while (svac3_next_nal(buf, len, &pos, &nal, &nal_len)) { const unsigned t = nal_len >= 2 ? nal[1] & 31u : 0; if (patch && (t == SVAC3_NAL_SPS || t == SVAC3_NAL_PIC_HEAD)) { @@ -184,14 +196,15 @@ int main(int argc, char **argv) size_t cut = (size_t)(nal - buf) - 3; if (cut > start && buf[cut - 1] == 0) cut--; - CHECK(svac3d_decode(d, buf + start, cut - start, out, &b) == 0); + CHECK(svac3d_decode_tag(d, buf + start, cut - start, ++au, out, &b) == 0); start = cut; patch = 0; } patch |= svac3_nal_is_patch(t); } - CHECK(svac3d_decode(d, buf + start, len - start, out, &b) == 0); + CHECK(svac3d_decode_tag(d, buf + start, len - start, ++au, out, &b) == 0); CHECK(b.pics == ref.pics && b.hash == ref.hash); + CHECK(b.pics == au); /* a picture per access unit, tagged */ /* after a reset, P pictures are refused until the next I picture */ svac3d_reset(d); @@ -199,15 +212,48 @@ int main(int argc, char **argv) CHECK(svac3d_decode(d, buf + start, len - start, out, &c) == SVAC3D_ERR_CORRUPT && c.pics == 0); svac3d_close(d); - /* threads: the same pictures, whole stream or access unit by unit */ + /* threads: the same pictures, whole stream or access unit by unit, + * the last of them from svac3d_flush() */ d = svac3d_open(); CHECK(svac3d_set_threads(d, 0) == SVAC3D_ERR_ARG && svac3d_set_threads(NULL, 2) == SVAC3D_ERR_ARG); CHECK(svac3d_set_threads(d, 4) >= 1); sink_t t = { 0xcbf29ce484222325ULL, 0, 0, 0 }; CHECK(svac3d_decode(d, buf, len, out, &t) == 0); + CHECK(svac3d_flush(d, out, &t) == 0); CHECK(t.pics == ref.pics && t.hash == ref.hash); + CHECK(t.last_type == 2 && t.last_poi == a.last_poi); + svac3d_reset(d); + sink_t u = { 0xcbf29ce484222325ULL, 0, 0, 0 }; + int behind = 0; + pos = start = 0; + patch = 0; + while (svac3_next_nal(buf, len, &pos, &nal, &nal_len)) { + const unsigned ty = nal_len >= 2 ? nal[1] & 31u : 0; + if (patch && (ty == SVAC3_NAL_SPS || ty == SVAC3_NAL_PIC_HEAD)) { + size_t cut = (size_t)(nal - buf) - 3; + if (cut > start && buf[cut - 1] == 0) + cut--; + CHECK(svac3d_decode_tag(d, buf + start, cut - start, ++behind, out, &u) == 0); + start = cut; + patch = 0; + } + patch |= svac3_nal_is_patch(ty); + } + CHECK(svac3d_decode_tag(d, buf + start, len - start, ++behind, out, &u) == 0); + CHECK(u.pics <= behind); + CHECK(svac3d_flush(d, out, &u) == 0); + CHECK(u.pics == ref.pics && u.hash == ref.hash); + CHECK(u.tags == b.tags); /* each picture keeps its access unit's tag */ + CHECK(svac3d_flush(d, out, &u) == 0 && u.pics == ref.pics); /* nothing left */ + /* a reset drops the pictures in flight; P pictures are refused after it */ + svac3d_reset(d); + sink_t v = { 0xcbf29ce484222325ULL, 0, 0, 0 }; + CHECK(svac3d_decode(d, buf + start, len - start, out, &v) == SVAC3D_ERR_CORRUPT); + CHECK(svac3d_flush(d, out, &v) == 0 && v.pics == 0); CHECK(svac3d_set_threads(d, 1) == 1); + CHECK(svac3d_flush(d, out, &v) == 0 && v.pics == 0); /* nothing with one */ svac3d_close(d); + CHECK(svac3d_flush(NULL, out, NULL) == SVAC3D_ERR_ARG); CHECK(svac3d_decode(NULL, buf, len, out, NULL) == SVAC3D_ERR_ARG); /* a NAL unit too short for its header is an error, not skipped */ diff --git a/tools/svac3dec.c b/tools/svac3dec.c index b0708ad..a027dd0 100644 --- a/tools/svac3dec.c +++ b/tools/svac3dec.c @@ -12,6 +12,42 @@ #include "read_file.h" #include "svac3_dec.h" +typedef struct { + FILE *o; + const char *name; + int pics, err; +} sink_t; + +/* one picture, cropped to its display size; the SPS parser guarantees + * that lies within the coded size */ +static int write_pic(sink_t *s, const svac3_frame_t *fr, int dw, int dh) +{ + for (int c = 0; c < 3; c++) { + const int cw = c ? (dw + 1) / 2 : dw, ch = c ? (dh + 1) / 2 : dh; + for (int y = 0; y < ch; y++) + if (fwrite(fr->plane[c] + (size_t)y * fr->stride[c], 1, (size_t)cw, s->o) != (size_t)cw) { + perror(s->name); + return -1; + } + } + s->pics++; + return 0; +} + +/* from picture threads: stop at the first picture in error */ +static void on_picture(void *ctx, const svac3_dec_pic_t *p) +{ + sink_t *s = ctx; + if (s->err) + return; + if (p->err) { + fprintf(stderr, "picture %d: error %d\n", s->pics, p->err); + s->err = 1; + } else if (write_pic(s, p->frame, p->width, p->height)) { + s->err = 1; + } +} + int main(int argc, char **argv) { int threads = 1; @@ -35,17 +71,19 @@ int main(int argc, char **argv) return 1; } svac3_dec_t *dec = svac3_dec_create(); - int rc = 1, pics = 0; + sink_t sink = { o, argv[2], 0, 0 }; + int rc = 1; if (!dec) goto out; if (threads > 1) svac3_dec_set_threads(dec, threads); + svac3_dec_set_output(dec, on_picture, &sink); size_t pos = 0, nal_len; const uint8_t *nal; unsigned idx = 0; - while (svac3_next_nal(buf, len, &pos, &nal, &nal_len)) { + while (!sink.err && svac3_next_nal(buf, len, &pos, &nal, &nal_len)) { const unsigned this_idx = idx++; if (nal_len < 2) continue; @@ -53,7 +91,7 @@ int main(int argc, char **argv) int ret = svac3_dec_nal(dec, nal, nal_len, &fr); if (ret == SVAC3_ERR_CORRUPT && svac3_nal_is_patch(nal[1] & 31u)) { fprintf(stderr, "picture %d: patch data out of sync from LCU %u, or reference missing\n", - pics, svac3_dec_stats(dec)->first_bad_lcu); + sink.pics, svac3_dec_stats(dec)->first_bad_lcu); goto out; } if (ret) { @@ -62,21 +100,14 @@ int main(int argc, char **argv) } if (!fr) continue; - /* one patch per picture is all that is supported: write it out. The - * SPS parser guarantees the display size lies within the coded size. */ const svac3_sps_t *sps = &svac3_dec_hls(dec)->sps; - const int dw = sps->horizontal_size, dh = sps->vertical_size; - for (int c = 0; c < 3; c++) { - const int cw = c ? (dw + 1) / 2 : dw, ch = c ? (dh + 1) / 2 : dh; - for (int y = 0; y < ch; y++) - if (fwrite(fr->plane[c] + (size_t)y * fr->stride[c], 1, (size_t)cw, o) != (size_t)cw) { - perror(argv[2]); - goto out; - } - } - pics++; + if (write_pic(&sink, fr, (int)sps->horizontal_size, (int)sps->vertical_size)) + goto out; } - fprintf(stderr, "%d pictures\n", pics); + svac3_dec_flush(dec); + if (sink.err) + goto out; + fprintf(stderr, "%d pictures\n", sink.pics); rc = 0; out: if (fclose(o) != 0) From a91d4f6439bcbe04c44a87d428639e1ca7bcc0a2 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:00:23 +0300 Subject: [PATCH 2/2] svac3: picture threads give one thread's pictures under errors A picture handed to a picture thread is in the buffer for the pictures after it to reference, but prunes it only once it has decoded, in decoding order, and then only of the pictures taken before it. One that fails prunes nothing and leaves the buffer, as on one thread. A picture whose reference has since failed, or been decoded again, has its references found again among the pictures before it and is decoded again on the caller's thread once nothing in flight reads it; one not yet handed out has them found again first. A library picture replacing one with the same index no longer makes the pictures in flight lose it. A damaged I picture in the library stream gave 44 pictures on one thread and 40 on four; it now gives 44 on both. test_api -lost damages a P picture and the second I picture, and compares 1 and 4 threads (also on the library stream, now in ctest and the TSan job). A slot decoded on the caller's thread is marked complete, so turning threads on mid-stream with it as a reference no longer dereferences a NULL decoder; test_api changes the number at an access unit both ways. A reference read above the picture is of its first row: the wait asks for at least one row. Recycled tasks free patch buffers the next picture need not reuse. svac3dec reports a lost patch by NAL unit with threads, and the FFmpeg wrapper keeps at most 256 packets' timing. --- .github/workflows/ci.yml | 1 + CMakeLists.txt | 2 + ...-add-SVAC-3.0-decoding-via-libsvac3d.patch | 21 +- source/svac3/svac3_dec.c | 201 +++++++++++++----- source/svac3/svac3_patch.c | 7 +- tests/test_api.c | 113 ++++++++++ tools/svac3dec.c | 10 +- 7 files changed, 293 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c43fb7..4a813a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,7 @@ jobs: done build/test_api tests/streams/cv608_synth_1080p_cbr2m.svac3 build/test_api -lost tests/streams/cv608_patch4_1080p.svac3 + build/test_api -lost tests/streams/cv608_smartp_1080p.svac3 fuzz: runs-on: ubuntu-24.04 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ba2181..a4c7747 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,8 @@ add_test(NAME test_api COMMAND test_api ${CMAKE_SOURCE_DIR}/tests/streams/cv608_synth_1080p_cbr2m.svac3) add_test(NAME test_api_lost_patch COMMAND test_api -lost ${CMAKE_SOURCE_DIR}/tests/streams/cv608_patch4_1080p.svac3) +add_test(NAME test_api_lost_library + COMMAND test_api -lost ${CMAKE_SOURCE_DIR}/tests/streams/cv608_smartp_1080p.svac3) add_executable(test_dsp tests/test_dsp.c) target_include_directories(test_dsp PRIVATE source/svac3) target_link_libraries(test_dsp svac3d) diff --git a/ffmpeg/0001-avcodec-add-SVAC-3.0-decoding-via-libsvac3d.patch b/ffmpeg/0001-avcodec-add-SVAC-3.0-decoding-via-libsvac3d.patch index 19aa81f..0672c0f 100644 --- a/ffmpeg/0001-avcodec-add-SVAC-3.0-decoding-via-libsvac3d.patch +++ b/ffmpeg/0001-avcodec-add-SVAC-3.0-decoding-via-libsvac3d.patch @@ -1,4 +1,4 @@ -From 27876dcaf30a7bbb10f3e603374c2a42c5270227 Mon Sep 17 00:00:00 2001 +From 97083a1788ba49dcda36f32b05c9bfeb922a5ca5 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin Date: Wed, 28 Oct 2026 23:38:39 +0300 Subject: [PATCH] avcodec: add SVAC 3.0 decoding via libsvac3d @@ -13,14 +13,14 @@ tool set of the GB/T SVAC 3.0 draft. libavcodec/allcodecs.c | 1 + libavcodec/codec_desc.c | 7 ++ libavcodec/codec_id.h | 1 + - libavcodec/libsvac3d.c | 244 ++++++++++++++++++++++++++++++++++++++ + libavcodec/libsvac3d.c | 251 ++++++++++++++++++++++++++++++++++++++ libavcodec/parsers.c | 1 + libavcodec/svac3.h | 56 +++++++++ - libavcodec/svac3_parser.c | 121 +++++++++++++++++++ + libavcodec/svac3_parser.c | 121 ++++++++++++++++++ libavformat/Makefile | 1 + libavformat/allformats.c | 1 + libavformat/svac3dec.c | 64 ++++++++++ - 12 files changed, 504 insertions(+) + 12 files changed, 511 insertions(+) create mode 100644 libavcodec/libsvac3d.c create mode 100644 libavcodec/svac3.h create mode 100644 libavcodec/svac3_parser.c @@ -127,10 +127,10 @@ index 668a068..a089876 100644 AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs diff --git a/libavcodec/libsvac3d.c b/libavcodec/libsvac3d.c new file mode 100644 -index 0000000..ff4110c +index 0000000..94f44cb --- /dev/null +++ b/libavcodec/libsvac3d.c -@@ -0,0 +1,244 @@ +@@ -0,0 +1,251 @@ +/* + * This file is part of FFmpeg. + * @@ -163,7 +163,12 @@ index 0000000..ff4110c +#include "codec_internal.h" +#include "decode.h" + -+/* the timing of a packet, found again by the tag its pictures carry */ ++/* The timing of a packet, found again by the tag its pictures carry. A ++ * picture is out at most a few pictures after its packet, so one this many ++ * packets back (after a run of packets with no picture, or with pictures ++ * in error) is not needed any more. */ ++#define MAX_PROPS 256 ++ +typedef struct PacketProps { + int64_t tag; + int64_t pts, dts, duration; @@ -345,6 +350,8 @@ index 0000000..ff4110c + .tag = ++s->tag, .pts = s->pkt->pts, .dts = s->pkt->dts, + .duration = s->pkt->duration, + }; ++ if (av_fifo_can_read(s->props) >= MAX_PROPS) ++ av_fifo_drain2(s->props, 1); + s->err = av_fifo_write(s->props, &p, 1); + if (s->err >= 0) + ret = svac3d_decode_tag(s->dec, s->pkt->data, s->pkt->size, p.tag, diff --git a/source/svac3/svac3_dec.c b/source/svac3/svac3_dec.c index bb62890..47d31de 100644 --- a/source/svac3/svac3_dec.c +++ b/source/svac3/svac3_dec.c @@ -38,6 +38,8 @@ typedef struct { int64_t poi; int used; uint32_t lib_index; /* library pictures: library_picture_index */ + int64_t seq; /* when the buffer took it, in decoding order */ + int64_t changed; /* when its picture failed or was decoded again */ /* pictures in flight */ int holds; /* pictures in flight using it (the caller's thread's) */ @@ -47,13 +49,21 @@ typedef struct { } pic_t; /* What the reference lists make of a picture at its first patch */ +typedef struct { + int lib; /* a library picture, by index */ + int64_t key; /* its DOI, or its library_picture_index */ +} ref_t; + typedef struct { pic_t *slot; int lib; uint32_t index; /* library pictures: library_picture_index */ int64_t doi, poi; int64_t tag; + int64_t seq; /* the buffer's, once it has the picture */ + int64_t when; /* the clock when its references were found */ int keep[DPB_SLOTS]; + ref_t list0[SVAC3_MAX_REFS]; svac3_refs_t refs; pic_t *ref[SVAC3_MAX_REFS]; } plan_t; @@ -71,6 +81,7 @@ typedef struct { int npatch, num_patches, patch_cap; plan_t plan; int ret, done; + int refused; /* not to be decoded: the error it has instead */ svac3_patch_stats_t st; } task_t; @@ -93,6 +104,8 @@ struct svac3_dec { int doi_prev, have_prev; int64_t last_poi; int64_t tag, last_tag; + int64_t seq; /* pictures the buffer has taken */ + int64_t clock; /* references found, pictures failed or decoded again */ /* the picture whose patches are being decoded */ svac3_picture_t *open; @@ -217,12 +230,17 @@ static int alloc_pic(pic_t *p, int w, int h) return SVAC3_OK; } -static pic_t *find_doi(svac3_dec_t *d, int64_t doi) +/* The newest picture with this DOI the buffer took before seq: with + * pictures in flight, one it is still to let go of may share it */ +static pic_t *find_doi(svac3_dec_t *d, int64_t doi, int64_t before) { - for (int i = 0; i < DPB_SLOTS; i++) - if (d->pic[i].used && d->pic[i].doi == doi) - return &d->pic[i]; - return NULL; + pic_t *f = NULL; + for (int i = 0; i < DPB_SLOTS; i++) { + pic_t *p = &d->pic[i]; + if (p->used && p->doi == doi && p->seq < before && (!f || p->seq > f->seq)) + f = p; + } + return f; } /* The reference picture list l of the current picture, or NULL */ @@ -239,11 +257,6 @@ static const svac3_rpl_t *rpl_of(const svac3_hls_t *hls, int l) return &s->rpls[l][idx]; } -typedef struct { - int lib; /* a library picture, by index */ - int64_t key; /* its DOI, or its library_picture_index */ -} ref_t; - /* 5.4.3.3: RefPicDoiList for list l; returns the number of entries */ static int ref_list(const svac3_rpl_t *r, int64_t doi, ref_t out[SVAC3_MAX_REFS]) { @@ -261,12 +274,15 @@ static int ref_list(const svac3_rpl_t *r, int64_t doi, ref_t out[SVAC3_MAX_REFS] return n; } -static pic_t *find_lib(svac3_dec_t *d, int64_t index) +static pic_t *find_lib(svac3_dec_t *d, int64_t index, int64_t before) { - for (int i = 0; i < LIB_SLOTS; i++) - if (d->lib[i].used && d->lib[i].lib_index == index) - return &d->lib[i]; - return NULL; + pic_t *f = NULL; + for (int i = 0; i < LIB_SLOTS; i++) { + pic_t *p = &d->lib[i]; + if (p->used && p->lib_index == index && p->seq < before && (!f || p->seq > f->seq)) + f = p; + } + return f; } static int64_t clamp_dist(int64_t dist) @@ -275,6 +291,29 @@ static int64_t clamp_dist(int64_t dist) return dist < -32768 ? -32768 : dist > 32767 ? 32767 : dist; } +/* The pictures list 0 names, among those the buffer took before seq */ +static int find_refs(svac3_dec_t *d, plan_t *pl, int64_t before) +{ + svac3_refs_t *refs = &pl->refs; + for (int j = 0; j < refs->num; j++) { + const ref_t *e = &pl->list0[j]; + pic_t *r = e->lib ? find_lib(d, e->key, before) : find_doi(d, e->key, before); + if (!r) + return SVAC3_ERR_CORRUPT; /* reference not decoded */ + pl->ref[j] = r; + refs->ref[j] = &r->f; + refs->ctx[j] = r; + /* a library reference's DistanceIndex is 2 * (POI - 1) (5.4.3.3) */ + refs->dist[j] = (int)clamp_dist(e->lib ? 2 : 2 * (pl->poi - r->poi)); + if (j == 0) { + refs->col = r->tmv; + refs->col_ctx = r; + } + } + pl->when = ++d->clock; + return SVAC3_OK; +} + static void abandon(svac3_dec_t *d) { svac3_picture_release(d->open, &d->cache); @@ -364,21 +403,11 @@ static int plan_picture(svac3_dec_t *d, plan_t *pl) keep[i] |= p->used && !list[l][j].lib && p->doi == list[l][j].key; } - svac3_refs_t *refs = &pl->refs; if (hls->ph.picture_coding_type == 2) { - refs->num = nlist[0]; - for (int j = 0; j < nlist[0]; j++) { - const ref_t *e = &list[0][j]; - pic_t *r = e->lib ? find_lib(d, e->key) : find_doi(d, e->key); - if (!r) - return SVAC3_ERR_CORRUPT; /* reference not decoded */ - pl->ref[j] = r; - refs->ref[j] = &r->f; - /* a library reference's DistanceIndex is 2 * (POI - 1) (5.4.3.3) */ - refs->dist[j] = (int)clamp_dist(e->lib ? 2 : 2 * (poi - r->poi)); - if (j == 0) - refs->col = r->tmv; - } + pl->refs.num = nlist[0]; + memcpy(pl->list0, list[0], sizeof(pl->list0)); + if ((ret = find_refs(d, pl, INT64_MAX))) + return ret; } /* a free slot, or failing that one the lists let go: at most @@ -412,21 +441,34 @@ static int start_picture(svac3_dec_t *d) &d->cache, &d->open); } -/* The buffer takes the picture */ -static void commit(svac3_dec_t *d, const plan_t *pl) +/* The buffer takes the picture, which later pictures may reference */ +static void add_pic(svac3_dec_t *d, plan_t *pl) { pic_t *cur = pl->slot; + if (pl->lib) + cur->lib_index = pl->index; + cur->used = 1; + cur->doi = pl->doi; + cur->poi = pl->poi; + cur->seq = pl->seq = ++d->seq; + cur->changed = 0; +} + +/* Once the picture has decoded, the buffer lets go of what its lists do + * not keep (the library picture it replaces) among the pictures taken + * before it: with pictures in flight, those taken after it are not its + * to drop, and a picture that fails drops nothing, as on one thread. */ +static void prune(svac3_dec_t *d, const plan_t *pl) +{ if (pl->lib) { for (int i = 0; i < LIB_SLOTS; i++) - d->lib[i].used = 0; - cur->lib_index = pl->index; + if (d->lib[i].seq < pl->seq) + d->lib[i].used = 0; } else { for (int i = 0; i < DPB_SLOTS; i++) - d->pic[i].used = pl->keep[i]; + if (d->pic[i].seq < pl->seq && !pl->keep[i]) + d->pic[i].used = 0; } - cur->used = 1; - cur->doi = pl->doi; - cur->poi = pl->poi; } /* One patch NAL unit. A picture that loses patches is dropped, and the @@ -461,7 +503,13 @@ static int decode_patch_nal(svac3_dec_t *d, const uint8_t *rbsp, size_t rl, d->open = NULL; if ((ret = svac3_picture_end_cached(pic, hls, &d->cache))) return ret; - commit(d, &d->open_plan); + pic_t *s = d->open_plan.slot; + add_pic(d, &d->open_plan); + prune(d, &d->open_plan); + /* complete, should threads be turned on with it as a reference */ + s->rows = s->h; + s->tmv_rows = (s->h + 15) >> 4; + s->failed = 0; d->last_poi = d->open_plan.poi; d->last_tag = d->open_plan.tag; *out = &d->open_plan.slot->f; @@ -505,7 +553,7 @@ static void on_progress(void *ctx, int rows, int tmv_rows) static void wait_on(pic_t *s, const int *v, int rows) { - if (load(v) >= rows) + if (load(v) >= rows || !s->dec) return; svac3_dec_t *d = s->dec; pthread_mutex_lock(&d->lock); @@ -533,7 +581,9 @@ static void run_task(task_t *t, svac3_picture_cache_t *cache, svac3_pool_t *pool const int inter = !pl->lib && t->hls.ph.picture_coding_type == 2; svac3_picture_t *pic; memset(&t->st, 0, sizeof(t->st)); - int ret = svac3_picture_begin_cached(&t->hls, &s->f, inter ? &pl->refs : NULL, s->tmv, + int ret = t->refused; + if (!ret) + ret = svac3_picture_begin_cached(&t->hls, &s->f, inter ? &pl->refs : NULL, s->tmv, cache, &pic); if (!ret) { svac3_picture_set_progress(pic, on_progress, s); @@ -552,7 +602,7 @@ static void run_task(task_t *t, svac3_picture_cache_t *cache, svac3_pool_t *pool ret = svac3_picture_end_cached(pic, &t->hls, cache); } /* a picture made from a reference that failed failed too */ - for (int j = 0; inter && j < pl->refs.num; j++) { + for (int j = 0; inter && !t->refused && j < pl->refs.num; j++) { wait_rows(pl->ref[j], pl->ref[j]->h); if (!ret && load(&pl->ref[j]->failed)) ret = SVAC3_ERR_CORRUPT; @@ -584,18 +634,54 @@ static void *worker_main(void *arg) return NULL; } +static void hold_refs(plan_t *pl, int by) +{ + for (int j = 0; j < SVAC3_MAX_REFS; j++) + if (pl->ref[j]) + pl->ref[j]->holds += by; +} + /* The slots a planned picture uses are held while it is in flight */ static void hold(plan_t *pl, int by) { pl->slot->holds += by; + hold_refs(pl, by); +} + +/* A picture was decoded from a reference that has since failed, or been + * decoded again: one thread would have found its references after that */ +static int stale(const plan_t *pl) +{ for (int j = 0; j < SVAC3_MAX_REFS; j++) - if (pl->ref[j]) - pl->ref[j]->holds += by; + if (pl->ref[j] && pl->ref[j]->changed > pl->when) + return 1; + return 0; +} + +/* Its references found again, among the pictures before it: an error if + * one is missing now */ +static int refind(svac3_dec_t *d, plan_t *pl) +{ + hold_refs(pl, -1); + memset(pl->ref, 0, sizeof(pl->ref)); + const int ret = find_refs(d, pl, pl->seq ? pl->seq : INT64_MAX); + hold_refs(pl, 1); + return ret; } static void spare_task(svac3_dec_t *d, task_t *t) { hold(&t->plan, -1); + /* what it keeps for the next picture: buffers of the size this one's + * patches needed, not a larger one's from before */ + for (int i = 0; i < t->patch_cap; i++) { + patch_buf_t *pb = &t->patch[i]; + if (i >= t->npatch || pb->cap > 2 * pb->len + 65536) { + free(pb->data); + pb->data = NULL; + pb->cap = 0; + } + } d->spare[d->nspare++] = t; } @@ -608,7 +694,8 @@ static void drop_collect(svac3_dec_t *d) /* The oldest picture in flight leaves, once done: to the output function * when emit is set. A picture that failed leaves the buffer, as one that - * fails on the caller's thread never enters it. */ + * fails on the caller's thread never enters it; one that decoded prunes + * it. */ static int retire(svac3_dec_t *d, int wait, int emit) { if (!d->n) @@ -627,8 +714,24 @@ static int retire(svac3_dec_t *d, int wait, int emit) if (!done) return 0; pic_t *s = t->plan.slot; - if (t->ret) + if (stale(&t->plan)) { + /* decoded again on this thread once nothing in flight reads it */ + pthread_mutex_lock(&d->lock); + for (int i = 0; i < d->n; i++) + while (!d->ring[(d->head + i) % SVAC3_MAX_THREADS]->done) + pthread_cond_wait(&d->cond, &d->lock); + pthread_mutex_unlock(&d->lock); + t->refused = refind(d, &t->plan); + s->rows = s->tmv_rows = s->failed = 0; + run_task(t, &d->cache, NULL); + s->changed = ++d->clock; + } + if (t->ret) { s->used = 0; + s->changed = ++d->clock; + } else { + prune(d, &t->plan); + } d->st = t->st; d->last_poi = t->plan.poi; d->last_tag = t->plan.tag; @@ -660,15 +763,15 @@ static void drain(svac3_dec_t *d, int emit) static void dispatch(svac3_dec_t *d, task_t *t) { plan_t *pl = &t->plan; - commit(d, pl); + t->refused = 0; + if (stale(pl)) /* a reference failed since */ + t->refused = refind(d, pl); + add_pic(d, pl); pic_t *s = pl->slot; s->dec = d; s->rows = s->tmv_rows = s->failed = 0; /* no thread uses the slot */ pl->refs.wait = wait_rows; pl->refs.wait_tmv = wait_tmv; - for (int j = 0; j < SVAC3_MAX_REFS; j++) - pl->refs.ctx[j] = pl->ref[j]; - pl->refs.col_ctx = pl->ref[0]; t->done = 0; t->ret = 0; while (retire(d, 0, 1)) /* what is done, out first */ diff --git a/source/svac3/svac3_patch.c b/source/svac3/svac3_patch.c index 4cec4a7..835a22b 100644 --- a/source/svac3/svac3_patch.c +++ b/source/svac3/svac3_patch.c @@ -689,10 +689,9 @@ static void ref_wait(const parser_t *p, int ref, int y0, int h, int mvy) const int luma = (((y0 << 2) + mvy) >> 2) + h + 4; const int chroma = 2 * (((((y0 >> 1) * 2 << 2) + mvy) >> 3) + h / 2 + 2); int need = luma > chroma ? luma : chroma; - if (need > p->pic_h) - need = p->pic_h; - if (need > 0) - p->refs->wait(p->refs->ctx[ref], need); + /* reads above the picture are of its first row */ + need = need < 1 ? 1 : need > p->pic_h ? p->pic_h : need; + p->refs->wait(p->refs->ctx[ref], need); } static void recon_inter(parser_t *p, int x0, int y0, int w, int h, int ref, const int mv[2], diff --git a/tests/test_api.c b/tests/test_api.c index 5c7688b..658ab62 100644 --- a/tests/test_api.c +++ b/tests/test_api.c @@ -53,6 +53,91 @@ static void out(void *opaque, const svac3d_picture_t *pic) s->tags = (s->tags ^ (uint64_t)pic->tag) * 0x100000001b3ULL; } +/* Where access units begin: before every SPS or picture header that + * follows a patch. Returns how many of at most max were found. */ +static int au_cuts(const uint8_t *buf, size_t len, size_t *cuts, int max) +{ + size_t pos = 0, nal_len; + const uint8_t *nal; + int n = 0, patch = 0; + while (svac3_next_nal(buf, len, &pos, &nal, &nal_len) && n < max) { + const unsigned t = nal_len >= 2 ? nal[1] & 31u : 0; + if (patch && (t == SVAC3_NAL_SPS || t == SVAC3_NAL_PIC_HEAD)) { + /* this unit's start code begins at most 4 bytes before it */ + size_t cut = (size_t)(nal - buf) - 3; + if (buf[cut - 1] == 0) + cut--; + cuts[n++] = cut; + patch = 0; + } + patch |= svac3_nal_is_patch(t); + } + return n; +} + +/* The pictures and the first error of decoding buf whole on threads */ +static int decode_all(const uint8_t *buf, size_t len, int threads, sink_t *s) +{ + svac3d_t *d = svac3d_open(); + if (!d) + return SVAC3D_ERR_NOMEM; + svac3d_set_threads(d, threads); + int err = svac3d_decode(d, buf, len, out, s); + const int e2 = svac3d_flush(d, out, s); + if (!err) + err = e2; + svac3d_close(d); + return err; +} + +/* A patch whose data is damaged, found only once it is decoded (on a + * picture thread, with threads): the pictures and the loss are the same + * on one thread and on four. Damaged are a P picture's first patch, and + * the second I picture's, which on one thread leaves the pictures before + * it in the buffer (with a library stream, the library picture it would + * have replaced). */ +static void corrupt_patch_checks(const uint8_t *buf, size_t len) +{ + for (int target = 0; target < 2; target++) { + size_t pos = 0, nal_len; + const uint8_t *nal; + int pictures = 0, intra = 0; + size_t at = 0, n = 0; + static svac3_hls_t h; + memset(&h, 0, sizeof(h)); + while (svac3_next_nal(buf, len, &pos, &nal, &nal_len)) { + if (nal_len < 2 || svac3_parse_nal(&h, nal, nal_len, 0, NULL)) + continue; + if (!svac3_nal_is_patch(nal[1] & 31u) || h.patch.patch_index != 0) + continue; + pictures++; + intra += h.ph.picture_coding_type == 1; + if (!n && (target ? intra == 2 && h.ph.picture_coding_type == 1 : pictures == 3)) { + at = (size_t)(nal - buf) + nal_len / 2; /* past its header */ + n = nal_len / 4; + } + } + if (target && intra < 2) + continue; /* no second I picture */ + CHECK(pictures > 4 && n > 0); + uint8_t *bad = malloc(len); + if (!bad) + return; + memcpy(bad, buf, len); + for (size_t i = 0; i < n; i++) + bad[at + i] = (uint8_t)(0x5a ^ i); /* never 0: no start code appears */ + sink_t one = { 0xcbf29ce484222325ULL, 0, 0, 0, 0 }, four = one; + const int e1 = decode_all(bad, len, 1, &one), e4 = decode_all(bad, len, 4, &four); + CHECK(e1 != 0 && e4 != 0); + CHECK(one.pics >= 2 && one.pics < pictures); + CHECK(four.pics == one.pics && four.hash == one.hash); + if (four.pics != one.pics) + fprintf(stderr, "damaged %s: %d pictures on 1 thread, %d on 4\n", + target ? "second I picture" : "picture 2", one.pics, four.pics); + free(bad); + } +} + /* A stream of several patches per picture, with the first picture's last * patch removed, in one buffer: the call reports the loss and still gives * every picture the units complete, as the internal decoder fed the same @@ -131,6 +216,7 @@ int main(int argc, char **argv) if (!buf) return 1; lost_patch_checks(buf, len); + corrupt_patch_checks(buf, len); free(buf); if (failures) fprintf(stderr, "%d check(s) failed\n", failures); @@ -255,6 +341,33 @@ int main(int argc, char **argv) svac3d_close(d); CHECK(svac3d_flush(NULL, out, NULL) == SVAC3D_ERR_ARG); + /* the number changed mid-stream, at an access unit: references + * decoded on one thread serve pictures threads decode, and back */ + size_t cuts[512]; + const int ncut = au_cuts(buf, len, cuts, 512); + CHECK(ncut > 4); + if (ncut > 4) { + const size_t mid = cuts[ncut / 2]; + d = svac3d_open(); + sink_t w = { 0xcbf29ce484222325ULL, 0, 0, 0, 0 }; + CHECK(svac3d_decode(d, buf, mid, out, &w) == 0); + CHECK(svac3d_set_threads(d, 4) >= 1); + CHECK(svac3d_decode(d, buf + mid, len - mid, out, &w) == 0); + CHECK(svac3d_flush(d, out, &w) == 0); + CHECK(w.pics == ref.pics && w.hash == ref.hash); + svac3d_close(d); + + d = svac3d_open(); + sink_t x = { 0xcbf29ce484222325ULL, 0, 0, 0, 0 }; + CHECK(svac3d_set_threads(d, 4) >= 1); + CHECK(svac3d_decode(d, buf, mid, out, &x) == 0); + CHECK(svac3d_flush(d, out, &x) == 0); /* before the number changes */ + CHECK(svac3d_set_threads(d, 1) == 1); + CHECK(svac3d_decode(d, buf + mid, len - mid, out, &x) == 0); + CHECK(x.pics == ref.pics && x.hash == ref.hash); + svac3d_close(d); + } + CHECK(svac3d_decode(NULL, buf, len, out, NULL) == SVAC3D_ERR_ARG); /* a NAL unit too short for its header is an error, not skipped */ d = svac3d_open(); diff --git a/tools/svac3dec.c b/tools/svac3dec.c index a027dd0..6ab7091 100644 --- a/tools/svac3dec.c +++ b/tools/svac3dec.c @@ -90,8 +90,14 @@ int main(int argc, char **argv) const svac3_frame_t *fr; int ret = svac3_dec_nal(dec, nal, nal_len, &fr); if (ret == SVAC3_ERR_CORRUPT && svac3_nal_is_patch(nal[1] & 31u)) { - fprintf(stderr, "picture %d: patch data out of sync from LCU %u, or reference missing\n", - sink.pics, svac3_dec_stats(dec)->first_bad_lcu); + /* with threads, patch data is decoded later: the error here is + * a patch lost or out of order, or a missing reference */ + if (threads > 1) + fprintf(stderr, "NAL unit %u: a patch lost or out of order, or reference missing\n", + this_idx); + else + fprintf(stderr, "picture %d: patch data out of sync from LCU %u, or reference missing\n", + sink.pics, svac3_dec_stats(dec)->first_bad_lcu); goto out; } if (ret) {