vdecoderh264_web_logo

VDecoderH264 C++ library

v1.1.0

Table of contents

Overview

The VDecoderH264 C++ library provides a standalone software H.264 (AVC) video decoder for Linux, Windows, and embedded platforms. Key features:

  • Zero external dependencies: Contains complete, self-contained implementations of NAL parsing, Exp-Golomb bitstream reading, CABAC / CAVLC entropy decoding, inverse quantization, 4x4 / 8x8 IDCT transforms, Intra 4x4 / Intra 8x8 / Intra 16x16 / Chroma prediction, Inter motion compensation (sub-pel 6-tap FIR interpolation and bi-prediction), DPB (Decoded Picture Buffer) management, and an adaptive in-loop deblocking filter. Zero third-party media frameworks or OS-specific decoding services are required.
  • Pure C++ Source Architecture for Certification: Built 100% from pure C++17 source code with native SIMD intrinsics on x86 (SSE4.1, AVX2) and a portable C++ implementation of the same kernels. Uses no precompiled libraries, preloaded shared objects, or runtime dynamic loading (.dll, .so, LoadLibrary, dlopen), meeting all strict software certification standards.
  • Runs on any CPU: the pixel kernels are compiled for AVX2, SSE4.1 and portable C++ and the best level is selected at run time with CPUID; only the two kernel files get instruction-set flags, so the library never needs an instruction the CPU does not have. See SIMD acceleration and portability.
  • Cross-platform: Windows (MSVC 2022, x86-64), Linux x86-64 (GCC, Clang) and other processors such as Linux ARM64 / AArch64 (GCC, Clang, portable C++ kernels). The decoded output is bit-identical on all platforms and for every SIMD level.
  • Supported streams: progressive 8-bit 4:2:0 Baseline, Main and High profile streams: CAVLC and CABAC, P and B slices, weighted prediction, 4x4 / 8x8 transforms, custom scaling matrices, constrained intra prediction, multiple slices. Not supported: interlaced coding (PAFF, MBAFF), 4:2:2 / 4:4:4 / monochrome, bit depth above 8, lossless coding, FMO / ASO, SP / SI slices and data partitioning (such streams give no output).
  • Motion Vector Extraction: Provides direct spatial motion vector extraction through the getMotionVector() method, returning [x, y, dx, dy] for every decoded macroblock.
  • NV12 Output: Decodes Annex B elementary streams into standard NV12 uncompressed video frames (Y plane followed by interleaved UV plane), ready for display or downstream computer vision / AI processing.
  • Fast MV-Only Extraction: Supports lightweight motion vector extraction mode (CUSTOM_1 = 1.0f), which extracts motion vectors while bypassing NV12 frame allocation and memory copying, and a parse-only motion vector mode (CUSTOM_1 = 2.0f), which additionally skips all pixel reconstruction and deblocking (1.6x - 4.4x faster than CUSTOM_1 = 1.0f, same motion vectors).
  • Single-threaded by default, optional multi-threading: By default the decoder works only in the thread which calls transcode(...). Optionally (CUSTOM_2) the calling thread parses the bitstream while worker threads reconstruct and deblock the macroblock rows of the pictures (wavefront order, up to two pictures at a time). transcode(...) stays synchronous and returns exactly the same frames and motion vectors in the same calls (bit-identical output for any number of threads).
  • Standard VCodec Interface: Inherits from the open-source cr::video::VCodec interface (3rdparty/VCodec), enabling drop-in compatibility across the RapidPixel SDK ecosystem.
  • C++17 Architecture: Adheres strictly to the PIMPL pattern, shielding public headers from internal decoder structures and ensuring rapid compilation and clean ABI stability. The VDecoderH264 class depends on the VCodec class, which determines video frame structures. The example application uses SimpleUI for window display and drawing vector overlays, and a lightweight built-in H.264 elementary stream parser (H264Parser). The library uses the C++17 standard.

Versions

Table 1 - Library versions.

Version Release date What’s new
1.0.0 03.10.2026 - First version.
1.1.0 06.10.2026 - Decoding engine rewritten from the H.264 specification.
- Undefined behaviour of the previous engine removed (out-of-bounds neighbour access, misaligned loads, shifts of negative values, null pointer arithmetic, reachable assert).
- Multi-threading by macroblock rows with frame pipelining, identical output for any number of threads.
- The memory of the frames waiting for retrieval is limited.
- SIMD kernels selected at run time (AVX2, SSE4.1, portable C++): no global instruction-set flags, the library runs on any CPU.
- No compiler warnings (MSVC /W4, GCC, Clang).

Library files

The repository structure is organized as follows:

CMakeLists.txt ---------------- Main CMake build configuration.
README.md --------------------- Documentation and usage guide.
3rdparty ---------------------- External dependencies.
    CMakeLists.txt ------------ CMake inclusion script for third-party libraries.
    VCodec -------------------- VCodec interface and Frame submodule.
src --------------------------- Library source code.
    CMakeLists.txt ------------ CMake script for VDecoderH264 library (SIMD checks, per-file instruction-set flags).
    VDecoderH264.h ------------ Public library interface (PIMPL).
    VDecoderH264.cpp ---------- Public library implementation wrapper.
    VDecoderH264Version.h ----- Version definition header.
    VDecoderH264Version.h.in -- Template for CMake version generation.
    impl ---------------------- Internal decoder implementation files.
        VDecoderH264Impl.h ---- Internal PIMPL class declaration.
        VDecoderH264Impl.cpp -- PIMPL class implementation & frame buffer management.
        H264Compat.h ---------- Compiler intrinsics, bit operations and SIMD vector types (SSE4.1 / AVX2, portable C++).
        H264Engine.h ---------- Decoding engine interface: decoded pictures and motion vector grid.
        H264Engine.cpp -------- Decoder core: bitstream, parameter sets, DPB, reconstruction order, threads; portable kernels.
        H264EngineInternal.h -- Internal data structures (parameter sets, pictures, macroblock data, kernel table).
        H264Pipeline.h -------- Progress counters shared by the parsing thread and the worker threads.
        H264Pipeline.cpp ------ Progress counter implementation (spinning, then blocking wait).
        H264PixelKernels.inl -- Pixel kernels: prediction, transforms, deblocking (compiled once per SIMD level).
        H264Slice.inl --------- Slice data: macroblock layer, CABAC / CAVLC decoding, motion vector prediction.
        x86 ------------------- x86 SIMD levels.
            H264SimdLevels.h -- Levels built into the library, kernel sets of the levels.
            H264SimdLevels.cpp - Choice of the level for the running CPU (CPUID, XGETBV), no instruction-set flags.
            H264PixelSse41.cpp - Pixel kernels compiled for SSE4.1.
            H264PixelAvx2.cpp -- Pixel kernels compiled for AVX2.
example ----------------------- Standalone sample application.
    SimpleUI ------------------ Lightweight window and drawing library.
    H264Parser.h -------------- Standalone H.264 elementary stream parser header.
    H264Parser.cpp ------------ Standalone H.264 elementary stream parser implementation.
    CMakeLists.txt ------------ Example build configuration.
    main.cpp ------------------ Example demonstrating video decoding and MV extraction.

VDecoderH264 class description

VDecoderH264 class declaration

The VDecoderH264 class is declared in VDecoderH264.h:

namespace cr
{
namespace video
{
class VDecoderH264 : public VCodec
{
public:

    /// Class constructor.
    VDecoderH264();

    /// Class destructor.
    ~VDecoderH264() override;

    /// Get library version.
    static std::string getVersion();

    /// Decode H.264 video frame to NV12.
    bool transcode(Frame& src, Frame& dst) override;

    /// Get motion vectors from the most recently decoded frame.
    const std::vector<std::vector<float>>& getMotionVector();

    /// Set parameter value.
    bool setParam(VCodecParam id, float value) override;

    /// Get parameter value.
    float getParam(VCodecParam id) override;

    /// Execute codec command.
    bool executeCommand(VCodecCommand id) override;
};
}
}

getVersion method

The getVersion() method returns a string with the current version of the library:

static std::string getVersion();

Example usage:

std::cout << "VDecoderH264 version: " << cr::video::VDecoderH264::getVersion() << std::endl;

Terminal output:

VDecoderH264 version: 1.1.0

transcode method

The transcode(…) method decodes an incoming H.264 Annex B bitstream packet into an uncompressed NV12 video frame.

bool transcode(Frame& src, Frame& dst) override;
Parameter Type Description
src Frame& Source compressed video frame. Field src.fourcc must be Fourcc::H264. Data pointer src.data points to Annex B NAL unit(s) or access unit, and src.size specifies the byte size.
dst Frame& Destination uncompressed video frame. Upon successful decode, the decoder populates dst.data with raw NV12 pixel data, sets dst.width, dst.height, dst.size = width * height * 3 / 2, and dst.fourcc = Fourcc::NV12.

Returns: true if a frame was successfully decoded and available; false otherwise (e.g., parameter set NAL units without slice data, or corrupted packet).

At EOF, repeatedly pass a source frame with fourcc = Fourcc::H264 and size = 0 until transcode(...) returns false. This returns all queued and delayed pictures, including B-frames. No input data pointer is required for this call. dst.frameId is the decoded picture identifier in decoding order; it is not copied from the current input packet. The decoder copies NAL data internally, so the input buffer requires no prefix or suffix padding.

If one call carries several pictures, the first one is returned and the others wait in a queue for the following calls. The memory of the waiting pictures is limited to 1 GiB: when a call produces more, the oldest waiting pictures are dropped (this happens only if the caller passes much more data than it reads frames, for example a damaged stream that expands to many large pictures).

getMotionVector method

The getMotionVector() method retrieves the spatial motion vectors for the most recently decoded video frame.

const std::vector<std::vector<float>>& getMotionVector();

Returns: A two-dimensional vector where each element is a 4-component vector [x, y, dx, dy]:

  • x: Horizontal pixel coordinate of the visible top-left corner of the 16x16 macroblock after SPS cropping.
  • y: Vertical pixel coordinate of the visible top-left corner of the 16x16 macroblock after SPS cropping.
  • dx: Horizontal motion displacement in pixels (average of the 16 4x4-block motion vectors of the macroblock, list 0, or list 1 for macroblocks predicted from list 1 only).
  • dy: Vertical motion displacement in pixels (same averaging).

The method returns one element per macroblock intersecting the visible picture, in raster order (for example 8160 elements for 1920x1080). A crop through a macroblock includes its visible portion; its position is clipped to the visible image and its displacement still averages the complete macroblock. For intra macroblocks (and all macroblocks of I / IDR frames) dx = dy = 0. The returned reference points to an internal buffer updated by transcode(...) and RESET.

Example:

auto mvs = decoder.getMotionVector();
for (const auto& mv : mvs)
{
    float x = mv[0];
    float y = mv[1];
    float dx = mv[2];
    float dy = mv[3];
    // Process or visualize motion vector
}

setParam method

The setParam(…) method configures decoder parameters.

bool setParam(VCodecParam id, float value) override;
Parameter Type Description
id VCodecParam Parameter identifier from the VCodecParam enum.
value float Value to assign to the parameter.

Supported parameters:

  • VCodecParam::LOG_LEVEL: Logging verbosity (0 - disabled, 1 - console).
  • VCodecParam::TYPE: Codec mode (1 - software decoder).
  • VCodecParam::CUSTOM_1: Decoding mode (default 0.0f):
    • 0.0f - full video decoding to NV12 + motion vectors.
    • 1.0f - motion vectors only: the frame is fully decoded, but NV12 frame buffer copy is skipped (dst gets width, height, fourcc = NV12 and size = 0).
    • 2.0f - motion vectors from parsing only: no pixel reconstruction and no deblocking, dst is set as in mode 1.0f. Motion vectors and the sequence of returned frames are identical to modes 0.0f and 1.0f.
    • Switching between 0.0f and 1.0f is possible at any time (bit-exact). Mode 2.0f does not reconstruct pixels, so after switching from 2.0f back to 0.0f the pictures are correct only from the next IDR picture (motion vectors are always correct).
  • VCodecParam::CUSTOM_2: Number of decoding threads (default 1.0f):
    • 1.0f - single-threaded decoding (default): everything is decoded in the thread which calls transcode(...), no worker threads are created.
    • 0.0f - automatic: up to 4 threads depending on the number of logical processors.
    • N >= 2 - parsing thread plus N - 1 worker threads (at most 5 threads).
    • The output is bit-identical for every value. Set the value before decoding starts (or execute VCodecCommand::RESET after changing it): more than two threads need an internal picture layout which is chosen when the stream format (SPS) is activated, so raising the value from 1.0f during decoding uses at most one worker thread until RESET or a resolution change. Decreasing the value takes effect immediately. Only 8-bit 4:2:0 progressive pictures are decoded with worker threads.
  • VCodecParam::CUSTOM_3: Color range control for output NV12 frame (default 1.0f):
    • 1.0f: Automatic (default). If bitstream signals or infers Limited Range and frame luminance data analysis confirms limited dynamic range [16..235], automatically expands luma [16..235] and chroma [16..240] to Full Range [0..255] during NV12 assembly. Full-range frames are preserved without clipping or alteration.
    • 0.0f: Disabled / pass-through: pass-through raw decoded samples without range expansion.
    • 2.0f: Forced expansion: always expand Limited Range to Full Range regardless of bitstream headers or pixel statistics.

Returns: true if the parameter was accepted, false otherwise.

getParam method

The getParam(…) method queries parameter values from the decoder.

float getParam(VCodecParam id) override;

Returns: Value of the specified parameter, or -1.0f if unsupported.

executeCommand method

The executeCommand(…) method sends control commands to the decoder.

bool executeCommand(VCodecCommand id) override;

Supported commands:

  • VCodecCommand::RESET: Flushes the Decoded Picture Buffer (DPB), clears internal state, and resets sequence parameters ready for a new stream.

Returns: true if the command was executed successfully.

Build and connect to your project

Typical commands to build VDecoderH264 library:

cd VDecoderH264
mkdir build
cd build
cmake ..
make

If you want to connect the VDecoderH264 library to your CMake project as source code, you can do the following. For example, if your repository has the structure:

CMakeLists.txt
src
    CMakeList.txt
    yourLib.h
    yourLib.cpp

Create folder 3rdparty in your repository and copy VDecoderH264 folder there. The new structure of your repository:

CMakeLists.txt
src
    CMakeList.txt
    yourLib.h
    yourLib.cpp
3rdparty
    VDecoderH264

Create a CMakeLists.txt file in the 3rdparty folder. The CMakeLists.txt file should contain:

cmake_minimum_required(VERSION 3.13)

################################################################################
## 3RD-PARTY
## dependencies for the project
################################################################################
project(3rdparty LANGUAGES CXX)

################################################################################
## SETTINGS
## basic 3rd-party settings before use
################################################################################
# To inherit the top-level architecture when the project is used as a submodule.
SET(PARENT ${PARENT}_YOUR_PROJECT_3RDPARTY)
# Disable self-overwriting of parameters inside included subdirectories.
SET(${PARENT}_SUBMODULE_CACHE_OVERWRITE OFF CACHE BOOL "" FORCE)

################################################################################
## INCLUDING SUBDIRECTORIES
## Adding subdirectories according to the 3rd-party configuration
################################################################################
add_subdirectory(VDecoderH264)

Next, you need to include the 3rdparty folder in the main CMakeLists.txt file of your repository. Add the following line at the end of your main CMakeLists.txt:

add_subdirectory(3rdparty)

Next, you need to include the VDecoderH264 library in your src/CMakeLists.txt file:

target_link_libraries(${PROJECT_NAME} VDecoderH264)

Include the public header in your code:

#include "VDecoderH264.h"

SIMD acceleration and portability

The pixel kernels of the decoder (inter prediction, inverse transforms, intra prediction, deblocking filter) are written once (src/impl/H264PixelKernels.inl) on a small vector type and compiled for several instruction-set levels. The level is chosen once per process, when the first decoder is created:

CPU Kernels used How the level is chosen
x86 / x86-64 AVX2 -> SSE4.1 -> portable C++ CPUID (SSSE3 and SSE4.1 for the SSE4.1 level). AVX2 also needs an operating system that saves the YMM registers (OSXSAVE and XGETBV).
ARM64 / AArch64 and any other CPU portable C++ No intrinsics are compiled.

Everything else (bitstream parsing, CABAC / CAVLC, parameter sets, DPB, threads, NV12 output) is compiled without instruction-set flags. A binary built with any compiler therefore starts and decodes on every CPU of its architecture: checked with emulated Haswell (AVX2), Nehalem (SSE4.2), Penryn (SSE4.1) and Conroe (SSSE3, portable kernels) CPUs. The decoded output is bit-identical for every level.

Instruction-set flags are applied per source file, never globally:

Source files Flags
src/impl/x86/H264PixelSse41.cpp -msse4.1 (GCC / Clang). MSVC needs no flag.
src/impl/x86/H264PixelAvx2.cpp -mavx2 (GCC / Clang), /arch:AVX2 (MSVC).
All other files, including the CPU detection (src/impl/x86/H264SimdLevels.cpp) and the portable kernels No instruction-set flags (no -march=native, no global -mavx2 or /arch:AVX2).

The kernel code of each level is in its own namespace (portable, sse41, avx2), so that the linker never replaces a function of a lower level by a copy compiled with AVX2 or SSE4.1 code generation. CMake checks with try_compile that the compiler can build the x86 kernels. If it cannot (old compiler, unusual toolchain, universal macOS binary), the affected levels are left out, a message is printed during configuration and the next lower level is used. Options:

Option / macro Effect
-DVDECODERH264_ENABLE_SIMD=OFF Builds the portable kernels only (defines VDECODERH264_NO_SIMD).
VDECODERH264_NO_SSE41, VDECODERH264_NO_AVX2 Leave out the SSE4.1 and AVX2 kernels (VDECODERH264_NO_SSE41) or only the AVX2 kernels. CMake defines them automatically when a check fails.

CMake adds these macros as private definitions of the VDecoderH264 target. Without CMake, compile all src/**/*.cpp files as C++17, add the flags of the table to the two kernel files (or define the macros above) and link the system thread library (-pthread).

Simple example

The example application (example/main.cpp) demonstrates opening an H.264 video file, decoding frames to NV12, extracting motion vectors, and rendering them using SimpleUI:

#include <iostream>
#include <string>
#include <vector>
#include <cmath>
#include "VDecoderH264.h"
#include "H264Parser.h"
#include "SimpleUi.h"

// Link namespaces.
using namespace std;
using namespace cr::ui;
using namespace cr::video;

// Entry point.
int main(int argc, char* argv[])
{
    // Print library version.
    cout << "VDecoderH264 v" << VDecoderH264::getVersion() << endl;

    // Get input H.264 file path.
    string videoPath;
    if (argc > 1)
    {
        videoPath = argv[1];
    }
    else
    {
        cout << "Enter path to video file (*.h264): ";
        getline(cin, videoPath);
    }
    if (!videoPath.empty() && (videoPath.front() == '"' || videoPath.front() == '\''))
        videoPath.erase(0, 1);
    if (!videoPath.empty() && (videoPath.back() == '"' || videoPath.back() == '\''))
        videoPath.pop_back();
    if (videoPath.empty())
        videoPath = "test.h264";

    // Open H.264 elementary stream file.
    H264Parser parser;
    if (!parser.open(videoPath))
    {
        cerr << "Failed to open video file: " << videoPath << endl;
        return -1;
    }

    // Initialize decoder and UI window.
    VDecoderH264 decoder;
    SimpleUiWindow window("VDecoderH264 - Motion Vectors");
    window.show();

    // Input H264 frame.
    Frame h264Frame(640, 480, Fourcc::H264);
    // Decoded NV12 frame.
    Frame nv12Frame(640, 480, Fourcc::NV12);
    // Converted YUV frame.
    Frame yuvFrame(640, 480, Fourcc::YUV24);

    // Decode and display video frames.
    while (window.isOpen())
    {
        if (!parser.getFrame(h264Frame))
            h264Frame.size = 0;
        // Decode H.264 frame to NV12 format.
        if (!decoder.transcode(h264Frame, nv12Frame))
        {
            if (h264Frame.size == 0)
                break;
            continue;
        }

        // Convert NV12 frame to YUV format with only Y channel.
        if (nv12Frame.width != yuvFrame.width || nv12Frame.height != yuvFrame.height)
            yuvFrame = Frame(nv12Frame.width, nv12Frame.height, Fourcc::YUV24);
        memset(yuvFrame.data, 128, yuvFrame.size);
        for (int i = 0; i < yuvFrame.size; i = i + 3)
            yuvFrame.data[i] = nv12Frame.data[i / 3];

        // Overlay motion vectors on the frame.
        vector<vector<float>> vectors = decoder.getMotionVector();
        for (const auto& mv : vectors)
            if (mv.size() >= 4 && (std::abs(mv[2]) > 0.5f || std::abs(mv[3]) > 0.5f))
                SimpleUiDraw::drawLine(yuvFrame, (int)mv[0], (int)mv[1],
                (int)(mv[0] + mv[2]), (int)(mv[1] + mv[3]), {0, 255, 0}, 1);

        // Render frame and process UI events (ESC to exit).
        window.render(yuvFrame);
        if (window.getKey(30) == 27)
            break;
    }

    return 0;
}

Table of contents