ImageComposite.cpp (14330B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "ImageComposite.h" 8 9 #include <inttypes.h> 10 11 #include "mozilla/ProfilerMarkers.h" 12 #include "gfxPlatform.h" 13 #include "nsPrintfCString.h" 14 15 namespace mozilla { 16 17 using namespace gfx; 18 19 namespace layers { 20 21 /* static */ const float ImageComposite::BIAS_TIME_MS = 1.0f; 22 23 ImageComposite::ImageComposite() = default; 24 25 ImageComposite::~ImageComposite() = default; 26 27 TimeStamp ImageComposite::GetBiasedTime(const TimeStamp& aInput) const { 28 switch (mBias) { 29 case ImageComposite::BIAS_NEGATIVE: 30 return aInput - TimeDuration::FromMilliseconds(BIAS_TIME_MS); 31 case ImageComposite::BIAS_POSITIVE: 32 return aInput + TimeDuration::FromMilliseconds(BIAS_TIME_MS); 33 default: 34 return aInput; 35 } 36 } 37 38 void ImageComposite::UpdateBias(size_t aImageIndex, bool aFrameChanged) { 39 MOZ_ASSERT(aImageIndex < ImagesCount()); 40 41 TimeStamp compositionTime = GetCompositionTime(); 42 TimeStamp compositedImageTime = mImages[aImageIndex].mTimeStamp; 43 TimeStamp nextImageTime = aImageIndex + 1 < ImagesCount() 44 ? mImages[aImageIndex + 1].mTimeStamp 45 : TimeStamp(); 46 47 if (profiler_thread_is_being_profiled_for_markers() && compositedImageTime && 48 nextImageTime) { 49 TimeDuration offsetCurrent = compositedImageTime - compositionTime; 50 TimeDuration offsetNext = nextImageTime - compositionTime; 51 nsPrintfCString str("current %.2lfms, next %.2lfms", 52 offsetCurrent.ToMilliseconds(), 53 offsetNext.ToMilliseconds()); 54 PROFILER_MARKER_TEXT("Video frame offsets", GRAPHICS, {}, str); 55 } 56 57 if (compositedImageTime.IsNull()) { 58 mBias = ImageComposite::BIAS_NONE; 59 return; 60 } 61 TimeDuration threshold = TimeDuration::FromMilliseconds(1.5); 62 if (compositionTime - compositedImageTime < threshold && 63 compositionTime - compositedImageTime > -threshold) { 64 // The chosen frame's time is very close to the composition time (probably 65 // just before the current composition time, but due to previously set 66 // negative bias, it could be just after the current composition time too). 67 // If the inter-frame time is almost exactly equal to (a multiple of) 68 // the inter-composition time, then we're in a dangerous situation because 69 // jitter might cause frames to fall one side or the other of the 70 // composition times, causing many frames to be skipped or duplicated. 71 // Try to prevent that by adding a negative bias to the frame times during 72 // the next composite; that should ensure the next frame's time is treated 73 // as falling just before a composite time. 74 mBias = ImageComposite::BIAS_NEGATIVE; 75 return; 76 } 77 if (!nextImageTime.IsNull() && nextImageTime - compositionTime < threshold && 78 nextImageTime - compositionTime > -threshold) { 79 // The next frame's time is very close to our composition time (probably 80 // just after the current composition time, but due to previously set 81 // positive bias, it could be just before the current composition time too). 82 // We're in a dangerous situation because jitter might cause frames to 83 // fall one side or the other of the composition times, causing many frames 84 // to be skipped or duplicated. 85 // Specifically, the next composite is at risk of picking the "next + 1" 86 // frame rather than the "next" frame, which would cause the "next" frame to 87 // be skipped. Try to prevent that by adding a positive bias to the frame 88 // times during the next composite; if the inter-frame time is almost 89 // exactly equal to the inter-composition time, that should ensure that the 90 // next + 1 frame falls just *after* the next composition time, and the next 91 // composite should then pick the next frame rather than the next + 1 frame. 92 mBias = ImageComposite::BIAS_POSITIVE; 93 return; 94 } 95 if (aFrameChanged) { 96 // The current and next video frames are a sufficient distance from the 97 // composition time and we can reliably pick the right frame without bias. 98 // Reset the bias. 99 // We only do this when the frame changed. Otherwise, when playing a 30fps 100 // video on a 60fps display, we'd keep resetting the bias during the "middle 101 // frames". 102 mBias = ImageComposite::BIAS_NONE; 103 } 104 } 105 106 int ImageComposite::ChooseImageIndex() { 107 // ChooseImageIndex is called for all images in the layer when it is visible. 108 // Change to this behaviour would break dropped frames counting calculation: 109 // We rely on this assumption to determine if during successive runs an 110 // image is returned that isn't the one following immediately the previous one 111 if (mImages.IsEmpty()) { 112 return -1; 113 } 114 115 TimeStamp compositionTime = GetCompositionTime(); 116 auto compositionOpportunityId = GetCompositionOpportunityId(); 117 if (compositionTime && 118 compositionOpportunityId != mLastChooseImageIndexComposition) { 119 // We are inside a composition, in the first call to ChooseImageIndex during 120 // this composition. 121 // Find the newest frame whose biased timestamp is at or before 122 // `compositionTime`. 123 uint32_t imageIndex = 0; 124 while (imageIndex + 1 < mImages.Length() && 125 mImages[imageIndex + 1].mTextureHost->IsValid() && 126 GetBiasedTime(mImages[imageIndex + 1].mTimeStamp) <= 127 compositionTime) { 128 ++imageIndex; 129 } 130 131 if (!mImages[imageIndex].mTextureHost->IsValid()) { 132 // Still not ready to be shown. 133 return -1; 134 } 135 136 bool wasVisibleAtPreviousComposition = 137 compositionOpportunityId == mLastChooseImageIndexComposition.Next(); 138 139 bool frameChanged = 140 UpdateCompositedFrame(imageIndex, wasVisibleAtPreviousComposition); 141 UpdateBias(imageIndex, frameChanged); 142 143 mLastChooseImageIndexComposition = compositionOpportunityId; 144 145 return imageIndex; 146 } 147 148 // We've been called before during this composition, or we're not in a 149 // composition. Just return the last image we picked (if it's one of the 150 // current images). 151 for (uint32_t i = 0; i < mImages.Length(); ++i) { 152 if (mImages[i].mFrameID == mLastFrameID && 153 mImages[i].mProducerID == mLastProducerID) { 154 return i; 155 } 156 } 157 158 return 0; 159 } 160 161 const ImageComposite::TimedImage* ImageComposite::ChooseImage() { 162 int index = ChooseImageIndex(); 163 return index >= 0 ? &mImages[index] : nullptr; 164 } 165 166 void ImageComposite::RemoveImagesWithTextureHost(TextureHost* aTexture) { 167 for (int32_t i = mImages.Length() - 1; i >= 0; --i) { 168 if (mImages[i].mTextureHost == aTexture) { 169 mImages.RemoveElementAt(i); 170 } 171 } 172 } 173 174 void ImageComposite::ClearImages() { mImages.Clear(); } 175 176 void ImageComposite::SetImages(nsTArray<TimedImage>&& aNewImages) { 177 if (!aNewImages.IsEmpty()) { 178 DetectTimeStampJitter(&aNewImages[0]); 179 180 // Frames older than the first frame in aNewImages that we haven't shown yet 181 // will never be shown. 182 CountSkippedFrames(&aNewImages[0]); 183 184 if (profiler_thread_is_being_profiled_for_markers()) { 185 int len = aNewImages.Length(); 186 const auto& first = aNewImages[0]; 187 const auto& last = aNewImages.LastElement(); 188 nsPrintfCString str("%d %s, frameID %" PRId32 " (prod %" PRId32 189 ") to frameID %" PRId32 " (prod %" PRId32 ")", 190 len, len == 1 ? "image" : "images", first.mFrameID, 191 first.mProducerID, last.mFrameID, last.mProducerID); 192 PROFILER_MARKER_TEXT("ImageComposite::SetImages", GRAPHICS, {}, str); 193 } 194 } 195 mImages = std::move(aNewImages); 196 } 197 198 // Returns whether the frame changed. 199 bool ImageComposite::UpdateCompositedFrame( 200 int aImageIndex, bool aWasVisibleAtPreviousComposition) { 201 MOZ_RELEASE_ASSERT(aImageIndex >= 0); 202 MOZ_RELEASE_ASSERT(aImageIndex < static_cast<int>(mImages.Length())); 203 const TimedImage& image = mImages[aImageIndex]; 204 205 auto compositionOpportunityId = GetCompositionOpportunityId(); 206 TimeStamp compositionTime = GetCompositionTime(); 207 MOZ_RELEASE_ASSERT(compositionTime, 208 "Should only be called during a composition"); 209 210 nsCString descr; 211 if (profiler_thread_is_being_profiled_for_markers()) { 212 nsCString relativeTimeString; 213 if (image.mTimeStamp) { 214 relativeTimeString.AppendPrintf( 215 " [relative timestamp %.1lfms]", 216 (image.mTimeStamp - compositionTime).ToMilliseconds()); 217 } 218 int remainingImages = mImages.Length() - 1 - aImageIndex; 219 static const char* kBiasStrings[] = {"NONE", "NEGATIVE", "POSITIVE"}; 220 descr.AppendPrintf( 221 "frameID %" PRId32 " (producerID %" PRId32 ") [composite %" PRIu64 222 "] [bias %s] [%d remaining %s]%s", 223 image.mFrameID, image.mProducerID, compositionOpportunityId.mId, 224 kBiasStrings[mBias], remainingImages, 225 remainingImages == 1 ? "image" : "images", relativeTimeString.get()); 226 if (mLastProducerID != image.mProducerID) { 227 descr.AppendPrintf(", previous producerID: %" PRId32, mLastProducerID); 228 } else if (mLastFrameID != image.mFrameID) { 229 descr.AppendPrintf(", previous frameID: %" PRId32, mLastFrameID); 230 } else { 231 descr.AppendLiteral(", no change"); 232 } 233 } 234 PROFILER_MARKER_TEXT("UpdateCompositedFrame", GRAPHICS, {}, descr); 235 236 if (mLastFrameID == image.mFrameID && mLastProducerID == image.mProducerID) { 237 // The frame didn't change. 238 return false; 239 } 240 241 CountSkippedFrames(&image); 242 243 int32_t dropped = mSkippedFramesSinceLastComposite; 244 mSkippedFramesSinceLastComposite = 0; 245 246 if (!aWasVisibleAtPreviousComposition) { 247 // This video was not part of the on-screen scene during the previous 248 // composition opportunity, for example it may have been scrolled off-screen 249 // or in a background tab, or compositing might have been paused. 250 // Ignore any skipped frames and don't count them as dropped. 251 dropped = 0; 252 } 253 254 if (dropped > 0) { 255 mDroppedFrames += dropped; 256 if (profiler_thread_is_being_profiled_for_markers()) { 257 const char* frameOrFrames = dropped == 1 ? "frame" : "frames"; 258 nsPrintfCString text("%" PRId32 " %s dropped: %" PRId32 " -> %" PRId32 259 " (producer %" PRId32 ")", 260 dropped, frameOrFrames, mLastFrameID, image.mFrameID, 261 mLastProducerID); 262 PROFILER_MARKER_TEXT("Video frames dropped", GRAPHICS, {}, text); 263 } 264 } 265 266 mLastFrameID = image.mFrameID; 267 mLastProducerID = image.mProducerID; 268 mLastFrameUpdateComposition = compositionOpportunityId; 269 270 return true; 271 } 272 273 void ImageComposite::OnFinishRendering(int aImageIndex, 274 const TimedImage* aImage, 275 base::ProcessId aProcessId, 276 const CompositableHandle& aHandle) { 277 if (mLastFrameUpdateComposition != GetCompositionOpportunityId()) { 278 // The frame did not change in this composition. 279 return; 280 } 281 282 if (aHandle) { 283 ImageCompositeNotificationInfo info; 284 info.mImageBridgeProcessId = aProcessId; 285 info.mNotification = ImageCompositeNotification( 286 aHandle, aImage->mTimeStamp, GetCompositionTime(), mLastFrameID, 287 mLastProducerID); 288 AppendImageCompositeNotification(info); 289 } 290 } 291 292 const ImageComposite::TimedImage* ImageComposite::GetImage( 293 size_t aIndex) const { 294 if (aIndex >= mImages.Length()) { 295 return nullptr; 296 } 297 return &mImages[aIndex]; 298 } 299 300 void ImageComposite::CountSkippedFrames(const TimedImage* aImage) { 301 if (aImage->mProducerID != mLastProducerID) { 302 // Switched producers. 303 return; 304 } 305 306 if (mImages.IsEmpty() || aImage->mFrameID <= mLastFrameID + 1) { 307 // No frames were skipped. 308 return; 309 } 310 311 uint32_t targetFrameRate = gfxPlatform::TargetFrameRate(); 312 if (targetFrameRate == 0) { 313 // Can't know whether we could have reasonably displayed all video frames. 314 return; 315 } 316 317 double targetFrameDurationMS = 1000.0 / targetFrameRate; 318 319 // Count how many images in mImages were skipped between mLastFrameID and 320 // aImage.mFrameID. Only count frames for which we can estimate a duration by 321 // looking at the next frame's timestamp, and only if the video frame rate is 322 // no faster than the target frame rate. 323 int32_t skipped = 0; 324 for (size_t i = 0; i + 1 < mImages.Length(); i++) { 325 const auto& img = mImages[i]; 326 if (img.mProducerID != aImage->mProducerID || 327 img.mFrameID <= mLastFrameID || img.mFrameID >= aImage->mFrameID) { 328 continue; 329 } 330 331 // We skipped img! Estimate img's time duration. 332 const auto& next = mImages[i + 1]; 333 if (next.mProducerID != aImage->mProducerID) { 334 continue; 335 } 336 337 MOZ_ASSERT(next.mFrameID > img.mFrameID); 338 TimeDuration duration = next.mTimeStamp - img.mTimeStamp; 339 if (floor(duration.ToMilliseconds()) >= floor(targetFrameDurationMS)) { 340 // Count the frame. 341 skipped++; 342 } 343 } 344 345 mSkippedFramesSinceLastComposite += skipped; 346 } 347 348 void ImageComposite::DetectTimeStampJitter(const TimedImage* aNewImage) { 349 if (!profiler_thread_is_being_profiled_for_markers() || 350 aNewImage->mTimeStamp.IsNull()) { 351 return; 352 } 353 354 // Find aNewImage in mImages and compute its timestamp delta, if found. 355 // Ideally, a given video frame should never change its timestamp (jitter 356 // should be zero). However, we re-adjust video frame timestamps based on the 357 // audio clock. If the audio clock drifts compared to the system clock, or if 358 // there are bugs or inaccuracies in the computation of these timestamps, 359 // jitter will be non-zero. 360 Maybe<TimeDuration> jitter; 361 for (const auto& img : mImages) { 362 if (img.mProducerID == aNewImage->mProducerID && 363 img.mFrameID == aNewImage->mFrameID) { 364 if (!img.mTimeStamp.IsNull()) { 365 jitter = Some(aNewImage->mTimeStamp - img.mTimeStamp); 366 } 367 break; 368 } 369 } 370 if (jitter) { 371 nsPrintfCString text("%.2lfms", jitter->ToMilliseconds()); 372 PROFILER_MARKER_TEXT("VideoFrameTimeStampJitter", GRAPHICS, {}, text); 373 } 374 } 375 376 } // namespace layers 377 } // namespace mozilla