diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea337f7..4a813a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,14 +42,17 @@ 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 + build/test_api -lost tests/streams/cv608_smartp_1080p.svac3 fuzz: runs-on: ubuntu-24.04 @@ -64,6 +67,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 +84,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/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/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..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 0000000000000000000000000000000000000000 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,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 | 251 ++++++++++++++++++++++++++++++++++++++ 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, 511 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..94f44cb --- /dev/null +++ b/libavcodec/libsvac3d.c -@@ -0,0 +1,206 @@ +@@ -0,0 +1,251 @@ +/* + * This file is part of FFmpeg. + * @@ -163,11 +163,25 @@ index 0000000..e69d957 +#include "codec_internal.h" +#include "decode.h" + ++/* 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; ++} 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 +235,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 +274,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 +300,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 +312,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 +338,26 @@ 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, ++ }; ++ 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, ++ svac3d_output, s); ++ av_packet_unref(s->pkt); ++ } + if (s->err < 0) { + s->pending_err = s->err; + } else if (ret < 0) { @@ -333,7 +378,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..47d31de 100644 --- a/source/svac3/svac3_dec.c +++ b/source/svac3/svac3_dec.c @@ -4,28 +4,94 @@ * 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 */ + 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) */ + 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 { + 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; + +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; + int refused; /* not to be decoded: the error it has instead */ + 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 +103,33 @@ struct svac3_dec { int64_t cycle; 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; - 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 +143,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 +158,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 +195,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 +218,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; @@ -139,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 */ @@ -161,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]) { @@ -183,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) @@ -197,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); @@ -207,38 +324,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 +377,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 +394,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,62 +403,72 @@ 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)); 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); - if (!r) - return SVAC3_ERR_CORRUPT; /* reference not decoded */ - 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 - * 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))) + if ((ret = alloc_pic(cur, w, h))) return ret; - if ((ret = svac3_picture_begin_cached(hls, &cur->f, refs, cur->tmv, &d->cache, &d->open))) - 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, 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) { - pic_t *cur = d->open_slot; - if (d->open_lib) { + if (pl->lib) { for (int i = 0; i < LIB_SLOTS; i++) - d->lib[i].used = 0; - cur->lib_index = d->open_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 = d->open_keep[i]; + if (d->pic[i].seq < pl->seq && !pl->keep[i]) + d->pic[i].used = 0; } - cur->used = 1; - cur->doi = d->open_doi; - cur->poi = d->open_poi; - d->last_poi = d->open_poi; - *out = &cur->f; } /* One patch NAL unit. A picture that loses patches is dropped, and the @@ -368,11 +503,465 @@ 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); + 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; } 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 || !s->dec) + 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 = 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); + 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 && !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; + } + 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; +} + +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]->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; +} + +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; one that decoded prunes + * 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 (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; + 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; + 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; + 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 +971,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..835a22b 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,26 @@ 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; + /* 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], 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 +732,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 +1202,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 +1223,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 +1341,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 +1366,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 +1425,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 +1448,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 +1456,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 +2019,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 +2092,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..658ab62 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,92 @@ 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; +} + +/* 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 @@ -106,8 +193,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); } @@ -119,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); @@ -176,7 +274,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 +282,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 +298,75 @@ 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); + + /* 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 */ diff --git a/tools/svac3dec.c b/tools/svac3dec.c index b0708ad..6ab7091 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,25 +71,33 @@ 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; 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", - 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) { @@ -62,21 +106,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)