Skip to content

Commit

Permalink
Add support for "enhanced error handling" to CGContext and CALayer (#…
Browse files Browse the repository at this point in the history
…2119)

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.
  • Loading branch information
DHowett authored Mar 3, 2017
1 parent 6bfc0cb commit abfaa3f
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 52 deletions.
93 changes: 77 additions & 16 deletions Frameworks/CoreGraphics/CGContext.mm
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,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 };

inline HRESULT _SaveD2DDrawingState(ID2D1DrawingStateBlock** pDrawingState) {
RETURN_HR_IF(E_POINTER, !pDrawingState);

Expand Down Expand Up @@ -473,22 +476,36 @@ inline void ClearPath() {
}

inline bool ShouldDraw() {
return CurrentGState().ShouldDraw();
return SUCCEEDED(_firstErrorHr) && CurrentGState().ShouldDraw();
}

inline void PushBeginDraw() {
if ((_beginEndDrawDepth)++ == 0) {
deviceContext->BeginDraw();
if (SUCCEEDED(_firstErrorHr)) {
deviceContext->BeginDraw();
}
}
}

inline HRESULT PopEndDraw() {
HRESULT hr = S_OK;
if (--(_beginEndDrawDepth) == 0) {
RETURN_IF_FAILED(deviceContext->EndDraw());
hr = deviceContext->EndDraw();
if (_useEnhancedErrorHandling && SUCCEEDED(_firstErrorHr) && FAILED(hr)) {
// If we haven't yet stored an error, and we're about to return an error (not S_OK), store it.
_firstErrorHr = hr;
hr = S_OK;
}
}
return S_OK;
return hr;
}

inline void EnableEnhancedErrorHandling() {
_useEnhancedErrorHandling = true;
}

bool GetError(CFErrorRef* /* returns-retained */ outError);

HRESULT Clip(CGPathDrawingMode pathMode);

HRESULT PushLayer(CGRect* rect = nullptr);
Expand Down Expand Up @@ -2311,8 +2328,10 @@ void CGContextShowGlyphsWithAdvances(CGContextRef context, const CGGlyph* glyphs
#pragma region Drawing Operations - Basic Shapes

HRESULT __CGContext::ClearRect(CGRect rect) {
// Skip drawing if the context has failed; this is not an error in ClearRect.
RETURN_RESULT_IF(FAILED(_firstErrorHr), S_OK);

PushBeginDraw();
auto endDraw = wil::ScopeExit([this]() { PopEndDraw(); });

ComPtr<ID2D1Factory> factory = Factory();
ComPtr<ID2D1RectangleGeometry> rectangle;
Expand Down Expand Up @@ -2340,7 +2359,8 @@ void CGContextShowGlyphsWithAdvances(CGContextRef context, const CGGlyph* glyphs
deviceContext->FillGeometry(CurrentGState().clippingGeometry.Get(), transparentBrush.Get());
deviceContext->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_SOURCE_OVER);
RETURN_IF_FAILED(PopGState());
return S_OK;

return PopEndDraw();
}

/**
Expand Down Expand Up @@ -2401,6 +2421,9 @@ void CGContextClearRect(CGContextRef context, CGRect rect) {

template <typename Lambda> // Lambda takes the form HRESULT(*)(CGContextRef, ID2D1DeviceContext*)
HRESULT __CGContext::Draw(__CGCoordinateMode coordinateMode, CGAffineTransform* additionalTransform, Lambda&& drawLambda) {
// Skip drawing if the context has failed; this is not an error in Draw.
RETURN_RESULT_IF(FAILED(_firstErrorHr), S_OK);

auto& state = CurrentGState();

if (!state.ShouldDraw()) {
Expand Down Expand Up @@ -2437,15 +2460,6 @@ void CGContextClearRect(CGContextRef context, CGRect rect) {
nullptr);
}

auto cleanup = wil::ScopeExit([this, layer]() {
if (layer) {
this->deviceContext->PopLayer();
}

// TODO GH#1194: We will need to re-evaluate Direct2D's D2DERR_RECREATE when we move to HW acceleration.
this->PopEndDraw();
});

// If the context has requested antialiasing other than the defaults we now need to update the device context.
if (CurrentGState().shouldAntialias != _kCGTrinaryDefault || !allowsAntialiasing) {
deviceContext->SetAntialiasMode(GetAntialiasMode());
Expand Down Expand Up @@ -2501,7 +2515,11 @@ void CGContextClearRect(CGContextRef context, CGRect rect) {
RETURN_IF_FAILED(std::forward<Lambda>(drawLambda)(this, deviceContext.Get()));
}

return S_OK;
if (layer) {
this->deviceContext->PopLayer();
}

return this->PopEndDraw();
}

HRESULT __CGContext::_DrawGeometryInternal(ID2D1Geometry* geometry,
Expand Down Expand Up @@ -2944,6 +2962,49 @@ void CGContextDrawPDFPage(CGContextRef context, CGPDFPageRef page) {
}
#pragma endregion

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

void CGContextIwEnableEnhancedErrorHandling(CGContextRef context) {
NOISY_RETURN_IF_NULL(context);
context->EnableEnhancedErrorHandling();
}

bool __CGContext::GetError(CFErrorRef* /* returns-retained */ outError) {
RETURN_RESULT_IF(!_useEnhancedErrorHandling, false);

HRESULT hr = _firstErrorHr;
if (SUCCEEDED(hr)) {
return false;
}

CGContextIwErrorCode errorCode = kCGContextErrorInvalidParameter;
if (hr == D2DERR_RECREATE_TARGET) {
errorCode = kCGContextErrorDeviceReset;
}
// All other errors are likely to be catastrophic; there's no point
// in differentiating them here.

if (outError) {
CFIndex embeddedHresultDowncast = static_cast<CFIndex>(hr);
auto embeddedHresult = woc::MakeStrongCF<CFNumberRef>(CFNumberCreate(nullptr, kCFNumberCFIndexType, &embeddedHresultDowncast));

CFTypeRef key = CFSTR("hresult"); // This matches the hresult exception key used in Foundation, which we can't import.
CFTypeRef value = embeddedHresult.get();
auto userInfo = woc::MakeStrongCF<CFDictionaryRef>(CFDictionaryCreate(nullptr, &key, &value, 1, &kCFCopyStringDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));

*outError = CFErrorCreate(nullptr, kCGErrorDomainIslandwood, errorCode, userInfo);
}

return true;
}

bool CGContextIwGetError(CGContextRef context, CFErrorRef* /* returns-retained */ outError) {
NOISY_RETURN_IF_NULL(context, false);
return context->GetError(outError);
}
#pragma endregion

#pragma region CGBitmapContext
struct __CGBitmapContext : CoreFoundation::CppBase<__CGBitmapContext, __CGContext> {
woc::unique_cf<CGImageRef> _image;
Expand Down
95 changes: 60 additions & 35 deletions Frameworks/QuartzCore/CALayer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#include "Quaternion.h"

#include "LoggingNative.h"
#include "NSLogging.h"
#include "CALayerInternal.h"

#import <objc/objc-arc.h>
Expand Down Expand Up @@ -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;

@interface CALayer () {
@public
CAPrivateInfo* priv;
Expand Down Expand Up @@ -316,7 +321,9 @@ CGContextRef CreateLayerContentsBitmapContext32(int width, int height, float sca
RETURN_NULL_IF_FAILED(factory->CreateWicBitmapRenderTarget(customWICBtmap.Get(), D2D1::RenderTargetProperties(), &renderTarget));
renderTarget->SetDpi(c_windowsDPI * scale, c_windowsDPI * scale);

return _CGBitmapContextCreateWithRenderTarget(renderTarget.Get(), image.get(), GUID_WICPixelFormat32bppPBGRA);
CGContextRef context = _CGBitmapContextCreateWithRenderTarget(renderTarget.Get(), image.get(), GUID_WICPixelFormat32bppPBGRA);
CGContextIwEnableEnhancedErrorHandling(context);
return context;
}

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

// Create the contents
CGContextRef drawContext = CreateLayerContentsBitmapContext32(width, height, priv->contentsScale);
unsigned int tries = 0;
do {
// Create the contents
woc::StrongCF<CGContextRef> drawContext{ woc::MakeStrongCF(
CreateLayerContentsBitmapContext32(width, height, priv->contentsScale)) };
_CGContextPushBeginDraw(drawContext);

priv->ownsContents = TRUE;
CGImageRef target = _CGBitmapContextGetImage(drawContext);
if (priv->_backgroundColor != nil && (int)[static_cast<UIColor*>(priv->_backgroundColor) _type] != solidBrush) {
CGContextSaveGState(drawContext);
CGContextSetFillColorWithColor(drawContext, [static_cast<UIColor*>(priv->_backgroundColor) CGColor]);

CGContextRetain(drawContext);
_CGContextPushBeginDraw(drawContext);
CGRect wholeRect = CGRectMake(0, 0, width, height);
CGContextFillRect(drawContext, wholeRect);
CGContextRestoreGState(drawContext);
}

auto popEnd = wil::ScopeExit([drawContext]() {
_CGContextPopEndDraw(drawContext);
CGContextRelease(drawContext);
});
// 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);
CGContextScaleCTM(drawContext, 1.0f, -1.0f);
CGContextTranslateCTM(drawContext, -priv->bounds.origin.x, -priv->bounds.origin.y);

CGImageRetain(target);
priv->savedContext = drawContext;
_CGContextSetShadowProjectionTransform(drawContext, CGAffineTransformMakeScale(1.0, -1.0));

if (priv->_backgroundColor != nil && (int)[static_cast<UIColor*>(priv->_backgroundColor) _type] != solidBrush) {
CGContextSaveGState(drawContext);
CGContextSetFillColorWithColor(drawContext, [static_cast<UIColor*>(priv->_backgroundColor) CGColor]);
[self drawInContext:drawContext];

CGRect wholeRect = CGRectMake(0, 0, width, height);
CGContextFillRect(drawContext, wholeRect);
CGContextRestoreGState(drawContext);
}
if (priv->delegate != 0) {
if ([priv->delegate respondsToSelector:@selector(displayLayer:)]) {
[priv->delegate displayLayer:self];
} else {
[priv->delegate drawLayer:self inContext:drawContext];
}
}

// 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);
CGContextScaleCTM(drawContext, 1.0f, -1.0f);
CGContextTranslateCTM(drawContext, -priv->bounds.origin.x, -priv->bounds.origin.y);
_CGContextPopEndDraw(drawContext);

_CGContextSetShadowProjectionTransform(drawContext, CGAffineTransformMakeScale(1.0, -1.0));
woc::StrongCF<CFErrorRef> renderError;
if (CGContextIwGetError(drawContext, &renderError)) {
switch (CFErrorGetCode(renderError)) {
case kCGContextErrorDeviceReset:
NSTraceInfo(TAG, @"Hardware device disappeared when rendering %@; retrying.", self);
++tries;
continue;
default: {
FAIL_FAST_MSG("Failed to render <%hs %p>: %hs",
object_getClassName(self),
self,
[[static_cast<NSError*>(renderError.get()) debugDescription] UTF8String]);
break;
}
}
}

[self drawInContext:drawContext];
CGImageRef target = _CGBitmapContextGetImage(drawContext);
priv->ownsContents = TRUE;
priv->savedContext = CGContextRetain(drawContext);
priv->contents = CGImageRetain(target);
break;
} while (tries < _kCALayerRenderAttempts);

if (priv->delegate != 0) {
if ([priv->delegate respondsToSelector:@selector(displayLayer:)]) {
[priv->delegate displayLayer:self];
} else {
[priv->delegate drawLayer:self inContext:drawContext];
}
if (!priv->contents) {
NSTraceError(TAG, @"Failed to render layer %@", self);
}

priv->contents = target;
} else if (priv->contents) {
priv->contentsSize.width = float(CGImageGetWidth(priv->contents));
priv->contentsSize.height = float(CGImageGetHeight(priv->contents));
Expand Down
4 changes: 4 additions & 0 deletions build/CoreGraphics/dll/CoreGraphics.def
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ LIBRARY CoreGraphics
CGContextConvertRectToDeviceSpace
CGContextConvertRectToUserSpace

; Public extension APIs
CGContextIwGetError
CGContextIwEnableEnhancedErrorHandling

; private exports below
_CGContextDrawImageRect
_CGContextDrawGlyphRuns
Expand Down
35 changes: 34 additions & 1 deletion include/CoreGraphics/CGContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,37 @@ COREGRAPHICS_EXPORT CGPoint CGContextConvertPointToUserSpace(CGContextRef c, CGP
COREGRAPHICS_EXPORT CGSize CGContextConvertSizeToDeviceSpace(CGContextRef c, CGSize size);
COREGRAPHICS_EXPORT CGSize CGContextConvertSizeToUserSpace(CGContextRef c, CGSize size);
COREGRAPHICS_EXPORT CGRect CGContextConvertRectToDeviceSpace(CGContextRef c, CGRect rect);
COREGRAPHICS_EXPORT CGRect CGContextConvertRectToUserSpace(CGContextRef c, CGRect rect);
COREGRAPHICS_EXPORT CGRect CGContextConvertRectToUserSpace(CGContextRef c, CGRect rect);

/*!
@function CGContextIwEnableEnhancedErrorHandling
[WinObjC Extension]
Turns on enhanced error handling for a CoreGraphics context.
A CGContext that encounters errors when rendering will, by default, assert and abort.
Enhanced handling both silences asserts and enables an interested consumer to determine whether a
context has encountered a fatal drawing error.
A consumer using enhanced error handling MUST check the status of a completed set of drawing
operations using CGContextIwGetError() and determine whether to redraw the frame.
@param context the context to enlighten
*/
COREGRAPHICS_EXPORT void CGContextIwEnableEnhancedErrorHandling(CGContextRef context);

COREGRAPHICS_EXPORT const CFStringRef kCGErrorDomainIslandwood;

typedef CF_ENUM(CFIndex, CGContextIwErrorCode) {
kCGContextErrorDeviceReset = 0x01,
kCGContextErrorInvalidParameter,
};

/*!
@function CGContextIwGetError
[WinObjC Extension]
Returns the error state of an enlightened context.
@param context the enlightened context
@param error an output pointer that, upon a true return, will hold a retained CFErrorRef
@result A boolean summarizing whether the context had an error state. If true, and if error
is provided, *error will be populated.
*/
COREGRAPHICS_EXPORT bool CGContextIwGetError(CGContextRef context, CFErrorRef* /* returns-retained */ error);

0 comments on commit abfaa3f

Please sign in to comment.