Merge "AnimatedImageDrawable: Eliminate unnecessary calls to redraw" into pi-dev

This commit is contained in:
TreeHugger Robot
2018-05-17 19:54:50 +00:00
committed by Android (Google) Code Review
8 changed files with 105 additions and 78 deletions

View File

@@ -239,11 +239,6 @@ static jlong AnimatedImageDrawable_nNativeByteSize(JNIEnv* env, jobject /*clazz*
return drawable->byteSize(); return drawable->byteSize();
} }
static void AnimatedImageDrawable_nMarkInvisible(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) {
auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr);
drawable->markInvisible();
}
static void AnimatedImageDrawable_nSetMirrored(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, static void AnimatedImageDrawable_nSetMirrored(JNIEnv* env, jobject /*clazz*/, jlong nativePtr,
jboolean mirrored) { jboolean mirrored) {
auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr); auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr);
@@ -264,7 +259,6 @@ static const JNINativeMethod gAnimatedImageDrawableMethods[] = {
{ "nSetRepeatCount", "(JI)V", (void*) AnimatedImageDrawable_nSetRepeatCount }, { "nSetRepeatCount", "(JI)V", (void*) AnimatedImageDrawable_nSetRepeatCount },
{ "nSetOnAnimationEndListener", "(JLandroid/graphics/drawable/AnimatedImageDrawable;)V", (void*) AnimatedImageDrawable_nSetOnAnimationEndListener }, { "nSetOnAnimationEndListener", "(JLandroid/graphics/drawable/AnimatedImageDrawable;)V", (void*) AnimatedImageDrawable_nSetOnAnimationEndListener },
{ "nNativeByteSize", "(J)J", (void*) AnimatedImageDrawable_nNativeByteSize }, { "nNativeByteSize", "(J)J", (void*) AnimatedImageDrawable_nNativeByteSize },
{ "nMarkInvisible", "(J)V", (void*) AnimatedImageDrawable_nMarkInvisible },
{ "nSetMirrored", "(JZ)V", (void*) AnimatedImageDrawable_nSetMirrored }, { "nSetMirrored", "(JZ)V", (void*) AnimatedImageDrawable_nSetMirrored },
}; };

View File

@@ -31,6 +31,7 @@ import android.graphics.PixelFormat;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.TypedValue; import android.util.TypedValue;
@@ -348,7 +349,7 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 {
if (mRunnable == null) { if (mRunnable == null) {
mRunnable = this::invalidateSelf; mRunnable = this::invalidateSelf;
} }
scheduleSelf(mRunnable, nextUpdate); scheduleSelf(mRunnable, nextUpdate + SystemClock.uptimeMillis());
} else if (nextUpdate == FINISHED) { } else if (nextUpdate == FINISHED) {
// This means the animation was drawn in software mode and ended. // This means the animation was drawn in software mode and ended.
postOnAnimationEnd(); postOnAnimationEnd();
@@ -430,23 +431,6 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 {
return mState.mAutoMirrored; return mState.mAutoMirrored;
} }
@Override
public boolean setVisible(boolean visible, boolean restart) {
if (!super.setVisible(visible, restart)) {
return false;
}
if (mState.mNativePtr == 0) {
throw new IllegalStateException("called setVisible on empty AnimatedImageDrawable");
}
if (!visible) {
nMarkInvisible(mState.mNativePtr);
}
return true;
}
// Animatable overrides // Animatable overrides
/** /**
* Return whether the animation is currently running. * Return whether the animation is currently running.
@@ -616,7 +600,5 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 {
@FastNative @FastNative
private static native long nNativeByteSize(long nativePtr); private static native long nNativeByteSize(long nativePtr);
@FastNative @FastNative
private static native void nMarkInvisible(long nativePtr);
@FastNative
private static native void nSetMirrored(long nativePtr, boolean mirror); private static native void nSetMirrored(long nativePtr, boolean mirror);
} }

View File

@@ -108,6 +108,12 @@ public:
// *OR* will post itself for the next vsync automatically, use this // *OR* will post itself for the next vsync automatically, use this
// only to avoid calling draw() // only to avoid calling draw()
bool canDrawThisFrame = true; bool canDrawThisFrame = true;
// Sentinel for animatedImageDelay meaning there is no need to post such
// a message.
static constexpr nsecs_t kNoAnimatedImageDelay = -1;
// This is used to post a message to redraw when it is time to draw the
// next frame of an AnimatedImageDrawable.
nsecs_t animatedImageDelay = kNoAnimatedImageDelay;
} out; } out;
// This flag helps to disable projection for receiver nodes that do not have any backward // This flag helps to disable projection for receiver nodes that do not have any backward

View File

@@ -22,13 +22,12 @@
#include <SkPicture.h> #include <SkPicture.h>
#include <SkRefCnt.h> #include <SkRefCnt.h>
#include <SkTLazy.h> #include <SkTLazy.h>
#include <SkTime.h>
namespace android { namespace android {
AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed) AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed)
: mSkAnimatedImage(std::move(animatedImage)), mBytesUsed(bytesUsed) { : mSkAnimatedImage(std::move(animatedImage)), mBytesUsed(bytesUsed) {
mTimeToShowNextSnapshot = mSkAnimatedImage->currentFrameDuration(); mTimeToShowNextSnapshot = ms2ns(mSkAnimatedImage->currentFrameDuration());
} }
void AnimatedImageDrawable::syncProperties() { void AnimatedImageDrawable::syncProperties() {
@@ -62,28 +61,42 @@ bool AnimatedImageDrawable::nextSnapshotReady() const {
} }
// Only called on the RenderThread while UI thread is locked. // Only called on the RenderThread while UI thread is locked.
bool AnimatedImageDrawable::isDirty() { bool AnimatedImageDrawable::isDirty(nsecs_t* outDelay) {
const double currentTime = SkTime::GetMSecs(); *outDelay = 0;
const double lastWallTime = mLastWallTime; const nsecs_t currentTime = systemTime(CLOCK_MONOTONIC);
const nsecs_t lastWallTime = mLastWallTime;
mLastWallTime = currentTime; mLastWallTime = currentTime;
if (!mRunning) { if (!mRunning) {
mDidDraw = false;
return false; return false;
} }
std::unique_lock lock{mSwapLock}; std::unique_lock lock{mSwapLock};
if (mDidDraw) {
mCurrentTime += currentTime - lastWallTime; mCurrentTime += currentTime - lastWallTime;
mDidDraw = false;
}
if (!mNextSnapshot.valid()) { if (!mNextSnapshot.valid()) {
// Need to trigger onDraw in order to start decoding the next frame. // Need to trigger onDraw in order to start decoding the next frame.
*outDelay = mTimeToShowNextSnapshot - mCurrentTime;
return true; return true;
} }
return nextSnapshotReady() && mCurrentTime >= mTimeToShowNextSnapshot; if (mTimeToShowNextSnapshot > mCurrentTime) {
*outDelay = mTimeToShowNextSnapshot - mCurrentTime;
} else if (nextSnapshotReady()) {
// We have not yet updated mTimeToShowNextSnapshot. Read frame duration
// directly from mSkAnimatedImage.
lock.unlock();
std::unique_lock imageLock{mImageLock};
*outDelay = ms2ns(mSkAnimatedImage->currentFrameDuration());
return true;
} else {
// The next snapshot has not yet been decoded, but we've already passed
// time to draw it. There's not a good way to know when decoding will
// finish, so request an update immediately.
*outDelay = 0;
}
return false;
} }
// Only called on the AnimatedImageThread. // Only called on the AnimatedImageThread.
@@ -91,7 +104,7 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::decodeNextFrame() {
Snapshot snap; Snapshot snap;
{ {
std::unique_lock lock{mImageLock}; std::unique_lock lock{mImageLock};
snap.mDuration = mSkAnimatedImage->decodeNextFrame(); snap.mDurationMS = mSkAnimatedImage->decodeNextFrame();
snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot()); snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot());
} }
@@ -105,7 +118,7 @@ AnimatedImageDrawable::Snapshot AnimatedImageDrawable::reset() {
std::unique_lock lock{mImageLock}; std::unique_lock lock{mImageLock};
mSkAnimatedImage->reset(); mSkAnimatedImage->reset();
snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot()); snap.mPic.reset(mSkAnimatedImage->newPictureSnapshot());
snap.mDuration = mSkAnimatedImage->currentFrameDuration(); snap.mDurationMS = mSkAnimatedImage->currentFrameDuration();
} }
return snap; return snap;
@@ -127,8 +140,6 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) {
canvas->scale(-1, 1); canvas->scale(-1, 1);
} }
mDidDraw = true;
const bool starting = mStarting; const bool starting = mStarting;
mStarting = false; mStarting = false;
@@ -157,12 +168,12 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) {
std::unique_lock lock{mSwapLock}; std::unique_lock lock{mSwapLock};
if (mCurrentTime >= mTimeToShowNextSnapshot) { if (mCurrentTime >= mTimeToShowNextSnapshot) {
mSnapshot = mNextSnapshot.get(); mSnapshot = mNextSnapshot.get();
const double timeToShowCurrentSnap = mTimeToShowNextSnapshot; const nsecs_t timeToShowCurrentSnap = mTimeToShowNextSnapshot;
if (mSnapshot.mDuration == SkAnimatedImage::kFinished) { if (mSnapshot.mDurationMS == SkAnimatedImage::kFinished) {
finalFrame = true; finalFrame = true;
mRunning = false; mRunning = false;
} else { } else {
mTimeToShowNextSnapshot += mSnapshot.mDuration; mTimeToShowNextSnapshot += ms2ns(mSnapshot.mDurationMS);
if (mCurrentTime >= mTimeToShowNextSnapshot) { if (mCurrentTime >= mTimeToShowNextSnapshot) {
// This would mean showing the current frame very briefly. It's // This would mean showing the current frame very briefly. It's
// possible that not being displayed for a time resulted in // possible that not being displayed for a time resulted in
@@ -192,7 +203,7 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) {
} }
} }
double AnimatedImageDrawable::drawStaging(SkCanvas* canvas) { int AnimatedImageDrawable::drawStaging(SkCanvas* canvas) {
SkAutoCanvasRestore acr(canvas, false); SkAutoCanvasRestore acr(canvas, false);
if (mStagingProperties.mAlpha != SK_AlphaOPAQUE || mStagingProperties.mColorFilter.get()) { if (mStagingProperties.mAlpha != SK_AlphaOPAQUE || mStagingProperties.mColorFilter.get()) {
SkPaint paint; SkPaint paint;
@@ -211,69 +222,69 @@ double AnimatedImageDrawable::drawStaging(SkCanvas* canvas) {
// to redraw. // to redraw.
std::unique_lock lock{mImageLock}; std::unique_lock lock{mImageLock};
canvas->drawDrawable(mSkAnimatedImage.get()); canvas->drawDrawable(mSkAnimatedImage.get());
return 0.0; return 0;
} }
if (mStarting) { if (mStarting) {
mStarting = false; mStarting = false;
double duration = 0.0; int durationMS = 0;
{ {
std::unique_lock lock{mImageLock}; std::unique_lock lock{mImageLock};
mSkAnimatedImage->reset(); mSkAnimatedImage->reset();
duration = mSkAnimatedImage->currentFrameDuration(); durationMS = mSkAnimatedImage->currentFrameDuration();
} }
{ {
std::unique_lock lock{mSwapLock}; std::unique_lock lock{mSwapLock};
mLastWallTime = 0.0; mLastWallTime = 0;
mTimeToShowNextSnapshot = duration; // The current time will be added later, below.
mTimeToShowNextSnapshot = ms2ns(durationMS);
} }
} }
bool update = false; bool update = false;
{ {
const double currentTime = SkTime::GetMSecs(); const nsecs_t currentTime = systemTime(CLOCK_MONOTONIC);
std::unique_lock lock{mSwapLock}; std::unique_lock lock{mSwapLock};
// mLastWallTime starts off at 0. If it is still 0, just update it to // mLastWallTime starts off at 0. If it is still 0, just update it to
// the current time and avoid updating // the current time and avoid updating
if (mLastWallTime == 0.0) { if (mLastWallTime == 0) {
mCurrentTime = currentTime; mCurrentTime = currentTime;
// mTimeToShowNextSnapshot is already set to the duration of the // mTimeToShowNextSnapshot is already set to the duration of the
// first frame. // first frame.
mTimeToShowNextSnapshot += currentTime; mTimeToShowNextSnapshot += currentTime;
} else if (mRunning && mDidDraw) { } else if (mRunning) {
mCurrentTime += currentTime - mLastWallTime; mCurrentTime += currentTime - mLastWallTime;
update = mCurrentTime >= mTimeToShowNextSnapshot; update = mCurrentTime >= mTimeToShowNextSnapshot;
} }
mLastWallTime = currentTime; mLastWallTime = currentTime;
} }
double duration = 0.0; int durationMS = 0;
{ {
std::unique_lock lock{mImageLock}; std::unique_lock lock{mImageLock};
if (update) { if (update) {
duration = mSkAnimatedImage->decodeNextFrame(); durationMS = mSkAnimatedImage->decodeNextFrame();
} }
canvas->drawDrawable(mSkAnimatedImage.get()); canvas->drawDrawable(mSkAnimatedImage.get());
} }
mDidDraw = true;
std::unique_lock lock{mSwapLock}; std::unique_lock lock{mSwapLock};
if (update) { if (update) {
if (duration == SkAnimatedImage::kFinished) { if (durationMS == SkAnimatedImage::kFinished) {
mRunning = false; mRunning = false;
return duration; return SkAnimatedImage::kFinished;
} }
const double timeToShowCurrentSnapshot = mTimeToShowNextSnapshot; const nsecs_t timeToShowCurrentSnapshot = mTimeToShowNextSnapshot;
mTimeToShowNextSnapshot += duration; mTimeToShowNextSnapshot += ms2ns(durationMS);
if (mCurrentTime >= mTimeToShowNextSnapshot) { if (mCurrentTime >= mTimeToShowNextSnapshot) {
// As in onDraw, prevent speedy catch-up behavior. // As in onDraw, prevent speedy catch-up behavior.
mCurrentTime = timeToShowCurrentSnapshot; mCurrentTime = timeToShowCurrentSnapshot;
} }
} }
return mTimeToShowNextSnapshot;
return ns2ms(mTimeToShowNextSnapshot - mCurrentTime);
} }
} // namespace android } // namespace android

View File

@@ -19,6 +19,7 @@
#include <cutils/compiler.h> #include <cutils/compiler.h>
#include <utils/Macros.h> #include <utils/Macros.h>
#include <utils/RefBase.h> #include <utils/RefBase.h>
#include <utils/Timers.h>
#include <SkAnimatedImage.h> #include <SkAnimatedImage.h>
#include <SkCanvas.h> #include <SkCanvas.h>
@@ -50,12 +51,15 @@ public:
AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed); AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed);
/** /**
* This updates the internal time and returns true if the animation needs * This updates the internal time and returns true if the image needs
* to be redrawn. * to be redrawn this frame.
* *
* This is called on RenderThread, while the UI thread is locked. * This is called on RenderThread, while the UI thread is locked.
*
* @param outDelay Nanoseconds in the future when the following frame
* will need to be drawn. 0 if not running.
*/ */
bool isDirty(); bool isDirty(nsecs_t* outDelay);
int getStagingAlpha() const { return mStagingProperties.mAlpha; } int getStagingAlpha() const { return mStagingProperties.mAlpha; }
void setStagingAlpha(int alpha) { mStagingProperties.mAlpha = alpha; } void setStagingAlpha(int alpha) { mStagingProperties.mAlpha = alpha; }
@@ -68,7 +72,9 @@ public:
virtual SkRect onGetBounds() override { return mSkAnimatedImage->getBounds(); } virtual SkRect onGetBounds() override { return mSkAnimatedImage->getBounds(); }
// Draw to software canvas, and return time to next draw. // Draw to software canvas, and return time to next draw.
double drawStaging(SkCanvas* canvas); // 0 means the animation is not running.
// -1 means the animation advanced to the final frame.
int drawStaging(SkCanvas* canvas);
// Returns true if the animation was started; false otherwise (e.g. it was // Returns true if the animation was started; false otherwise (e.g. it was
// already running) // already running)
@@ -84,11 +90,9 @@ public:
mEndListener = std::move(listener); mEndListener = std::move(listener);
} }
void markInvisible() { mDidDraw = false; }
struct Snapshot { struct Snapshot {
sk_sp<SkPicture> mPic; sk_sp<SkPicture> mPic;
int mDuration; int mDurationMS;
Snapshot() = default; Snapshot() = default;
@@ -124,16 +128,13 @@ private:
bool nextSnapshotReady() const; bool nextSnapshotReady() const;
// When to switch from mSnapshot to mNextSnapshot. // When to switch from mSnapshot to mNextSnapshot.
double mTimeToShowNextSnapshot = 0.0; nsecs_t mTimeToShowNextSnapshot = 0;
// The current time for the drawable itself. // The current time for the drawable itself.
double mCurrentTime = 0.0; nsecs_t mCurrentTime = 0;
// The wall clock of the last time we called isDirty. // The wall clock of the last time we called isDirty.
double mLastWallTime = 0.0; nsecs_t mLastWallTime = 0;
// Whether we drew since the last call to isDirty.
bool mDidDraw = false;
// Locked when assigning snapshots and times. Operations while this is held // Locked when assigning snapshots and times. Operations while this is held
// should be short. // should be short.

View File

@@ -93,12 +93,18 @@ bool SkiaDisplayList::prepareListAndChildren(
bool isDirty = false; bool isDirty = false;
for (auto& animatedImage : mAnimatedImages) { for (auto& animatedImage : mAnimatedImages) {
nsecs_t timeTilNextFrame = TreeInfo::Out::kNoAnimatedImageDelay;
// If any animated image in the display list needs updated, then damage the node. // If any animated image in the display list needs updated, then damage the node.
if (animatedImage->isDirty()) { if (animatedImage->isDirty(&timeTilNextFrame)) {
isDirty = true; isDirty = true;
} }
if (animatedImage->isRunning()) {
info.out.hasAnimations = true; if (animatedImage->isRunning() &&
timeTilNextFrame != TreeInfo::Out::kNoAnimatedImageDelay) {
auto& delay = info.out.animatedImageDelay;
if (delay == TreeInfo::Out::kNoAnimatedImageDelay || timeTilNextFrame < delay) {
delay = timeTilNextFrame;
}
} }
} }

View File

@@ -140,6 +140,7 @@ CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode*
IContextFactory* contextFactory, IContextFactory* contextFactory,
std::unique_ptr<IRenderPipeline> renderPipeline) std::unique_ptr<IRenderPipeline> renderPipeline)
: mRenderThread(thread) : mRenderThread(thread)
, mGenerationID(0)
, mOpaque(!translucent) , mOpaque(!translucent)
, mAnimationContext(contextFactory->createAnimationContext(mRenderThread.timeLord())) , mAnimationContext(contextFactory->createAnimationContext(mRenderThread.timeLord()))
, mJankTracker(&thread.globalProfileData(), thread.mainDisplayInfo()) , mJankTracker(&thread.globalProfileData(), thread.mainDisplayInfo())
@@ -196,6 +197,7 @@ void CanvasContext::setSurface(sp<Surface>&& surface) {
mSwapHistory.clear(); mSwapHistory.clear();
} else { } else {
mRenderThread.removeFrameCallback(this); mRenderThread.removeFrameCallback(this);
mGenerationID++;
} }
} }
@@ -204,6 +206,7 @@ void CanvasContext::setSwapBehavior(SwapBehavior swapBehavior) {
} }
bool CanvasContext::pauseSurface() { bool CanvasContext::pauseSurface() {
mGenerationID++;
return mRenderThread.removeFrameCallback(this); return mRenderThread.removeFrameCallback(this);
} }
@@ -211,6 +214,7 @@ void CanvasContext::setStopped(bool stopped) {
if (mStopped != stopped) { if (mStopped != stopped) {
mStopped = stopped; mStopped = stopped;
if (mStopped) { if (mStopped) {
mGenerationID++;
mRenderThread.removeFrameCallback(this); mRenderThread.removeFrameCallback(this);
mRenderPipeline->onStop(); mRenderPipeline->onStop();
} else if (mIsDirty && hasSurface()) { } else if (mIsDirty && hasSurface()) {
@@ -383,6 +387,7 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame); mCurrentFrameInfo->addFlag(FrameInfoFlags::SkippedFrame);
} }
bool postedFrameCallback = false;
if (info.out.hasAnimations || !info.out.canDrawThisFrame) { if (info.out.hasAnimations || !info.out.canDrawThisFrame) {
if (CC_UNLIKELY(!Properties::enableRTAnimations)) { if (CC_UNLIKELY(!Properties::enableRTAnimations)) {
info.out.requiresUiRedraw = true; info.out.requiresUiRedraw = true;
@@ -391,6 +396,24 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
// If animationsNeedsRedraw is set don't bother posting for an RT anim // If animationsNeedsRedraw is set don't bother posting for an RT anim
// as we will just end up fighting the UI thread. // as we will just end up fighting the UI thread.
mRenderThread.postFrameCallback(this); mRenderThread.postFrameCallback(this);
postedFrameCallback = true;
}
}
if (!postedFrameCallback &&
info.out.animatedImageDelay != TreeInfo::Out::kNoAnimatedImageDelay) {
// Subtract the time of one frame so it can be displayed on time.
const nsecs_t kFrameTime = mRenderThread.timeLord().frameIntervalNanos();
if (info.out.animatedImageDelay <= kFrameTime) {
mRenderThread.postFrameCallback(this);
} else {
const auto delay = info.out.animatedImageDelay - kFrameTime;
int genId = mGenerationID;
mRenderThread.queue().postDelayed(delay, [this, genId]() {
if (mGenerationID == genId) {
mRenderThread.postFrameCallback(this);
}
});
} }
} }
} }
@@ -398,6 +421,7 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
void CanvasContext::stopDrawing() { void CanvasContext::stopDrawing() {
mRenderThread.removeFrameCallback(this); mRenderThread.removeFrameCallback(this);
mAnimationContext->pauseAnimators(); mAnimationContext->pauseAnimators();
mGenerationID++;
} }
void CanvasContext::notifyFramePending() { void CanvasContext::notifyFramePending() {

View File

@@ -213,6 +213,9 @@ private:
// stopped indicates the CanvasContext will reject actual redraw operations, // stopped indicates the CanvasContext will reject actual redraw operations,
// and defer repaint until it is un-stopped // and defer repaint until it is un-stopped
bool mStopped = false; bool mStopped = false;
// Incremented each time the CanvasContext is stopped. Used to ignore
// delayed messages that are triggered after stopping.
int mGenerationID;
// CanvasContext is dirty if it has received an update that it has not // CanvasContext is dirty if it has received an update that it has not
// painted onto its surface. // painted onto its surface.
bool mIsDirty = false; bool mIsDirty = false;