diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc index 0660e23b53e46..0560ea22dc29b 100644 --- a/media/gpu/h265_decoder_unittest.cc +++ b/media/gpu/h265_decoder_unittest.cc @@ -545,6 +545,324 @@ TEST_F(H265DecoderTest, SetStreamRetry) { EXPECT_TRUE(decoder_->Flush()); } +// ============================================================================ +// POC: Dropped RASL picture (never decoded by HW) used as inter-prediction +// reference for a subsequent TRAIL picture. +// +// The VA-API and V4L2 H.265 accelerator delegates set drop_frame_=true when a +// RASL picture follows an IRAP with NoRaslOutputFlag=1 and return Status::kOk +// from SubmitFrameMetadata/SubmitSlice/SubmitDecode without ever issuing a +// decode to the hardware. However, by the time the drop decision is made, +// H265Decoder::PerformDpbOperations() has ALREADY stored the picture (with a +// real, never-written GPU surface) in the DPB. A later TRAIL picture whose +// short-term RPS references the RASL's POC will have the undecoded surface +// passed to the hardware as RefPicList[0][0]. +// ============================================================================ + +// An accelerator that mimics the production VA-API/V4L2 delegate RASL-drop +// behaviour and records which pictures were "decoded" vs "dropped". When a +// non-dropped frame is submitted whose reference list contains a picture that +// was previously dropped (i.e. never decoded), it records that as the bug +// being triggered. +class RaslDropAccelerator : public H265Decoder::H265Accelerator { + public: + RaslDropAccelerator() = default; + + scoped_refptr CreateH265Picture() override { + return base::MakeRefCounted(); + } + + Status SubmitFrameMetadata( + const H265SPS* sps, + const H265PPS* pps, + const H265SliceHeader* slice_hdr, + const H265Picture::Vector& ref_pic_list, + const H265Picture::Vector& ref_pic_set_lt_curr, + const H265Picture::Vector& ref_pic_set_st_curr_after, + const H265Picture::Vector& ref_pic_set_st_curr_before, + scoped_refptr pic) override { + // Exactly mirrors h265_vaapi_video_decoder_delegate.cc:83-91 and + // v4l2_video_decoder_delegate_h265.cc:200-208. + drop_frame_ = false; + if (pic->no_rasl_output_flag_ && + (slice_hdr->nal_unit_type == H265NALU::RASL_N || + slice_hdr->nal_unit_type == H265NALU::RASL_R)) { + // "Drop RASL frame": no VA buffers are submitted, the surface is never + // written by the hardware decoder. + drop_frame_ = true; + dropped_pics_.push_back(pic); + return Status::kOk; + } + + // For a real (non-dropped) frame, the production delegate writes every + // picture in |ref_pic_list| / |ref_pic_set_st_curr_before| into + // VAPictureParameterBufferHEVC::ReferenceFrames[] / + // v4l2_ctrl_hevc_decode_params::dpb[].timestamp. If any of those pictures + // was previously dropped, the hardware will inter-predict from a + // never-decoded dmabuf. + for (const auto& ref : ref_pic_list) { + if (ref && WasDropped(ref.get())) { + undecoded_ref_used_ = true; + undecoded_ref_poc_ = ref->pic_order_cnt_val_; + victim_poc_ = pic->pic_order_cnt_val_; + } + } + for (const auto& ref : ref_pic_set_st_curr_before) { + if (ref && WasDropped(ref.get())) { + undecoded_ref_in_st_curr_before_ = true; + } + } + return Status::kOk; + } + + Status SubmitSlice(const H265SPS* sps, + const H265PPS* pps, + const H265SliceHeader* slice_hdr, + const H265Picture::Vector& ref_pic_list0, + const H265Picture::Vector& ref_pic_list1, + const H265Picture::Vector& ref_pic_set_lt_curr, + const H265Picture::Vector& ref_pic_set_st_curr_after, + const H265Picture::Vector& ref_pic_set_st_curr_before, + scoped_refptr pic, + const uint8_t* data, + size_t size, + const std::vector& subsamples) override { + if (drop_frame_) { + // Mirrors h265_vaapi_video_decoder_delegate.cc:334-336. + return Status::kOk; + } + // The production delegate writes ref_pic_list0[i] into + // VASliceParameterBufferHEVC::RefPicList[0][i]. The hardware uses this as + // the inter-prediction source for the slice's P/B blocks. + for (const auto& ref : ref_pic_list0) { + if (ref && WasDropped(ref.get())) { + undecoded_ref_in_l0_ = true; + } + } + return Status::kOk; + } + + Status SubmitDecode(scoped_refptr pic) override { + if (drop_frame_) { + // Mirrors h265_vaapi_video_decoder_delegate.cc:500-502: + // ExecuteAndDestroyPendingBuffers() is NEVER called, so |pic|'s + // VASurface is never written. + return Status::kOk; + } + decoded_pics_.push_back(pic); + return Status::kOk; + } + + bool OutputPicture(scoped_refptr pic) override { + output_pocs_.push_back(pic->pic_order_cnt_val_); + return true; + } + + void Reset() override {} + bool IsChromaSamplingSupported(VideoChromaSampling format) override { + return format == VideoChromaSampling::k420; + } + + bool WasDropped(const H265Picture* p) const { + for (const auto& d : dropped_pics_) { + if (d.get() == p) { + return true; + } + } + return false; + } + + bool drop_frame_ = false; + std::vector> dropped_pics_; + std::vector> decoded_pics_; + std::vector output_pocs_; + + // Bug indicators. + bool undecoded_ref_used_ = false; + bool undecoded_ref_in_st_curr_before_ = false; + bool undecoded_ref_in_l0_ = false; + int undecoded_ref_poc_ = -1; + int victim_poc_ = -1; +}; + +namespace { + +// Appends an inline short-term reference picture set with |num_neg| negative +// pictures (each with the given |delta_poc_s0_minus1| and used_by_curr_pic=1) +// and zero positive pictures, assuming sps.num_short_term_ref_pic_sets==0. +void AppendInlineStRps(H26xAnnexBBitstreamBuilder& b, + int num_neg, + int delta_poc_s0_minus1) { + // st_rps_idx == 0, so inter_ref_pic_set_prediction_flag is NOT present. + b.AppendUE(num_neg); // num_negative_pics + b.AppendUE(0); // num_positive_pics + for (int i = 0; i < num_neg; ++i) { + b.AppendUE(delta_poc_s0_minus1); // delta_poc_s0_minus1[i] + b.AppendBool(true); // used_by_curr_pic_s0_flag[i] + } +} + +// Builds an Annex-B bitstream consisting of: +// VPS + SPS + PPS +// BLA_W_LP (I-slice) POC = 100 +// RASL_R (P-slice) POC = 98, RPS = {delta_s0=-4, used=1} +// TRAIL_R (P-slice) POC = 101, RPS = {delta_s0=-3, used=1} -> ref POC 98 +std::vector BuildBlaRaslTrailBitstream() { + H26xAnnexBBitstreamBuilder b(/*insert_emulation_prevention_bytes=*/true); + + // ---- VPS ---- + H265VPS vps = {}; + vps.vps_max_layers_minus1 = 0; + vps.vps_max_sub_layers_minus1 = 0; + vps.vps_temporal_id_nesting_flag = true; + vps.profile_tier_level.general_profile_idc = 1; + vps.profile_tier_level.general_level_idc = 60; + vps.vps_max_dec_pic_buffering_minus1[0] = 3; + vps.vps_max_num_reorder_pics[0] = 2; + BuildPackedH265VPS(b, vps); + + // ---- SPS ---- + H265SPS sps = {}; + sps.sps_video_parameter_set_id = 0; + sps.sps_max_sub_layers_minus1 = 0; + sps.sps_temporal_id_nesting_flag = true; + sps.profile_tier_level.general_profile_idc = 1; + sps.profile_tier_level.general_level_idc = 60; + sps.sps_seq_parameter_set_id = 0; + sps.chroma_format_idc = 1; + sps.pic_width_in_luma_samples = 64; + sps.pic_height_in_luma_samples = 64; + sps.log2_max_pic_order_cnt_lsb_minus4 = 4; // 8-bit POC LSB. + sps.sps_max_dec_pic_buffering_minus1[0] = 3; + sps.sps_max_num_reorder_pics[0] = 2; + sps.log2_min_luma_coding_block_size_minus3 = 0; + sps.log2_diff_max_min_luma_coding_block_size = 3; // 64x64 CTB -> single CTU. + sps.log2_min_luma_transform_block_size_minus2 = 0; + sps.log2_diff_max_min_luma_transform_block_size = 3; + sps.max_transform_hierarchy_depth_inter = 0; + sps.max_transform_hierarchy_depth_intra = 0; + sps.num_short_term_ref_pic_sets = 0; + sps.long_term_ref_pics_present_flag = false; + sps.sps_temporal_mvp_enabled_flag = false; + sps.sample_adaptive_offset_enabled_flag = false; + BuildPackedH265SPS(b, sps); + + // ---- PPS ---- + H265PPS pps = {}; + pps.pps_pic_parameter_set_id = 0; + pps.pps_seq_parameter_set_id = 0; + pps.num_ref_idx_l0_default_active_minus1 = 0; + pps.num_ref_idx_l1_default_active_minus1 = 0; + pps.init_qp_minus26 = 0; + BuildPackedH265PPS(b, pps); + + const int kPocBits = 8; + + // ---- BLA_W_LP, I-slice, POC LSB = 100 ---- + b.BeginNALU(H265NALU::BLA_W_LP); + b.AppendBool(true); // first_slice_segment_in_pic_flag + b.AppendBool(false); // no_output_of_prior_pics_flag (IRAP only) + b.AppendUE(0); // slice_pic_parameter_set_id + b.AppendUE(2); // slice_type = I (2) + b.AppendBits(kPocBits, 100); // slice_pic_order_cnt_lsb + b.AppendBool(false); // short_term_ref_pic_set_sps_flag + AppendInlineStRps(b, 0, 0); // empty RPS + b.AppendSE(0); // slice_qp_delta + b.AppendBits(8, 0xAA); // dummy slice payload byte + b.FinishNALU(); + + // ---- RASL_R, P-slice, POC LSB = 98, RPS={delta_s0=-4, used=1} ---- + b.BeginNALU(H265NALU::RASL_R); + b.AppendBool(true); // first_slice_segment_in_pic_flag + b.AppendUE(0); // slice_pic_parameter_set_id + b.AppendUE(1); // slice_type = P (1) + b.AppendBits(kPocBits, 98); // slice_pic_order_cnt_lsb + b.AppendBool(false); // short_term_ref_pic_set_sps_flag + AppendInlineStRps(b, 1, 3); // 1 neg pic, delta_poc_s0_minus1=3 -> delta=-4 + b.AppendBool(false); // num_ref_idx_active_override_flag + b.AppendUE(0); // five_minus_max_num_merge_cand + b.AppendSE(0); // slice_qp_delta + b.AppendBits(8, 0xAA); // dummy slice payload byte + b.FinishNALU(); + + // ---- TRAIL_R, P-slice, POC LSB = 101, RPS={delta_s0=-3, used=1} ---- + b.BeginNALU(H265NALU::TRAIL_R); + b.AppendBool(true); // first_slice_segment_in_pic_flag + b.AppendUE(0); // slice_pic_parameter_set_id + b.AppendUE(1); // slice_type = P (1) + b.AppendBits(kPocBits, 101); // slice_pic_order_cnt_lsb + b.AppendBool(false); // short_term_ref_pic_set_sps_flag + AppendInlineStRps(b, 1, 2); // 1 neg pic, delta_poc_s0_minus1=2 -> delta=-3 + b.AppendBool(false); // num_ref_idx_active_override_flag + b.AppendUE(0); // five_minus_max_num_merge_cand + b.AppendSE(0); // slice_qp_delta + b.AppendBits(8, 0xAA); // dummy slice payload byte + b.FinishNALU(); + + return std::vector(b.data().begin(), b.data().end()); +} + +} // namespace + +TEST(H265DecoderRaslDropTest, DroppedRaslSurfaceUsedAsReference) { + auto accel = std::make_unique(); + RaslDropAccelerator* a = accel.get(); + H265Decoder decoder(std::move(accel), VIDEO_CODEC_PROFILE_UNKNOWN); + + std::vector bitstream = BuildBlaRaslTrailBitstream(); + auto buffer = DecoderBuffer::CopyFrom(bitstream); + decoder.SetStream(0, buffer); + + ASSERT_EQ(AcceleratedVideoDecoder::kConfigChange, decoder.Decode()); + ASSERT_EQ(AcceleratedVideoDecoder::kRanOutOfStreamData, decoder.Decode()); + ASSERT_TRUE(decoder.Flush()); + + // Sanity: the RASL was processed by H265Decoder (a picture was created and + // stored in the DPB) but the accelerator dropped it -- it never reached + // SubmitDecode's "real" decode path. + ASSERT_EQ(1u, a->dropped_pics_.size()) + << "RASL_R should have been dropped by the accelerator"; + EXPECT_EQ(98, a->dropped_pics_[0]->pic_order_cnt_val_); + + // BLA (POC 100) and TRAIL (POC 101) were actually decoded. + ASSERT_EQ(2u, a->decoded_pics_.size()); + EXPECT_EQ(100, a->decoded_pics_[0]->pic_order_cnt_val_); + EXPECT_EQ(101, a->decoded_pics_[1]->pic_order_cnt_val_); + + // === The bug === + // The TRAIL_R picture's reference set RefPicSetStCurrBefore[0] and + // RefPicList0[0] is the dropped RASL_R picture, whose backing GPU surface + // was never written by the hardware. On VA-API this surface ID is written + // into VAPictureParameterBufferHEVC::ReferenceFrames[0].picture_id and + // VASliceParameterBufferHEVC::RefPicList[0][0]; on V4L2 its reference + // timestamp is written into v4l2_ctrl_hevc_decode_params::dpb[0].timestamp. + // The hardware then inter-predicts the TRAIL frame from uninitialised + // dmabuf memory. + EXPECT_FALSE(a->undecoded_ref_used_) + << "BUG: TRAIL_R (POC " << a->victim_poc_ + << ") was given dropped/never-decoded RASL_R (POC " + << a->undecoded_ref_poc_ + << ") as a reference picture in SubmitFrameMetadata::ref_pic_list -- " + "the hardware will inter-predict from an uninitialised GPU surface " + "and the result is delivered to the renderer via OutputPicture"; + EXPECT_FALSE(a->undecoded_ref_in_st_curr_before_) + << "BUG: dropped RASL appears in RefPicSetStCurrBefore for TRAIL frame"; + EXPECT_FALSE(a->undecoded_ref_in_l0_) + << "BUG: dropped RASL appears in RefPicList0 for TRAIL frame " + "(VASliceParameterBufferHEVC::RefPicList[0][0] -> undecoded surface)"; + + // The TRAIL frame is delivered to the renderer. + bool trail_output = false; + for (int poc : a->output_pocs_) { + if (poc == 101) { + trail_output = true; + } + } + EXPECT_TRUE(trail_output) + << "TRAIL_R (POC 101) -- decoded from uninitialised reference -- " + "is OutputPicture()d and reaches the renderer/compositor"; +} + TEST_F(H265DecoderTest, DecodeMultiFrameInput) { SetInputFrameFiles({"bear.hevc"}); EXPECT_EQ(AcceleratedVideoDecoder::kConfigChange, Decode());