Skip to content

H.264 decode speed on x86-64 #22

Open

Description

@gunwooterry

Goal

CoreAV's H.264 decoder is no slower than FFmpeg on x86-64: the median per-stream time ratio (CoreAV over FFmpeg) is at most 1.00 overall and in every resolution class (sd, 720p, 1080p, 4k), at one thread and at eight threads, on the held-out streams.

Current state

  • Opt-in HEVC interpolation reuses the existing residual workspace for temporary rows (perf(hevc): reuse interpolation row scratch #539, merged as 3951933e). Complete automatic and forced-scalar conformance passes; filtered samples, validation, allocation, scratch accounting and defaults are unchanged.

  • Opt-in HEVC intra-reference gathering reuses availability within each four-by-four luma cell during one component call (perf(hevc): reuse intra neighbour availability #538, merged as ba38f93e). Complete automatic and forced-scalar conformance passes; decoded output, per-sample bounds, slice and tile admission, constrained intra rules and statistics are unchanged.

  • Opt-in HEVC prediction skips the unused comparison predictor unless the current component has nondefault weights (perf(hevc): skip unused comparison predictions #536, merged as 68cbc803). Complete automatic and forced-scalar conformance passes; decoded output, destination validation, weighting, statistics and allocation are unchanged.

  • Opt-in HEVC Main10 interpolation now borrows checked interior reference rows too (perf(hevc): borrow Main10 reference rows #532, merged as ef6821e2). Complete automatic and forced-scalar conformance passes; filter arithmetic, shifts, edge replication, validation, memory accounting and defaults are unchanged.

  • Opt-in HEVC eight-bit interpolation borrows checked interior reference rows instead of gathering copies (perf(hevc): borrow interior reference rows #528, merged as de671521). Complete automatic and forced-scalar conformance passes. Filter arithmetic, edge replication, scratch accounting, ten-bit behavior and defaults are unchanged.

  • Opt-in HEVC eight-bit residual reconstruction borrows complete active rows and preserves full-domain clipping, validation errors and output padding (perf(hevc): streamline eight-bit reconstruction #502, merged as 7a55d8ab). Complete automatic and forced-scalar conformance passes; ten-bit reconstruction, SIMD and scratch accounting are unchanged.

  • Opt-in HEVC uni, bi and weighted prediction combine validated rows with signed i32 arithmetic (perf(hevc): combine predictor rows #520, merged as 7616faff). Full automatic and forced-scalar conformance passes; decoded samples, errors, defaults and isolation stay unchanged. No new SIMD is added.

  • Opt-in native HEVC now decodes eligible wavefront rows concurrently through the existing thread setting (perf(hevc): decode wavefront rows in parallel #495, merged as 90900454). Complete conformance preserves identical normalized records for 144 streams and 8,195 pictures at 1, 2, 4 and 8 threads. Whole-picture filtering, defaults and syntax admission are unchanged. Frame scheduling and tiles remain follow-up work.

  • Opt-in HEVC coefficient scans use compile-time forward and inverse lookup tables with checked access (perf(hevc): precompute coefficient scans #499, merged as 81271b80). Decoded output, syntax admission and CABAC arithmetic are unchanged.

  • Opt-in HEVC coefficient-level decoding skips zero positions while preserving bin order, output, errors and budgets (perf(hevc): skip zero coefficient positions #515, merged as ae8e267b).

  • Packed native H.264 output storage is reused in the CLI and transcode (perf(decode): reuse frame buffers #458, merged as 79fc6131). Raw writers consume CPU pictures and stream their planes without clone or staging buffers. Raw output hashes and AV1 packet identities are unchanged. Decoder kernels, release allocators, isolation, build defaults and the official decoder snapshot are unchanged.

  • The Linux release binaries (x86-64 and arm64 musl, default and full builds) allocate through mimalloc 3.3.2 instead of musl's allocator (perf(cli): use mimalloc in Linux musl binaries #466, merged as 1ed05684), which removes most of musl's mmap/munmap and page-fault cost in decoding. Output and other platforms are unchanged, and the binaries carry mimalloc's licence (release.md, "Allocator"). The official decoder snapshot does not include it.

The official snapshot of 2026-10-29 against FFmpeg 9.0.2 (snapshot-361cd29f), held-out streams, decoding with output discarded:

  • Eight threads: 0.828 overall (95% interval 0.764 to 0.857), CoreAV faster on 62 of 64 streams; every class is below 1.00 (sd 0.874, 720p 0.861, 1080p 0.819, 4k 0.656).
  • One thread: 0.958 overall (0.923 to 0.985), faster on 45 of 64; 720p is the one class above the bar at 1.001 (0.980 to 1.019), and 1080p is 0.974 with an interval reaching 1.033.

Bars and method: docs/design/h264.md ("Bars", "Measurement method").

  • Round tooling (fix(h264): refuse parity runs with failed streams #34): scripts/h264_parity.py exits with status 3 when a stream it was asked to time fails, and scripts/h264_parity_bars.py is the one analyzer for a round's bars, refusing reports that miss a stream or hold a time or ratio that is not a finite positive number, and bars files with a subset named per_stream (fix(h264): refuse a bars subset named per_stream #45).

  • Bundle 3b (perf(h264): bundle 3b cuts one-thread time 1.1% #58), merged after the snapshot: one thread k/base 0.9886 over the 32 training streams and 0.9934 (0.9899 to 0.9958) over the 64 held-out streams, faster on 49 of 64, on the 2.90 GHz class its bars named; eight threads 0.9984 discard and 0.9994 write; output unchanged on 96 of 96 streams (bundle3b/gating). The snapshot above does not include it yet.

  • One-thread host (docs(h264): fixed-frequency one-thread host #55): one-thread arms of rounds whose bars are committed from now on run on the fixed-frequency Compute Engine class (c4-standard-8, one thread a core, turboMode=ALL_CORE_MAX, Xeon Platinum 8581C, --one-thread-class --turbo all-core-max). Its A/A and null-arm calibration met every pre-registered bar (gcp-1t-fixed). One-thread bars measured on earlier classes do not transfer to it.

  • Several sample descriptions (fix(decode): follow MP4 sample description changes #132): an MP4 or MOV H.264 track whose samples switch between avc1 entries at IDR pictures now decodes with each entry's parameter sets in coreav thumbnail, coreav transcode, coreav ffmpeg encodes and tools/decode-oracle; other changes are refused by name (decode.md, "Several sample descriptions").

  • Bundle 6 (perf(h264): bundle 6, about 1% faster decoding #106), merged as 2235eadd: band-view lookup and the CAVLC P path, without the edge window. Against its base, eight threads read 0.9888 [0.9814, 0.9913] over 32 training streams and 0.9869 [0.9760, 0.9906] over 64 held-out streams; one thread read 0.9975 over the training streams (CABAC 0.9979, CAVLC 0.9546). Held-out eight-thread time against FFmpeg 9.0.2 is 0.811, faster on 63 of 64 streams. All pre-registered bars are met and output matches on 256 of 256 calls (bundle6/gating). These are round results, not a new official snapshot.

  • One band for small pictures at eight threads (docs(h264): record one-band rejection #192), rejected; evidence merged as 7cd77c9d. The valid 32-stream c4 run measured SD discard time at 2.4996 times the baseline (95% interval 1.9522 to 2.7176), missing the registered 0.985 bar, and 2.1582 times FFmpeg 9.0.2, missing parity. Output identity passed 256 of 256 calls. The decoder change was removed; validation and one-thread timing did not run under the committed stop rule. The existing band policy and official snapshot are unchanged (round evidence).

  • Opt-in PGO held-out evaluation (docs(pgo): report the held-out evaluation #384), results merged as e2de2ba5: the registered 57-file cohort has class counts 16/16/16/9. Overall one-thread PGO/base is 0.971482 (95% interval 0.965980 to 0.980164), a 2.85% median improvement that misses the fixed 3% bar; all four eight-thread class interval guards pass. The separate CLI diagnostics do not establish their 1% guards, and one-thread FLAC decode is slower in this run. No build or release default changes, no rerun, and no new official snapshot (results and raw calls).

  • Conformance acquisition (fix(ci): retain verified conformance downloads #422), merged as 965b737e: CI retains manifest-bound complete and partial archive caches before decoding, verifies SHA-256 on every reuse, and logs bounded network retries. A second digest mismatch fails acquisition. Manifest digests, URL candidates, selected streams and decode bars are unchanged; full fast and safe conformance passed before merge.

Checklist

  • Reuse existing residual workspace for interpolation row scratch without changing filtered samples, allocation or scratch accounting (perf(hevc): reuse interpolation row scratch #539).

  • Preserve borrowed interpolation row workspace and update private-helper tests when integrating scoped HEVC frame references (perf(hevc): decode ready pictures concurrently #516, perf(hevc): reuse interpolation row scratch #539).

  • Preserve scoped neighbour admission and pixel access when integrating HEVC row storage with intra-reference availability reuse (perf(hevc): decode ready pictures concurrently #516, perf(hevc): reuse intra neighbour availability #538).

  • Extend checked interior reference-row borrowing to ten-bit interpolation while retaining its shifts, error behavior and memory accounting. (perf(hevc): borrow Main10 reference rows #532).

  • Add an interior fractional-luma origin to the automatic/scalar interpolation width-and-phase regression so it directly exercises borrowed Main10 reference rows.

  • Measure SIMD predictor-range validation and retain all reference outcomes, including the Main10 timing regression.

  • Complete the Main10 predictor-validation follow-up with block-level SIMD dispatch and full automatic and forced-scalar correctness checks.

  • Expand combination differential fuzzing to choose the faulty predictor independently of uni/bi selection and offsets.

  • Profile ten-bit inverse reconstruction separately before proposing a follow-up change.

  • Remove eight-bit interpolation reference-row copies after measuring their narrow instruction cost, keeping filter arithmetic and shared reference-support calculation intact (perf(hevc): borrow interior reference rows #528).

  • Profile HEVC neighbour lookup and reuse repeated intra-reference availability checks without changing output (perf(hevc): reuse intra neighbour availability #538).

  • Rename the HEVC kernel differential plan file to match its expanded target set without changing campaign membership.

  • Explore incremental HEVC row-batch entropy reservations with an exact serial continuation point, without weakening the bin limit.

  • Add HEVC tile concurrency after frame scheduling, reusing scheduler machinery only when ownership and filtering stay clear.

  • Verify skipped-picture scratch reservations at independent cold admission boundaries, with and without retained worker storage.

  • Remove the per-picture full-frame copy when resetting private HEVC wavefront workers while preserving reference and prediction availability.

  • Bound the combined HEVC worker count by available CPUs, including explicit requests above host capacity.

  • Add bounded HEVC frame-level scheduling without changing decoded output, presentation order, terminal error order or cold memory admission.

  • Measure safe HEVC sparse coefficient-level loops against the current decoder while preserving exact bin order and complete output (perf(hevc): skip zero coefficient positions #515).

  • Measure the removable HEVC CABAC decision and bypass ceiling, then compare a bounded safe candidate and any assembly candidate with arbitrary-bitstream differential checks. Decision ablation and differential checks are complete; the assembly candidate was closed without merging (perf(hevc): shortcut no-refill CABAC decisions #521). Further CABAC implementation is stopped because no structural candidate has a demonstrated removable ceiling sufficient to proceed; unproved profiling shares are not treated as measured savings.

  • Extend coefficient-level differential checks across successive groups with carried contexts and level state.

  • Bring the 720p class at one thread to at most 1.00 on the held-out streams.

  • Narrow the 1080p one-thread interval so that it lies below 1.00.

  • Take a new official snapshot on the same two hosts after each accepted round, and link it here.

  • Commit the one-thread bars of the next H.264 round for the fixed-frequency class.

  • Calibrate the eight-thread class (c4-standard-16) with turboMode=ALL_CORE_MAX, and adopt it only if it meets pre-registered bars.

  • Report the fixed-frequency one-thread class in the next official snapshot.

  • Give rounds one shared identity job that fails closed: it checks each decoder's exit status and stops at the first failed decode, instead of hashing the end of a pipe (bundle 3b's identity-job.sh does not check it).

  • Make coreav ffprobe -show_frames decode a track with several sample descriptions with each description's parameter sets, as the thumbnail video reader does.

  • Let remux, clip and split write every sample entry and keep stsc's description indices, then lift their refusal of tracks with several sample descriptions.

  • Make V1's FFmpeg reference command keep the colour description when a stream's colour tags change mid-stream (it converts BT.601 frames to BT.709 there even with -noautoscale), or record that as a known divergence (release crew).

  • Measure whether keeping one band at eight threads on small frames improves decoding, with bars committed before the run. Rejected in docs(h264): record one-band rejection #192; the existing band policy is retained.

  • Record every stream's coded dimensions and band-rule eligibility in the committed registration before timing.

  • Preserve partial VM-job output before a time-limit deletion, and retain completed output independently of the controller, so a controller restart cannot lose the reports.

  • Give the H.264 decode example a strict mode that exits nonzero on decode_nal or flush errors, and use it in the shared round identity job.

  • Measure the musl release binary's decode time with and without mimalloc on GitHub Actions as a reference figure, if a public number is wanted.

Not in scope

Activity

  1. added
    area:h264H.264 decoding, its kernels and its speed
    perfSpeed or memory use
    trackingTracks a goal across several pull requests
    on Oct 29, 2026
  2. capy-ai commented on Oct 29, 2026

    @capy-ai

    Per-macroblock placement (candidate: do the per-block placement and window checks once a macroblock) is not going to a gated round.

    Instruction counts at one thread against main 2e3f0e2 (callgrind):

    build 76e0b1692494.0.720p, 100 frames 175433794e02.0.1080p, 40 frames
    checks once a macroblock, predictions queued until its motion is derived +0.29% +0.39% (first version)
    ceiling: the macroblock, length and stride tests deleted outright (unsound probe, not kept) -1.08% (-51 Ir/MB) -0.43% (-46 Ir/MB)

    The ceiling is well below the -2 to -3% expected: most of the per-block cost under predict_fast_body is window address arithmetic and the kernel calls, which any placement does, and the window test depends on each block's vector. Holding the picture's planes for a whole macroblock needs them apart from the decoder state the motion derivation borrows; the queue that does this costs more than the checks it removes. Output was identical to main on the streams tried. No bars were committed and no timed run was made. The code is kept as bundle material; it becomes worth retrying if the planes are split from the band data for another reason.

  3. capy-ai commented on Oct 30, 2026

    @capy-ai

    Bundle 5 (a band's kept view lent, the edge window's runs, the CAVLC P macroblock path, on top of bundle 3b) is not going in: it missed the one-thread guard B5-6. Every call was run once and none was void. Bars and composition were fixed before the runs.

    bar measure result
    B5-1 8 threads, discard, 32 training 0.9882 [0.9824, 0.9921], 30 of 32 faster met
    B5-2 to B5-4 8 threads, sd / 720p / 1080p and 4k / write 0.9679 / 0.9938 / 0.9912 and 0.9831 / 0.9905 met
    B5-5 1 thread, sd; the six CAVLC streams 0.9930; 0.9531 [0.9284, 0.9964] met
    B5-6 1 thread, all 32, discard and write (at most 1.000); each class and CABAC (at most 1.003) discard 1.0013 [0.9999, 1.0026], write 1.0017 [0.9984, 1.0034]; 4k write 1.0056 not met
    B5-7 8 threads, discard, 64 held-out 0.9831 [0.9762, 0.9889], 62 of 64 faster met
    B5-8 output identical on 96 streams at 1 and 8 threads, by a job that fails closed met

    Eight threads ran on c4-standard-16 with one thread a core. One thread ran on the fixed-frequency class from #55. At eight threads the gain matches the instruction ceiling measured before the bars (2.5% fewer instructions at the median). At one thread the CAVLC P path gives 4.7% on the six CAVLC streams. The 26 CABAC streams run 0.2% fewer instructions but take 0.2% more time, and that is what moves the median of all 32 above 1.000. The P path does not run on CABAC streams, and the band lookup does not run at one thread. On those streams that leaves the edge window's runs and the code's new placement, and which of the two costs the time was not measured.

    Information, the ratio to FFmpeg 9.0.2 with output discarded (base then the bundle): 8 threads 0.827 then 0.800 on the training streams and 0.802 then 0.789 on the held-out streams; 1 thread 0.928 then 0.929.

    The code is kept as bundle material. The eight-thread part (the band lookup) and the CAVLC P path each met their own bars. A later round could gate them without the edge window's runs, with bars committed before the run.

  4. gunwooterry commented on Nov 8, 2026

    @gunwooterry
    ContributorAuthor

    Frame-level picture scheduling is under review in #516, with its design as the first commit. The opt-in native backend schedules ready pictures against complete references and reconciles results in coded order. CPU-bounded workers, row-local wavefront resets and skipped-picture scratch checks are included. Immediate-predecessor reference chains remain serial; tiles are still a later unit. Build defaults, syntax admission, fallback and filter arithmetic are unchanged.

    Local verification passes 277 HEVC library tests, 11 backend tests, example tests, strict targeted Clippy, the opt-in CLI typecheck and 225 Python tests with seven skips. Complete 1/2/4/8-thread conformance now compares full records after normalizing only memory and scheduling counters. Exact-head conformance, sanitizer stress, paired reference checks against main and similarity are running; these are not yet correctness or performance results.

  5. gunwooterry commented on Nov 8, 2026

    @gunwooterry
    ContributorAuthor

    Picture scheduling follow-up in #516 at dd1a2aa044e5d7371e109b03bcf2efa80ee4ef6f:

    • Untimed diagnostics at ad6c4f4c complete all 30 reference inputs and 864 decoded pictures with zero frame-job overlap. Queue-budget barriers affect 264 pictures, wavefront selection 192, output reorder 135, output capacity 19 and unavailable references 15. Counts overlap and do not measure wait duration.
    • Picture jobs now reserve the smaller of their byte-derived bound and their existing hard picture-bin limit. Outstanding reservations still fit the stream allowance. A mutation-tested regression proves real overlap and identical output/error prefixes around both limits at 1/2/4/8 threads.
    • Local checks pass 281 ordinary and 283 diagnostic HEVC tests, 11 backend tests, strict Clippy in both modes, all 18 scheduler tests with one visible Linux CPU, and 67 CLI integration tests. The closed-stdout CI probe is isolated from other spawning tests without changing its assertions.
    • Exact-head full conformance, sanitizer stress, paired reference measurements and updated diagnostics are queued. Earlier conformance passes remain bound to 3014f881, not this head. New-head independent review, CI and similarity are still required; no general speedup is claimed.
  6. gunwooterry commented on Nov 8, 2026

    @gunwooterry
    ContributorAuthor

    Work on #516 now includes replayable header-dependency, output-visibility and bounded-output-lag models. The serial-exact deferred-output alternative is not selected, and no output-pipeline or row-publication implementation has been added. Existing decoder methods and build defaults remain unchanged. Any latency-allowing design still needs a concrete pending-budget and backpressure contract before implementation; model results are not decoder performance evidence.

  7. gunwooterry commented on Nov 8, 2026

    @gunwooterry
    ContributorAuthor

    The diagnostic-only row-read study in #516 completed all twenty-four selected non-wavefront inputs and 672 decoded pictures without errors or missing inputs. Source/prepared-input hashes and complete header/output graphs match the earlier study, and all 144 input/timing/filter-model schedules independently replay. The study does not implement row publication, change filtering or establish a measured speedup. Any row-level implementation remains a separate design decision.

  8. gunwooterry commented on Nov 8, 2026

    @gunwooterry
    ContributorAuthor

    Native picture-row scheduling in #516 now has an actual packet-boundary evidence driver at d1b4cf0. Generated 8/10-bit fixtures preserve pixels and semantic work at 1/2/4/8/automatic workers in hash and consume-only modes. Ordinary/diagnostic HEVC tests pass 308/321, kernel tests 50/51, backend tests 18 plus the process-wide allocation test, and the HEVC Python suite passes 311 with seven skips. Strict targeted Clippy, workflow checks and release-feature exclusions also pass. A bounded Actions preflight is running before complete native conformance, ThreadSanitizer stress and paired reference calls. Earlier complete-reference results are not credited as evidence for the new row pipeline; no speedup or merge readiness is claimed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:h264H.264 decoding, its kernels and its speedperfSpeed or memory usetrackingTracks a goal across several pull requests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions