Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "enhanced error handling" to CGContext and CALayer #2119

Merged

Conversation

DHowett-MSFT
Copy link

@DHowett-MSFT DHowett-MSFT commented Mar 1, 2017

Direct2D can emit a failure or remove the backing hardware device from your render target at the end of any drawing operation. However, CoreGraphics does not natively have support for communicating failure states out to interested consumers.

Enhanced Error Handling, when enabled, allows a sufficiently motivated developer to become part of CGContext's rendering lifecycle.

The error of first and foremost importance is D2DERR_RECREATE_TARGET: It is not an error, but a plea to retry the last rendering operation after regenerating the render target. While CoreGraphics could batch up a list of pending drawing operations and replay them on a new target, it opts instead to report this to an Enhanced Error Handling consumer through kCGContextErrorDeviceReset.

All other failing drawing operations result in kCGContextErrorInvalidParameter.

In this pull request, CALayer has been augmented (in a bid to better prepare us for a hardware-accelerated future) to retry its rendering up to three (3) times if the device goes away. Since a layer can draw itself from start to finish, -display is the best point at which to handle the newly-minted CGContext errors.

Fixes #1194.

@@ -47,6 +47,7 @@
#import <cmath>
#import <list>
#import <vector>
#import <array>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oof

@@ -471,21 +475,32 @@ inline void ClearPath() {
}

inline bool ShouldDraw() {
return CurrentGState().ShouldDraw();
return FAILED(_firstErrorHr) || CurrentGState().ShouldDraw();
Copy link
Author

@DHowett-MSFT DHowett-MSFT Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hilarious. prefer SUCCEEDED or !FAILED #Closed

Copy link
Author

@DHowett-MSFT DHowett-MSFT Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And probably && #Closed

@@ -274,6 +275,9 @@ inline HRESULT GetTarget(ID2D1Image** pTarget) {
// Since nothing needs to actually be put on a stack, just increment a counter insteads
std::atomic_uint32_t _beginEndDrawDepth = { 0 };

bool _useEnhancedErrorHandling{ false };
HRESULT _firstErrorHr{ S_OK };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just _firstError, no need to put type in name

deviceContext->BeginDraw();
}
}

inline HRESULT PopEndDraw() {
HRESULT hr = S_OK;
if (--(_beginEndDrawDepth) == 0) {
Copy link
Contributor

@aballway aballway Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in PushBeginDraw we'll not increment _beginEndDrawDepth if FAILED, but here we always decrement, so we may wrap around into the middle of nowhere. Should we even attempt to draw here if the context failed at some previous point? #Resolved

#pragma endregion

#pragma region Enhanced Error Handling
const CFStringRef kCGErrorDomainIslandwood = CFSTR("kCGErrorDomainIslandwood");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: WinObjC?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aballway hm. we go back and forth on this a lot.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like standardizing on WinOBJC :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I promise I removed the comment above before submitting my review. :/

return hr;
}

inline void EnableEnhancedErrorHandling() {
Copy link
Contributor

@aballway aballway Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this make more sense as a global setting vs. per context? #WontFix

Copy link
Author

@DHowett-MSFT DHowett-MSFT Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aballway no; external bitmap context consumers are not enlightened as to error handling. #ByDesign

auto error = woc::MakeStrongCF<CFErrorRef>(CFErrorCreate(nullptr, kCGErrorDomainIslandwood, errorCode, nullptr));

if (outError) {
*outError = error.detach();
Copy link
Contributor

@aballway aballway Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just make the error here, rather than making StrongCF and detaching #Resolved

Copy link
Author

@DHowett-MSFT DHowett-MSFT Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleverer. #Closed

CGContextSetFillColorWithColor(drawContext, [static_cast<UIColor*>(priv->_backgroundColor) CGColor]);
// UIKit and CALayer consumers expect the origin to be in the top left.
// CoreGraphics defaults to the bottom left, so we must flip and translate the canvas.
CGContextTranslateCTM(drawContext, 0, heightInPoints);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: squish this into one transformation like in the other places this is done

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bah, i suppose ;)

priv->savedContext = CGContextRetain(drawContext);
priv->contents = CGImageRetain(target);
break;
} while (tries < 3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is a for loop with a fake nose and a handlebar mustache

Copy link
Contributor

@aballway aballway Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, no magic number. Maybe stuff it in the context (if we keep the enabled flag in there) or a global that can be set by user #Resolved

Copy link
Author

@DHowett-MSFT DHowett-MSFT Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that there's worth in exposing this. #Resolved

Copy link
Contributor

@ms-jihua ms-jihua Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know about exposing it, but I agree that giving it a name/placing it next to the declaration of tries might be slightly more readable. #Resolved

} else {
priv->contents = target;
if (!priv->contents) {
NSTraceError(TAG, @"Failed to render layer %@", self);
}
} else if (priv->contents) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I know you didn't modify this but please get rid of the if here

NSTraceInfo(TAG, @"Hardware device disappeared when rendering %@; retrying.", self);
++tries;
continue;
default: {
Copy link
Contributor

@aballway aballway Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use the constant here for this case, and have a default "something went terribly wrong" case. #ByDesign

Copy link
Author

@DHowett-MSFT DHowett-MSFT Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aballway default IS the "something went terribly wrong" case. #ByDesign

Copy link
Contributor

@aballway aballway Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aye but it could also be "something went terribly horribly wrong" where we don't even have a name for it 😜 #ByDesign

@@ -2367,6 +2382,10 @@ void CGContextClearRect(CGContextRef context, CGRect rect) {
HRESULT __CGContext::Draw(_CGCoordinateMode coordinateMode, CGAffineTransform* additionalTransform, Lambda&& drawLambda) {
auto& state = CurrentGState();

if (FAILED(_firstErrorHr)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I assume S_FALSE is valid to have here?
or should we take care of that?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S_FALSE is a valid "success" return :)

if (--(_beginEndDrawDepth) == 0) {
RETURN_IF_FAILED(deviceContext->EndDraw());
hr = deviceContext->EndDraw();
Copy link
Contributor

@msft-Jeyaram msft-Jeyaram Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm thinking about S_FALSE, looking at the documentation, i don't see much about it for EndDraw(), but given it's technically a success, I assume we don't have to worry about it? #ByDesign

@@ -2402,12 +2421,6 @@ void CGContextClearRect(CGContextRef context, CGRect rect) {
}

auto cleanup = wil::ScopeExit([this, layer]() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto cleanup = wil::ScopeExit(this, layer { [](start = 3, length = 48)

no longer needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

priv->backgroundColor.a);
} else {
CGContextClearToColor(drawContext, 0, 0, 0, 0);
CGRect wholeRect = CGRectMake(0, 0, width, height);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ect wholeRect = CGRectMake(0, 0, width, height); [](start = 19, length = 48)

nit: you can directly pass it

// A developer using enhanced error handling MUST check the status of a completed set of drawing
// operations using CGContextIwGetError() and determine whether to redraw the frame.
COREGRAPHICS_EXPORT void CGContextIwEnableEnhancedErrorHandling(CGContextRef context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should have some tag to indicate we have an extension?

Also it would make it easier to look at all the extensions we support/have


typedef CF_ENUM(CFIndex, CGContextIwErrorCode) {
kCGContextErrorDeviceReset,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: set the values explicitly to reduce future mishaps (if it were to ever happen)

};

COREGRAPHICS_EXPORT bool CGContextIwGetError(CGContextRef context, CFErrorRef* /* returns-retained */ error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/* returns-retained */ [](start = 79, length = 22)

since this is a public api and we are adding it, we should add c/c++ style document to it, to explicitly say what it returns and what's going on.

if (!FAILED(_firstErrorHr) && (_beginEndDrawDepth)++ == 0) {
deviceContext->BeginDraw();
if ((_beginEndDrawDepth)++ == 0) {
if (__builtin_expect(SUCCEEDED(_firstErrorHr), true)) {
Copy link
Contributor

@ms-jihua ms-jihua Mar 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fancy #Resolved

@DHowett-MSFT
Copy link
Author

The doc comment format here matches that used in CoreFoundation (!)

return false;
}

CGContextIwErrorCode errorCode = kCGContextErrorInvalidParameter;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: nice case for a ternary

@@ -85,6 +86,10 @@
NSString* const kCAFilterNearest = @"kCAFilterNearest";
NSString* const kCAFilterTrilinear = @"kCAFilterTrilinear";

// The number of rendering attempts a CALayer will make when
// its backing device disappears.
static const unsigned int _kCALayerRenderAttempts = 3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: _skCA.. ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or s_kCA... or sc_CA...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about static_const_CFStyleConstantWithAK_CALayer...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or just inline the value since it's only used in one place 😉

Copy link
Contributor

@msft-Jeyaram msft-Jeyaram left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

@DHowett-MSFT
Copy link
Author

DHowett-MSFT commented Mar 1, 2017

That can't be good.

2017-03-01T23:15:21.8217323Z ##[error]frameworks\coregraphics\cgcontext.mm(482,0):
  Error C1001: An internal error has occurred in the compiler.

}

CGContextIwErrorCode errorCode = kCGContextErrorInvalidParameter;
switch (hr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch missing default. Also could use an if here instead. Other errors are likely catastrophic with no way to recover.

}

inline void PushBeginDraw() {
if ((_beginEndDrawDepth)++ == 0) {
deviceContext->BeginDraw();
if (__builtin_expect(SUCCEEDED(_firstErrorHr), true)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other than the ICE, there is really no need to use __builtin_expect to optimize here - this is not a hot code path :).

#pragma endregion

#pragma region Enhanced Error Handling
const CFStringRef kCGErrorDomainIslandwood = CFSTR("kCGErrorDomainIslandwood");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like standardizing on WinOBJC :)

continue;
default: {
NSTraceError(TAG, @"Error %@ rendering %@.", renderError.get(), self);
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail_fast here instead. It would give us enough to up the number of retries. Another thing to note is that we should probably have another counter that falls back to warp if the particular device fails many times. That is, if each frame takes 2 attempts to draw, we are probably much better off using warp. This of course isn't necessary until we move to h/w acceleration.

@@ -577,7 +577,10 @@ - (void)display {
++tries;
continue;
default: {
NSTraceError(TAG, @"Error %@ rendering %@.", renderError.get(), self);
FAIL_FAST_MSG("Failed to render <%hs %p>: %hs",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error description can be augmented to include the failing HRESULT, printing it plus the layer should allow us to correlate events and layer classes.

@rajsesh
Copy link
Contributor

rajsesh commented Mar 2, 2017

Nit: Could you update the description with Fixes# before merge?

#pragma endregion

#pragma region Enhanced Error Handling
const CFStringRef kCGErrorDomainIslandwood = CFSTR("kCGErrorDomainIslandwood");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I promise I removed the comment above before submitting my review. :/

@DHowett-MSFT
Copy link
Author

@msft-Jeyaram: Whichever of this pull request or #2133 merges second will have to add error handling to __CGContext::ClearRect

@DHowett-MSFT
Copy link
Author

@MSFTFox: Whichever of this pull request or #2019 merges second will have to endure a merge conflict.

@DHowett-MSFT
Copy link
Author

Ugh, the build agent is stuck downloading LFS file again.

@DHowett-MSFT
Copy link
Author

The latest change here adds this cool thing:

E/WIL: E:\src\WinObjC\Frameworks\QuartzCore\CALayer.mm(583)\(null)!01E2C3CE:
    (caller: 01E2B337) FailFast(1) tid(5438) 8000FFFF Catastrophic failure
    Msg:[Failed to render <CALayer 13A3A984>:
    Error Domain=kCGErrorDomainIslandwood Code=2 "(null)"
    UserInfo={hresult=-2003238902}] [-[CALayer display]]

The important bit being hresult=-2003238902 😄

} else {
CGContextClearToColor(drawContext, 0, 0, 0, 0);

if (priv->_backgroundColor != nil && (int)[static_cast<UIColor*>(priv->_backgroundColor) _type] != solidBrush) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MSFTFox this is straight from your pull request. however, i'm concerned about what it's checking.

It looks like it will NOT fill the background if there is a solid color/solid brush background color?

@@ -524,51 +531,69 @@ - (void)display {
return;
}

// Create the contents
CGContextRef drawContext = CreateLayerContentsBitmapContext32(width, height, priv->contentsScale);
unsigned int tries = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: for (unsigned int tries = 0; tries < _kCA...; ++tries)

object_getClassName(self),
self,
[[static_cast<NSError*>(renderError.get()) debugDescription] UTF8String]);
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we need a break if we're FAIL_FAST?

@DHowett-MSFT DHowett-MSFT merged commit abfaa3f into microsoft:develop Mar 3, 2017
@DHowett-MSFT DHowett-MSFT deleted the 201702-enhanced-error-handling branch March 3, 2017 00:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants