diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc index 0660e23b53e46..f05ff628cfe32 100644 --- a/media/gpu/h265_decoder_unittest.cc +++ b/media/gpu/h265_decoder_unittest.cc @@ -751,4 +751,142 @@ TEST_F(H265DecoderTest, DependentSliceLongTermRefPics) { EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, res2); } +// SPS replacement bypasses PPS cross-validation: parsing a PPS validates +// num_tile_columns_minus1 / num_tile_rows_minus1 against the SPS present at +// PPS-parse time, but a later SPS NALU with the same sps_id silently overwrites +// the map entry without re-validating dependent PPSes. The new SPS is then +// shipped together with the stale PPS to the platform accelerator +// (VAAPI / D3D11 / V4L2), violating the H.265 spec invariant +// num_tile_columns_minus1 < PicWidthInCtbsY that drivers rely on. +// +// On VAAPI this causes column_width_minus1[i] to be computed as (uint16_t)-1 = +// 0xFFFF for a picture only 5 CTBs wide; on V4L2 the inconsistent +// v4l2_ctrl_hevc_sps + v4l2_ctrl_hevc_pps pair is sent directly to the kernel. +// +// This test crafts SPS0 (CTB=16, 19x21 CTBs) -> PPS0 (19x21 tiles) -> IDR -> +// SPS0' (same coded size but CTB=64, 5x6 CTBs) -> IDR and verifies that the +// inconsistent SPS0' + PPS0 pair would today be passed through unchanged to +// SubmitFrameMetadata. With the fix, the second IDR must be rejected. +TEST_F(H265DecoderTest, SpsReplacementInvalidatesStalePps) { + // Common SPS template: 304x336, Main profile, 8-bit, 4:2:0, level 3.1. + // 304 and 336 are multiples of both 16 (SPS0 min_cb) and 8 (SPS0' min_cb), + // and yield identical max_dpb_size, so the H265Decoder config-change gate + // (which only compares coded size / profile / bit-depth / chroma / dpb size) + // is NOT tripped by SPS0'. + auto make_sps = [](int log2_min_cb_minus3, int log2_diff_cb) { + H265SPS sps = {}; + sps.sps_temporal_id_nesting_flag = true; + sps.profile_tier_level.general_profile_idc = 1; // Main + sps.profile_tier_level.general_level_idc = 93; // Level 3.1 + sps.chroma_format_idc = 1; // 4:2:0 + sps.pic_width_in_luma_samples = 304; + sps.pic_height_in_luma_samples = 336; + sps.log2_max_pic_order_cnt_lsb_minus4 = 4; + sps.sps_max_dec_pic_buffering_minus1[0] = 1; + sps.log2_min_luma_coding_block_size_minus3 = log2_min_cb_minus3; + sps.log2_diff_max_min_luma_coding_block_size = log2_diff_cb; + return sps; + }; + // SPS0: min_cb=16, CTB=16 => PicWidthInCtbsY=19, PicHeightInCtbsY=21 + H265SPS sps0 = make_sps(/*log2_min_cb_minus3=*/1, /*log2_diff_cb=*/0); + + // PPS0: 19 x 21 tiles -- maximal under both the per-SPS spec bound + // (num_tile_columns_minus1 <= PicWidthInCtbsY-1 = 18) and the + // SPS-independent kMaxNumTileColumnWidth/kMaxNumTileRowHeight caps. + H265PPS pps0 = {}; + pps0.tiles_enabled_flag = true; + pps0.num_tile_columns_minus1 = 18; + pps0.num_tile_rows_minus1 = 20; + pps0.uniform_spacing_flag = true; + pps0.loop_filter_across_tiles_enabled_flag = true; + + // Buffer 1: SPS0 + PPS0 + IDR. + H26xAnnexBBitstreamBuilder b1(/*insert_emulation_prevention_bytes=*/true); + BuildPackedH265SPS(b1, sps0); + BuildPackedH265PPS(b1, pps0); + b1.BeginNALU(H265NALU::IDR_W_RADL); + b1.AppendBool(true); // first_slice_segment_in_pic_flag + b1.AppendBool(false); // no_output_of_prior_pics_flag + b1.AppendUE(0); // slice_pic_parameter_set_id + b1.AppendUE(2); // slice_type = I + b1.AppendSE(0); // slice_qp_delta + b1.AppendUE(0); // num_entry_point_offsets (tiles_enabled) + b1.FinishNALU(); + std::vector buf1_bytes(b1.data().begin(), b1.data().end()); + + // ---- Buffer 2: SPS0' + IDR referencing PPS0 ---- + // SPS0': identical width/height/profile/bit-depth/chroma/level, but + // min_cb = 8 and CTB = 64 => PicWidthInCtbsY = ceil(304/64) = 5, + // PicHeightInCtbsY = ceil(336/64) = 6. + H265SPS sps0p = make_sps(/*log2_min_cb_minus3=*/0, /*log2_diff_cb=*/3); + + H26xAnnexBBitstreamBuilder b2(/*insert_emulation_prevention_bytes=*/true); + BuildPackedH265SPS(b2, sps0p); + b2.BeginNALU(H265NALU::IDR_W_RADL); + b2.AppendBool(true); // first_slice_segment_in_pic_flag + b2.AppendBool(false); // no_output_of_prior_pics_flag + b2.AppendUE(0); // slice_pic_parameter_set_id -> stale PPS0 + b2.AppendUE(2); // slice_type = I + b2.AppendSE(0); // slice_qp_delta + b2.AppendUE(0); // num_entry_point_offsets (tiles_enabled) + b2.FinishNALU(); + std::vector buf2_bytes(b2.data().begin(), b2.data().end()); + + // Decode buffer 1. + auto buffer1 = DecoderBuffer::CopyFrom(buf1_bytes); + decoder_->SetStream(0, buffer1); + EXPECT_EQ(AcceleratedVideoDecoder::kConfigChange, decoder_->Decode()); + EXPECT_EQ(gfx::Size(304, 336), decoder_->GetPicSize()); + EXPECT_CALL(*accelerator_, SubmitFrameMetadata(_, _, _, _, _, _, _, _)) + .WillOnce(Return(H265Decoder::H265Accelerator::Status::kOk)); + EXPECT_EQ(AcceleratedVideoDecoder::kRanOutOfStreamData, decoder_->Decode()); + EXPECT_TRUE(Mock::VerifyAndClearExpectations(&*accelerator_)); + ResetExpectations(); + + // Decode buffer 2. Prior to the fix, ProcessPPS() sees no config change + // (same coded size / dpb / profile / bit-depth / chroma), so StartNewFrame() + // ships SPS0' + stale PPS0 straight to the accelerator. Capture the SPS+PPS + // pair handed to SubmitFrameMetadata and verify that the spec invariant + // num_tile_columns_minus1 <= PicWidthInCtbsY - 1 holds. + bool submit_called = false; + int captured_pic_width_in_ctbs_y = 0; + int captured_pic_height_in_ctbs_y = 0; + int captured_num_tile_columns_minus1 = 0; + int captured_num_tile_rows_minus1 = 0; + EXPECT_CALL(*accelerator_, SubmitFrameMetadata(_, _, _, _, _, _, _, _)) + .WillRepeatedly( + [&](const H265SPS* sps, const H265PPS* pps, const H265SliceHeader*, + const H265Picture::Vector&, const H265Picture::Vector&, + const H265Picture::Vector&, const H265Picture::Vector&, + scoped_refptr) { + submit_called = true; + captured_pic_width_in_ctbs_y = sps->pic_width_in_ctbs_y; + captured_pic_height_in_ctbs_y = sps->pic_height_in_ctbs_y; + captured_num_tile_columns_minus1 = pps->num_tile_columns_minus1; + captured_num_tile_rows_minus1 = pps->num_tile_rows_minus1; + return H265Decoder::H265Accelerator::Status::kOk; + }); + + auto buffer2 = DecoderBuffer::CopyFrom(buf2_bytes); + decoder_->SetStream(1, buffer2); + std::ignore = decoder_->Decode(); + + // What the VAAPI delegate would compute for column_width_minus1[0] from this + // pair (h265_vaapi_video_decoder_delegate.cc, uniform_spacing branch): + // ((1*5)/19) - ((0*5)/19) - 1 = -1, stored into uint16_t as 0xFFFF and sent + // to vaRenderPicture. With the fix, the slice referencing the stale PPS must + // be rejected before reaching the accelerator. + if (submit_called) { + EXPECT_LE(captured_num_tile_columns_minus1, + captured_pic_width_in_ctbs_y - 1) + << "spec-violating SPS+PPS pair reached accelerator: " + << "num_tile_columns_minus1=" << captured_num_tile_columns_minus1 + << " PicWidthInCtbsY=" << captured_pic_width_in_ctbs_y; + EXPECT_LE(captured_num_tile_rows_minus1, captured_pic_height_in_ctbs_y - 1) + << "spec-violating SPS+PPS pair reached accelerator: " + << "num_tile_rows_minus1=" << captured_num_tile_rows_minus1 + << " PicHeightInCtbsY=" << captured_pic_height_in_ctbs_y; + } +} + } // namespace media