]> git.nihav.org Git - nihav-player.git/commitdiff
hwdec_vaapi: add (simplified) VP9 decoder
authorKostya Shishkov <kostya.shishkov@gmail.com>
Fri, 30 Oct 2026 19:55:25 +0000 (20:55 +0100)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Fri, 30 Oct 2026 19:55:25 +0000 (20:55 +0100)
hwdec-vaapi/src/lib.rs
hwdec-vaapi/src/vp9.rs [new file with mode: 0644]

index ff993007af476e501f7c6e5b0b93c061d21f3dcd..aaeeb40e889ba6b8767c59fd3bd69535844db385 100644 (file)
@@ -435,3 +435,6 @@ fn fill_frame(ifmt: VAImageFormat, pic: &Picture<PictureSync>, frm: &mut NABuffe
 #[allow(dead_code)]
 mod h264;
 pub use h264::new_h264_hwdec;
+
+mod vp9;
+pub use vp9::new_vp9_hwdec;
diff --git a/hwdec-vaapi/src/vp9.rs b/hwdec-vaapi/src/vp9.rs
new file mode 100644 (file)
index 0000000..506ec3c
--- /dev/null
@@ -0,0 +1,652 @@
+use std::collections::VecDeque;
+use std::rc::Rc;
+
+use nihav_core::codecs::*;
+use nihav_core::io::byteio::*;
+use nihav_core::io::bitreader::*;
+
+use libva::*;
+
+use crate::{HWDecoder, HWDecoderCore, HWWrapper, fill_frame, profile_name};
+
+const NUM_REFS: usize = 8;
+const MAX_SB_FRAMES: usize = 8;
+
+trait ReadBitsSgn {
+    fn read_sgn(&mut self, n: u8) -> BitReaderResult<i32>;
+}
+
+impl<'a> ReadBitsSgn for BitReader<'a> {
+    fn read_sgn(&mut self, n: u8) -> BitReaderResult<i32> {
+        let val = self.read(n)? as i32;
+        if self.read_bool()? {
+            Ok(-val)
+        } else {
+            Ok(val)
+        }
+    }
+}
+
+const VP9_PROFILES: &[i32; 4] = &[
+    VAProfile::VAProfileVP9Profile0, VAProfile::VAProfileVP9Profile1,
+    VAProfile::VAProfileVP9Profile2, VAProfile::VAProfileVP9Profile3
+];
+
+enum QFrame {
+    Frame(bool, bool, Picture<PictureEnd>),
+    ShowAgain(u32),
+}
+
+#[allow(dead_code)]
+struct VaapiInternals {
+    display:        Rc<Display>,
+    context:        Rc<Context>,
+    surfaces:       Vec<Surface>,
+    ifmt:           VAImageFormat,
+    queue:          VecDeque<(u64, QFrame)>,
+    ref_pics:       Vec<Picture<PictureSync>>,
+}
+
+#[derive(Default)]
+struct FrameState {
+    width:                  u16,
+    height:                 u16,
+    profile:                u8,
+    depth:                  u8,
+    cspace:                 u8,
+    crange:                 bool,
+    profile_supp:           [bool; 4],
+    sub_x:                  u8,
+    sub_y:                  u8,
+    seg_tree_probs:         [u8; 7],
+    seg_pred_probs:         [u8; 3],
+    refresh_frame_flags:    u8,
+}
+
+fn read_prob(br: &mut BitReader) -> DecoderResult<u8> {
+    if br.read_bool()? {
+        Ok(br.read(8)? as u8)
+    } else {
+        Ok(255)
+    }
+}
+
+impl FrameState {
+    fn read_color_config(&mut self, br: &mut BitReader) -> DecoderResult<()> {
+        if self.profile >= 2 {
+            self.depth                  = if br.read_bool()? { 12 } else { 10 };
+        } else {
+            self.depth = 8;
+        }
+        self.cspace                     = br.read(3)? as u8;
+        if self.cspace != 7 {
+            self.crange                 = br.read_bool()?;
+            if self.profile == 1 || self.profile == 3 {
+                self.sub_x              = br.read(1)? as u8;
+                self.sub_y              = br.read(1)? as u8;
+                let reserved_zero       = br.read_bool()?;
+                validate!(!reserved_zero);
+            } else {
+                self.sub_x = 1;
+                self.sub_y = 1;
+            }
+        } else {
+            self.crange = true;
+            if self.profile == 1 || self.profile == 3 {
+                self.sub_x = 0;
+                self.sub_y = 0;
+                let reserved_zero       = br.read_bool()?;
+                validate!(!reserved_zero);
+            }
+        }
+        Ok(())
+    }
+    fn read_frame_size(&mut self, br: &mut BitReader) -> DecoderResult<()> {
+        let width1                      = br.read(16)? as u16;
+        let height1                     = br.read(16)? as u16;
+        validate!(width1 < 16384 && height1 < 16384);
+        self.width  = width1 + 1;
+        self.height = height1 + 1;
+        Ok(())
+    }
+    fn read_render_size(&mut self, br: &mut BitReader) -> DecoderResult<()> {
+        if br.read_bool()? {
+            let _render_width1          = br.read(16)? as u16;
+            let _render_height1         = br.read(16)? as u16;
+        }
+        Ok(())
+    }
+    fn do_frame(&mut self, vactx: &mut VaapiInternals, src: &[u8], timestamp: u64, ref_frames: &mut [u32; NUM_REFS]) -> DecoderResult<()> {
+        let mut br = BitReader::new(src, BitReaderMode::BE);
+        let frame_marker                = br.read(2)?;
+        if frame_marker != 2 {
+            return Err(DecoderError::InvalidData);
+        }
+        let profile                     = br.read(2)? as usize;
+        if !self.profile_supp[profile] {
+            println!("Unsupported {}", profile_name(VP9_PROFILES[profile]));
+            return Err(DecoderError::NotImplemented);
+        }
+        if profile == 3 {
+            let reserved_zero           = br.read_bool()?;
+            if reserved_zero {
+                return Err(DecoderError::InvalidData);
+            }
+        }
+        if br.read_bool()? {
+            let idx                     = br.read(3)? as usize;
+            if ref_frames[idx] == VA_INVALID_ID {
+                return Err(DecoderError::MissingReference);
+            }
+            vactx.queue.push_back((timestamp, QFrame::ShowAgain(ref_frames[idx])));
+            return Ok(());
+        }
+        let frame_type                  = br.read(1)?;
+        let show_frame                  = br.read_bool()?;
+        let error_resilient_mode        = br.read_bool()?;
+
+        let intra_only;
+        let mut reset_frame_context = 0;
+        let mut allow_high_precision_mv = false;
+        let mut interp_type = 0;
+        let mut ref_frame_idx = [0; 3];
+        let mut ref_frame_sign_bias = [0; 3];
+        if frame_type == 0 {
+            let sync_code               = br.read(24)?;
+            if sync_code != 0x498342 {
+                return Err(DecoderError::InvalidData);
+            }
+            self.read_color_config(&mut br)?;
+            self.read_frame_size(&mut br)?;
+            self.read_render_size(&mut br)?;
+            intra_only = true;
+            self.refresh_frame_flags = 0xFF;
+        } else {
+            if self.width == 0 {
+                return Err(DecoderError::MissingReference)?;
+            }
+            intra_only                  = !show_frame && br.read_bool()?;
+            if !error_resilient_mode {
+                reset_frame_context     = br.read(2)?;
+            }
+            if intra_only {
+                let sync_code           = br.read(24)?;
+                if sync_code != 0x498342 {
+                    return Err(DecoderError::InvalidData);
+                }
+                if profile > 0 {
+                    self.read_color_config(&mut br)?;
+                } else {
+                    self.cspace = 1;
+                    self.sub_x = 1;
+                    self.sub_y = 1;
+                    self.depth = 8;
+                }
+                self.refresh_frame_flags     = br.read(8)? as u8;
+                self.read_frame_size(&mut br)?;
+                self.read_render_size(&mut br)?;
+            } else {
+                self.refresh_frame_flags     = br.read(8)? as u8;
+                for (idx, sign_bias) in ref_frame_idx.iter_mut().zip(ref_frame_sign_bias.iter_mut()) {
+                    *idx                = br.read(3)?;
+                    *sign_bias          = br.read(1)?;
+                }
+                let mut found = false;
+                for _i in 0..3 {
+                    if br.read_bool()? {
+                        found = true;
+                        break;
+                    }
+                }
+                if !found {
+                    self.read_frame_size(&mut br)?;
+                }
+                self.read_render_size(&mut br)?;
+
+                allow_high_precision_mv = br.read_bool()?;
+                if br.read_bool()? {
+                    interp_type = 4;
+                } else {
+                    const INTERP_TYPES: [u8; 4] = [1, 0, 2, 3];
+                    let idx             = br.read(2)? as usize;
+                    interp_type = INTERP_TYPES[idx];
+                }
+            }
+        }
+
+        let refresh_frame_context;
+        let frame_par_dec_mode;
+        if !error_resilient_mode {
+            refresh_frame_context       = br.read_bool()?;
+            frame_par_dec_mode          = br.read_bool()?;
+        } else {
+            refresh_frame_context = false;
+            frame_par_dec_mode = true;
+        }
+        let frame_context_idx           = br.read(2)?;
+
+        let loop_filter_level           = br.read(6)? as i8;
+        let loop_filter_sharpness       = br.read(3)? as u8;
+        let loop_filter_delta           = br.read_bool()?;
+        let mut update_ref_delta = [false; 4];
+        let mut lf_ref_deltas = [0i8; 4];
+        let mut upd_mode_delta = [false; 2];
+        let mut lf_mode_deltas = [0i8; 2];
+        if loop_filter_delta {
+            let loop_filter_delta_update = br.read_bool()?;
+            if loop_filter_delta_update {
+                for (upd_ref, lf_ref) in update_ref_delta.iter_mut().zip(lf_ref_deltas.iter_mut()) {
+                    *upd_ref            = br.read_bool()?;
+                    if *upd_ref {
+                        *lf_ref         = br.read_sgn(6)? as i8;
+                    }
+                }
+                for (upd_ref, lf_ref) in upd_mode_delta.iter_mut().zip(lf_mode_deltas.iter_mut()) {
+                    *upd_ref            = br.read_bool()?;
+                    if *upd_ref {
+                        *lf_ref         = br.read_sgn(6)? as i8;
+                    }
+                }
+            }
+        }
+
+        let base_q_idx                  = br.read(8)? as i32;
+        let dq_y_dc                     = if br.read_bool()? { br.read_sgn(4)? } else { 0 };
+        let dq_uv_dc                    = if br.read_bool()? { br.read_sgn(4)? } else { 0 };
+        let dq_uv_ac                    = if br.read_bool()? { br.read_sgn(4)? } else { 0 };
+        let lossless = base_q_idx == 0 && dq_y_dc == 0 && dq_uv_dc == 0 && dq_uv_ac == 0;
+
+        let q_y_dc = (base_q_idx + dq_y_dc).clamp(0, 255) as i16;
+        let q_y_ac = base_q_idx as i16;
+        let q_uv_dc = (base_q_idx + dq_uv_dc).clamp(0, 255) as i16;
+        let q_uv_ac = (base_q_idx + dq_uv_ac).clamp(0, 255) as i16;
+        let seg_enabled                 = br.read_bool()?;
+        let mut seg_update_map = false;
+        let mut seg_temp_update = false;
+        if seg_enabled {
+            seg_update_map              = br.read_bool()?;
+            if seg_update_map {
+                for prob in self.seg_tree_probs.iter_mut() {
+                    *prob               = read_prob(&mut br)?;
+                }
+                seg_temp_update         = br.read_bool()?;
+                if seg_temp_update {
+                    for prob in self.seg_pred_probs.iter_mut() {
+                        *prob           = read_prob(&mut br)?;
+                    }
+                } else {
+                    for prob in self.seg_pred_probs.iter_mut() {
+                        *prob = 255;
+                    }
+                }
+            }
+            let seg_update_data         = br.read_bool()?;
+            if seg_update_data {
+unimplemented!();
+            }
+        }
+
+        let sb64_cols = (((self.width + 7) >> 3) + 7) >> 3;
+        let mut min_log2_tile_cols = 0;
+        while (64 << min_log2_tile_cols) < sb64_cols {
+            min_log2_tile_cols += 1;
+        }
+        let mut max_log2_tile_cols = 1;
+        while (sb64_cols >> max_log2_tile_cols) >= 4 {
+            max_log2_tile_cols += 1;
+        }
+        max_log2_tile_cols -= 1;
+        let mut tile_cols_log2 = min_log2_tile_cols;
+        while (tile_cols_log2 < max_log2_tile_cols) && br.read_bool()? {
+            tile_cols_log2 += 1;
+        }
+        let mut tile_rows_log2          = br.read(1)? as u8;
+        if tile_rows_log2 == 1 && br.read_bool()? {
+            tile_rows_log2 += 1;
+        }
+
+        let header_size                 = br.read(16)? as u16;
+        let bytes_read = (br.tell() + 7) / 8;
+
+        if vactx.surfaces.is_empty() {
+panic!("ran out of free surfaces");
+//            return Err(DecoderError::AllocError);
+        }
+        let surface = vactx.surfaces.pop().unwrap();
+        //let surface_id = surface.id();
+        let mut pic = Picture::new(timestamp, vactx.context.clone(), surface);
+
+        let pic_fields = VP9PicFields::new(self.sub_x.into(), self.sub_y.into(), frame_type, show_frame as u32,
+                error_resilient_mode as u32, intra_only as u32, allow_high_precision_mv as u32,
+                interp_type.into(), frame_par_dec_mode as u32,
+                reset_frame_context, refresh_frame_context as u32, frame_context_idx,
+                seg_enabled as u32, seg_temp_update as u32, seg_update_map as u32,
+                // last, golden, altref
+                ref_frame_idx[0], ref_frame_sign_bias[0],
+                ref_frame_idx[1], ref_frame_sign_bias[1],
+                ref_frame_idx[2], ref_frame_sign_bias[2],
+                lossless as u32);
+        let pic_param = PictureParameterBufferVP9::new(self.width, self.height, *ref_frames, &pic_fields,
+                loop_filter_level as u8, loop_filter_sharpness, tile_rows_log2, tile_cols_log2,
+                bytes_read as u8, header_size, self.seg_tree_probs,
+                self.seg_pred_probs, profile as u8, self.depth);
+        let pic_param = BufferType::PictureParameter(PictureParameter::VP9(pic_param));
+        let buf = vactx.context.create_buffer(pic_param).map_err(|_| DecoderError::Bug)?;
+        pic.add_buffer(buf);
+
+        let lf_scale = if loop_filter_level >= 32 { 2 } else { 1 };
+        let mut filter_level = [[0; 2]; 4];
+        filter_level[0] = [loop_filter_level as u8; 2];
+        for (i, (fl, &delta)) in filter_level.iter_mut().zip(lf_ref_deltas.iter()).enumerate() {
+            for (val, &mode) in fl.iter_mut().zip(lf_mode_deltas.iter()) {
+                *val = (loop_filter_level + (delta + if i > 0 { mode } else { 0 }) * lf_scale).clamp(0, 63) as u8;
+            }
+        }
+        let seg_flags = VP9SegmentFlags::new(0, 0, 0); // TODO support segmented stuff?
+        let mut seg_params = std::array::from_fn(|_i| SegmentParameterVP9::new(&seg_flags, [[0; 2]; 4], 0, 0, 0, 0));
+        seg_params[0] = SegmentParameterVP9::new(&seg_flags, filter_level, q_y_ac, q_y_dc, q_uv_ac, q_uv_dc);
+        let slc_param = SliceParameterBufferVP9::new(src.len() as u32, 0, VASliceDataFlag::All as u32, seg_params);
+        let slc_param = BufferType::SliceParameter(SliceParameter::VP9(slc_param));
+        let buf = vactx.context.create_buffer(slc_param).map_err(|_| DecoderError::Bug)?;
+        pic.add_buffer(buf);
+
+        let pic_data = BufferType::SliceData(src.to_vec());
+        let buf = vactx.context.create_buffer(pic_data).map_err(|_| DecoderError::Bug)?;
+        pic.add_buffer(buf);
+
+        let cur_id = pic.surface().id();
+        for (i, rf) in ref_frames.iter_mut().enumerate() {
+            if (self.refresh_frame_flags & (1 << i)) != 0 {
+                *rf = cur_id;
+            }
+        }
+
+        let bpic = pic.begin().expect("begin");
+        let rpic = bpic.render().expect("render");
+        let epic = rpic.end().expect("end");
+
+        vactx.queue.push_back((timestamp, QFrame::Frame(frame_type == 0, show_frame, epic)));
+
+        Ok(())
+    }
+}
+
+pub struct VaapiVP9Decoder {
+    info:           NACodecInfoRef,
+    vaapi:          Option<VaapiInternals>,
+    needs_derive:   bool,
+    tb_num:         u32,
+    tb_den:         u32,
+    out_frm:        NABufferType,
+    fstate:         FrameState,
+    ref_ids:        [u32; NUM_REFS],
+}
+
+impl Default for VaapiVP9Decoder {
+    fn default() -> Self {
+        Self {
+            info:           NACodecInfoRef::default(),
+            vaapi:          None,
+            needs_derive:   false,
+            tb_num:         0,
+            tb_den:         0,
+            out_frm:        NABufferType::None,
+            fstate:         FrameState::default(),
+            ref_ids:        [VA_INVALID_ID; NUM_REFS],
+        }
+    }
+}
+
+impl VaapiVP9Decoder {
+    pub fn new() -> Self { Self::default() }
+}
+
+impl HWDecoderCore for VaapiVP9Decoder {
+    fn init(&mut self, info: NACodecInfoRef) -> DecoderResult<()> {
+        if let NACodecTypeInfo::Video(vinfo) = info.get_properties() {
+            let width  = (vinfo.get_width()  + 15) & !15;
+            let height = (vinfo.get_height() + 15) & !15;
+
+            let display = Display::open_silently().ok_or_else(||{ println!(" cannot open display"); DecoderError::InvalidData})?;
+
+            let num_surfaces = NUM_REFS + MAX_SB_FRAMES;
+
+            for (supp, &va_profile) in self.fstate.profile_supp.iter_mut().zip(VP9_PROFILES.iter()) {
+                if let Ok(profiles) = display.query_config_profiles() {
+                    *supp = profiles.contains(&va_profile);
+                    if let Ok(points) = display.query_config_entrypoints(va_profile) {
+                        if !points.contains(&VAEntrypoint::VAEntrypointVLD) {
+                            *supp = false;
+                        }
+                    }
+                } else {
+                    return Err(DecoderError::Bug);
+                }
+            }
+
+            let needs_derive= if let Ok(vendor) = display.query_vendor_string() {
+                    vendor.contains("Kaby Lake")
+                } else { false };
+
+            let config = display.create_config(vec![
+                    VAConfigAttrib { type_: VAConfigAttribType::VAConfigAttribRTFormat, value: RTFormat::YUV420.into() },
+                ], VP9_PROFILES[0], VAEntrypoint::VAEntrypointVLD).map_err(|_| {
+println!("config creation failed!");
+                    DecoderError::Bug
+                })?;
+            let surfaces = display.create_surfaces(RTFormat::YUV420, None, width as u32, height as u32, Some(UsageHint::Decoder.into()), num_surfaces as u32).map_err(|_| DecoderError::AllocError)?;
+            let context = display.create_context(&config, width as i32, height as i32, Some(&surfaces), true).map_err(|_| DecoderError::Bug)?;
+
+            let image_formats = display.query_image_formats().map_err(|_| DecoderError::Bug)?;
+            validate!(!image_formats.is_empty());
+            let mut ifmt = image_formats[0];
+            for fmt in image_formats.iter() {
+                if fmt.bits_per_pixel == 12 {
+                    ifmt = *fmt;
+                    break;
+                }
+            }
+
+            let ref_pics = Vec::with_capacity(NUM_REFS);
+            let queue = VecDeque::with_capacity(MAX_SB_FRAMES);
+            self.vaapi = Some(VaapiInternals { display, context, surfaces, ifmt, ref_pics, queue });
+            self.needs_derive = needs_derive;
+
+            let vinfo = NAVideoInfo::new(vinfo.get_width(), vinfo.get_height(), false, YUV420_FORMAT);
+            self.info = NACodecInfo::new_ref(info.get_name(), NACodecTypeInfo::Video(vinfo), info.get_extradata()).into_ref();
+            self.out_frm = alloc_video_buffer(vinfo, 4)?;
+
+            Ok(())
+        } else {
+            Err(DecoderError::InvalidData)
+        }
+    }
+    fn decode(&mut self, pkt: &NAPacket) -> DecoderResult<()> {
+        let src = pkt.get_buffer();
+        validate!(!src.is_empty());
+        let vactx = if let Some(ref mut ctx) = self.vaapi { ctx } else { return Err(DecoderError::Bug) };
+
+        let timestamp = pkt.get_pts().unwrap_or(0);
+        self.tb_num = pkt.ts.tb_num;
+        self.tb_den = pkt.ts.tb_den;
+
+        let mut frame_sizes = [0; 8];
+        let last_byte = src[src.len() - 1];
+        if (last_byte & 0xE0) == 0xC0 {
+            let width = usize::from((last_byte >> 3) & 3) + 1;
+            let nframes = usize::from(last_byte & 7) + 1;
+            let sfidx_size = nframes * width + 2;
+            validate!(src.len() >= nframes + sfidx_size);
+            let mut rest_size = src.len() - sfidx_size;
+            let idx_pos = src.len() - sfidx_size + 1;
+            for (i, fsize) in frame_sizes.iter_mut().enumerate().take(nframes) {
+                let size_src = &src[idx_pos + width * i..];
+                *fsize = match width {
+                        1 => usize::from(size_src[0]),
+                        2 => usize::from(read_u16le(size_src).unwrap_or_default()),
+                        3 => read_u24le(size_src).unwrap_or_default() as usize,
+                        _ => read_u32le(size_src).unwrap_or_default() as usize,
+                    };
+                    validate!(*fsize > 0 && *fsize <= rest_size);
+                rest_size -= *fsize;
+            }
+        } else {
+            frame_sizes[0] = src.len();
+        }
+
+        let mut offset = 0;
+        for &fsize in frame_sizes.iter() {
+            if fsize == 0 { break; }
+            self.fstate.do_frame(vactx, &src[offset..][..fsize], timestamp, &mut self.ref_ids)?;
+            offset += fsize;
+        }
+
+        Ok(())
+    }
+    fn get_frame(&mut self) -> Option<NAFrameRef> {
+        if let Some(ref mut vactx) = self.vaapi {
+            while let Some((ts, entry)) = vactx.queue.pop_front() {
+                match entry {
+                    QFrame::Frame(intra, show_frame, epic) => {
+                        if let Ok(pic) = epic.sync() {
+                            if show_frame {
+                                let _ = fill_frame(vactx.ifmt, &pic, &mut self.out_frm, self.needs_derive);
+                            }
+                            let id = pic.surface().id();
+                            if self.ref_ids.contains(&id) {
+                                vactx.ref_pics.push(pic);
+                            } else if let Ok(surf) = pic.take_surface() {
+                                vactx.surfaces.push(surf);
+                            } else {
+                                panic!("can't take surface");
+                            }
+                            let mut idx = 0;
+                            while idx < vactx.ref_pics.len() {
+                                if !self.ref_ids.contains(&vactx.ref_pics[idx].surface().id()) {
+                                    let pic = vactx.ref_pics.remove(idx);
+                                    if let Ok(surf) = pic.take_surface() {
+                                        vactx.surfaces.push(surf);
+                                    } else {
+                                        panic!("can't take surface");
+                                    }
+                                } else {
+                                    idx += 1;
+                                }
+                            }
+                            if !show_frame {
+                                continue;
+                            }
+                            let ts = NATimeInfo::new(Some(ts), None, None, self.tb_num, self.tb_den);
+                            return Some(NAFrame::new(ts, if intra { FrameType::I } else { FrameType::P }, intra, self.info.clone(), self.out_frm.clone()).into_ref());
+                        } else {
+                            panic!("sync failed");
+                        }
+                    },
+                    QFrame::ShowAgain(id) => {
+                        if let Some(pic) = vactx.ref_pics.iter().find(|p| p.surface().id() == id) {
+                            let _ = fill_frame(vactx.ifmt, pic, &mut self.out_frm, self.needs_derive);
+                            let ts = NATimeInfo::new(Some(ts), None, None, self.tb_num, self.tb_den);
+                            return Some(NAFrame::new(ts, FrameType::P, false, self.info.clone(), self.out_frm.clone()).into_ref());
+                        }
+                    },
+                }
+            }
+        }
+        None
+    }
+    fn get_last_frames(&mut self) -> Option<NAFrameRef> {
+        self.get_frame()
+    }
+    fn flush(&mut self) {
+        if let Some(ref mut vactx) = self.vaapi {
+            for pic in vactx.ref_pics.drain(..) {
+                let surface = pic.take_surface().expect("take surface");
+                vactx.surfaces.push(surface);
+            }
+            for el in self.ref_ids.iter_mut() {
+                *el = VA_INVALID_ID;
+            }
+        }
+    }
+}
+
+impl NAOptionHandler for VaapiVP9Decoder {
+    fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] }
+    fn set_options(&mut self, _options: &[NAOption]) {}
+    fn query_option_value(&self, _name: &str) -> Option<NAValue> { None }
+}
+
+fn new_vp9_vadec() -> Box<dyn HWDecoderCore> {
+    Box::new(VaapiVP9Decoder::new())
+}
+
+pub fn new_vp9_hwdec() -> Box<dyn HWDecoder + Send> {
+    Box::new(HWWrapper::new(new_vp9_vadec))
+}
+
+#[cfg(test)]
+mod test {
+    use nihav_core::codecs::*;
+    use nihav_core::io::byteio::*;
+    use nihav_core::demuxers::{RegisteredDemuxers, create_demuxer};
+    use nihav_commonfmt::generic_register_all_demuxers;
+    use super::new_vp9_vadec;
+    use std::io::prelude::*;
+
+    fn decode_vp9(name: &str, dname: &str, dmx_reg: &RegisteredDemuxers, opfx: &str, limit: Option<usize>) -> DecoderResult<()> {
+        let dmx_f = dmx_reg.find_demuxer(dname).expect("demuxer exists");
+        let file = std::fs::File::open(name).expect("file exists");
+        let mut br = FileReader::new_read(file);
+        let mut dmx = create_demuxer(dmx_f, &mut br).expect("create demuxer");
+
+        let mut vstream_id = 0;
+        let mut dec = new_vp9_vadec();
+        for stream in dmx.get_streams() {
+            if stream.get_media_type() == StreamType::Video {
+                dec.init(stream.get_info()).expect("inited");
+                vstream_id = stream.get_id();
+                break;
+            }
+        }
+
+        let mut frameno = 0;
+        while let Ok(pkt) = dmx.get_frame() {
+            if pkt.get_stream().get_id() != vstream_id {
+                continue;
+            }
+            dec.decode(&pkt).expect("decoded");
+            let frm = dec.get_last_frames().expect("get frame");
+            let timestamp = frm.get_pts().unwrap_or(0);
+
+            let pic = frm.get_buffer().get_vbuf().expect("got picture");
+
+            let nname = format!("assets/test_out/{}{:06}_{}.pgm", opfx, timestamp, frameno);
+            frameno += 1;
+            let mut file = std::fs::File::create(&nname).expect("create file");
+            let (w, h) = pic.get_dimensions(0);
+            file.write_all(format!("P5\n{} {}\n255\n", w, h * 3 / 2).as_bytes()).expect("header written");
+            let data = pic.get_data();
+            for yline in data.chunks(pic.get_stride(0)).take(h) {
+                file.write_all(&yline[..w]).expect("Y line written");
+            }
+            for (uline, vline) in data[pic.get_offset(1)..].chunks(pic.get_stride(1))
+                    .zip(data[pic.get_offset(2)..].chunks(pic.get_stride(2))).take(h / 2) {
+                file.write_all(&uline[..w / 2]).expect("U line written");
+                file.write_all(&vline[..w / 2]).expect("V line written");
+            }
+            if Some(frameno) == limit { break; }
+        }
+        Ok(())
+    }
+
+
+    // samples if not specified are from VP9 test vectors
+
+    #[test]
+    fn test_vp9() {
+        let mut dmx_reg = RegisteredDemuxers::new();
+        generic_register_all_demuxers(&mut dmx_reg);
+
+        decode_vp9("assets/Duck/vp90-2-00-quantizer-00.webm", "matroska", &dmx_reg, "hw", Some(32)).unwrap();
+    }
+}