From abfaa3f8050717a39d60f5ba5c4e1a9fbe5bfc6c Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett (MSFT)" Date: Thu, 2 Mar 2017 16:39:17 -0800 Subject: [PATCH] Add support for "enhanced error handling" to CGContext and CALayer (#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. --- Frameworks/CoreGraphics/CGContext.mm | 93 +++++++++++++++++++----- Frameworks/QuartzCore/CALayer.mm | 95 ++++++++++++++++--------- build/CoreGraphics/dll/CoreGraphics.def | 4 ++ include/CoreGraphics/CGContext.h | 35 ++++++++- 4 files changed, 175 insertions(+), 52 deletions(-) diff --git a/Frameworks/CoreGraphics/CGContext.mm b/Frameworks/CoreGraphics/CGContext.mm index cb2d7f2041..0c40754786 100644 --- a/Frameworks/CoreGraphics/CGContext.mm +++ b/Frameworks/CoreGraphics/CGContext.mm @@ -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); @@ -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); @@ -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 factory = Factory(); ComPtr rectangle; @@ -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(); } /** @@ -2401,6 +2421,9 @@ void CGContextClearRect(CGContextRef context, CGRect rect) { template // 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()) { @@ -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()); @@ -2501,7 +2515,11 @@ void CGContextClearRect(CGContextRef context, CGRect rect) { RETURN_IF_FAILED(std::forward(drawLambda)(this, deviceContext.Get())); } - return S_OK; + if (layer) { + this->deviceContext->PopLayer(); + } + + return this->PopEndDraw(); } HRESULT __CGContext::_DrawGeometryInternal(ID2D1Geometry* geometry, @@ -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(hr); + auto embeddedHresult = woc::MakeStrongCF(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(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 _image; diff --git a/Frameworks/QuartzCore/CALayer.mm b/Frameworks/QuartzCore/CALayer.mm index d43d24f523..2f4a82cc18 100644 --- a/Frameworks/QuartzCore/CALayer.mm +++ b/Frameworks/QuartzCore/CALayer.mm @@ -47,6 +47,7 @@ #include "Quaternion.h" #include "LoggingNative.h" +#include "NSLogging.h" #include "CALayerInternal.h" #import @@ -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; @@ -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; @@ -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 drawContext{ woc::MakeStrongCF( + CreateLayerContentsBitmapContext32(width, height, priv->contentsScale)) }; + _CGContextPushBeginDraw(drawContext); - priv->ownsContents = TRUE; - CGImageRef target = _CGBitmapContextGetImage(drawContext); + if (priv->_backgroundColor != nil && (int)[static_cast(priv->_backgroundColor) _type] != solidBrush) { + CGContextSaveGState(drawContext); + CGContextSetFillColorWithColor(drawContext, [static_cast(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(priv->_backgroundColor) _type] != solidBrush) { - CGContextSaveGState(drawContext); - CGContextSetFillColorWithColor(drawContext, [static_cast(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 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(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)); diff --git a/build/CoreGraphics/dll/CoreGraphics.def b/build/CoreGraphics/dll/CoreGraphics.def index 18cf6fa0e6..42df0c75a8 100644 --- a/build/CoreGraphics/dll/CoreGraphics.def +++ b/build/CoreGraphics/dll/CoreGraphics.def @@ -228,6 +228,10 @@ LIBRARY CoreGraphics CGContextConvertRectToDeviceSpace CGContextConvertRectToUserSpace + ; Public extension APIs + CGContextIwGetError + CGContextIwEnableEnhancedErrorHandling + ; private exports below _CGContextDrawImageRect _CGContextDrawGlyphRuns diff --git a/include/CoreGraphics/CGContext.h b/include/CoreGraphics/CGContext.h index d9de52ea22..11d6292dae 100644 --- a/include/CoreGraphics/CGContext.h +++ b/include/CoreGraphics/CGContext.h @@ -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); \ No newline at end of file +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);