From 98953e3ee0be70fc8940553c6f22f20bc91f65a1 Mon Sep 17 00:00:00 2001 From: Claire Andrews Date: Tue, 2 Jun 2015 10:32:29 -0700 Subject: [PATCH] Add CanvasDevice.DeviceLost --- winrt/docsrc/CanvasDevice.xml | 49 +++- winrt/lib/drawing/CanvasDevice.abi.idl | 29 +++ winrt/lib/drawing/CanvasDevice.cpp | 63 ++++++ winrt/lib/drawing/CanvasDevice.h | 11 + .../graphics/CanvasDeviceUnitTests.cpp | 211 ++++++++++++++++++ winrt/test.internal/mocks/MockCanvasDevice.h | 29 +++ .../stubs/TestDeviceResourceCreationAdapter.h | 7 +- winrt/test.managed/Shared/DeviceLost.cs | 54 +++++ .../winrt.test.managed.Shared.projitems | 3 +- 9 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 winrt/test.managed/Shared/DeviceLost.cs diff --git a/winrt/docsrc/CanvasDevice.xml b/winrt/docsrc/CanvasDevice.xml index 67f8713b..976f3881 100644 --- a/winrt/docsrc/CanvasDevice.xml +++ b/winrt/docsrc/CanvasDevice.xml @@ -89,7 +89,54 @@ under the License. , or . - + + + Returns whether this device has lost the ability to be operational. + +

+ This method expects an error code from an exception that your app has caught. + IsDeviceLost will return true if the device is indeed lost, and the error + code actually corresponds to device removal. +

+

+ This is intended to be used like: + + try { DrawStuff(); } + catch (Exception e) where canvasDevice.IsDeviceLost(e.ErrorCode) + { + canvasDevice.RaiseDeviceLost(); + } + +

+
+
+ + Raises an event on the device, indicating that it is no longer operational. + +

+ This method should be called when your app has caught a device lost exception. + See . +

+

+ Any event handlers subscribed to the + will be issued. This will occur, even if there was no actual Direct3D device loss. This + is just a convenience, to make application testing easier. +

+
+
+ + Subscribe to this event to be notified whenever the device ceases to be operational. + +

+ Any handlers subscribed to this event will be issued whenever + is called. +

+

+ An implementation of this handler is expected to re-create the device, and any device-dependent resources. +

+
+
+ diff --git a/winrt/lib/drawing/CanvasDevice.abi.idl b/winrt/lib/drawing/CanvasDevice.abi.idl index 7cbf3ee9..6ca1835e 100644 --- a/winrt/lib/drawing/CanvasDevice.abi.idl +++ b/winrt/lib/drawing/CanvasDevice.abi.idl @@ -74,6 +74,35 @@ namespace Microsoft.Graphics.Canvas [propget] HRESULT MaximumBitmapSizeInPixels( [out, retval] INT32* value); + + // + // This event is raised whenever the native device resource is lost- + // for example, due to a user switch, lock screen, or unexpected + // graphics driver behavior. Apps are expected to re-create their + // CanvasDevice and device-dependent resources in response to this event. + // + [eventadd] HRESULT DeviceLost( + [in] Windows.Foundation.TypedEventHandler* value, + [out, retval] EventRegistrationToken* token); + + [eventremove] HRESULT DeviceLost([in] EventRegistrationToken token); + + // + // Identifies whether this error code matches a device lost. + // If this device was not actually lost, this method always + // returns false. + // + HRESULT IsDeviceLost([in] int hresult, [out, retval] boolean* value); + + // + // Call this method when a lost device error occurs- in particular, + // if an exception was caught by the app, whose error code yielded + // IsDeviceLost = true. + // + // This will cause invocation of the DeviceLost event. Occurs + // regardless of whether the native device as actually lost. + // + HRESULT RaiseDeviceLost(); }; [version(VERSION), activatable(VERSION), activatable(ICanvasDeviceFactory, VERSION), static(ICanvasDeviceStatics, VERSION)] diff --git a/winrt/lib/drawing/CanvasDevice.cpp b/winrt/lib/drawing/CanvasDevice.cpp index 2ec3ffe7..6c022042 100644 --- a/winrt/lib/drawing/CanvasDevice.cpp +++ b/winrt/lib/drawing/CanvasDevice.cpp @@ -426,6 +426,69 @@ namespace ABI { namespace Microsoft { namespace Graphics { namespace Canvas }); } + IFACEMETHODIMP CanvasDevice::add_DeviceLost( + DeviceLostHandlerType* value, + EventRegistrationToken* token) + { + return ExceptionBoundary( + [&] + { + GetResource(); // this ensures that Close() hasn't been called + CheckInPointer(value); + CheckInPointer(token); + + ThrowIfFailed(m_deviceLostEventList.Add(value, token)); + }); + } + + IFACEMETHODIMP CanvasDevice::remove_DeviceLost( + EventRegistrationToken token) + { + return ExceptionBoundary( + [&] + { + // + // This does not check if this object was closed, so to + // allow shutdown paths to freely remove events. + // + ThrowIfFailed(m_deviceLostEventList.Remove(token)); + }); + } + + IFACEMETHODIMP CanvasDevice::IsDeviceLost( + int hresult, + boolean* value) + { + return ExceptionBoundary( + [&] + { + auto& dxgiDevice = m_dxgiDevice.EnsureNotClosed(); + + CheckInPointer(value); + + if (DeviceLostException::IsDeviceLostHResult(hresult)) + { + auto d3dDevice = As(dxgiDevice); + *value = d3dDevice->GetDeviceRemovedReason() != S_OK; + } + else + { + *value = false; + } + }); + } + + IFACEMETHODIMP CanvasDevice::RaiseDeviceLost() + { + return ExceptionBoundary( + [&] + { + GetResource(); // this ensures that Close() hasn't been called + + ThrowIfFailed(m_deviceLostEventList.InvokeAll(this, nullptr)); + }); + } + IFACEMETHODIMP CanvasDevice::Close() { HRESULT hr = ResourceWrapper::Close(); diff --git a/winrt/lib/drawing/CanvasDevice.h b/winrt/lib/drawing/CanvasDevice.h index 5f6c037f..c95f6543 100644 --- a/winrt/lib/drawing/CanvasDevice.h +++ b/winrt/lib/drawing/CanvasDevice.h @@ -146,6 +146,7 @@ namespace ABI { namespace Microsoft { namespace Graphics { namespace Canvas typedef CanvasDeviceManager manager_t; }; + typedef ITypedEventHandler DeviceLostHandlerType; // // The CanvasDevice class itself. @@ -172,6 +173,8 @@ namespace ABI { namespace Microsoft { namespace Graphics { namespace Canvas // device object is open or closed. ComPtr m_primaryOutput; + EventSource> m_deviceLostEventList; + public: CanvasDevice( std::shared_ptr manager, @@ -188,6 +191,14 @@ namespace ABI { namespace Microsoft { namespace Graphics { namespace Canvas IFACEMETHOD(get_MaximumBitmapSizeInPixels)(int32_t* value) override; + IFACEMETHOD(add_DeviceLost)(DeviceLostHandlerType* value, EventRegistrationToken* token) override; + + IFACEMETHOD(remove_DeviceLost)(EventRegistrationToken token) override; + + IFACEMETHOD(IsDeviceLost)(int hresult, boolean* value) override; + + IFACEMETHOD(RaiseDeviceLost)() override; + // // ICanvasResourceCreator // diff --git a/winrt/test.internal/graphics/CanvasDeviceUnitTests.cpp b/winrt/test.internal/graphics/CanvasDeviceUnitTests.cpp index 0034c589..dc0ac16b 100644 --- a/winrt/test.internal/graphics/CanvasDeviceUnitTests.cpp +++ b/winrt/test.internal/graphics/CanvasDeviceUnitTests.cpp @@ -428,3 +428,214 @@ TEST_CLASS(DefaultDeviceResourceCreationAdapterTests) Assert::AreEqual(dxgiDevice.Get(), actualDxgiDevice.Get()); } }; + +static const HRESULT deviceRemovedHResults[] = { + DXGI_ERROR_DEVICE_HUNG, + DXGI_ERROR_DEVICE_REMOVED, + DXGI_ERROR_DEVICE_RESET, + DXGI_ERROR_DRIVER_INTERNAL_ERROR, + DXGI_ERROR_INVALID_CALL, + D2DERR_RECREATE_TARGET +}; + +TEST_CLASS(CanvasDeviceLostTests) +{ + std::shared_ptr m_resourceCreationAdapter; + std::shared_ptr m_deviceManager; + +public: + + TEST_METHOD_INITIALIZE(Reset) + { + m_resourceCreationAdapter = std::make_shared(); + m_deviceManager = std::make_shared(m_resourceCreationAdapter); + } + + TEST_METHOD_EX(CanvasDeviceLostTests_Closed) + { + auto canvasDevice = m_deviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + Assert::AreEqual(S_OK, canvasDevice->Close()); + + EventRegistrationToken token{}; + MockEventHandler dummyDeviceLostHandler(L"DeviceLost"); + Assert::AreEqual(RO_E_CLOSED, canvasDevice->add_DeviceLost(dummyDeviceLostHandler.Get(), &token)); + + // remove_DeviceLost is intended to not check if the object is closed, and like all EventSource + // events it returns success if you try and remove an unregistered token. + Assert::AreEqual(S_OK, canvasDevice->remove_DeviceLost(token)); + + boolean b; + Assert::AreEqual(RO_E_CLOSED, canvasDevice->IsDeviceLost(0, &b)); + + Assert::AreEqual(RO_E_CLOSED, canvasDevice->RaiseDeviceLost()); + + } + + TEST_METHOD_EX(CanvasDeviceLostTests_NullArgs) + { + auto canvasDevice = m_deviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + EventRegistrationToken token{}; + MockEventHandler dummyDeviceLostHandler(L"DeviceLost"); + Assert::AreEqual(E_INVALIDARG, canvasDevice->add_DeviceLost(nullptr, &token)); + Assert::AreEqual(E_INVALIDARG, canvasDevice->add_DeviceLost(dummyDeviceLostHandler.Get(), nullptr)); + Assert::AreEqual(E_INVALIDARG, canvasDevice->IsDeviceLost(0, nullptr)); + } + + class DeviceLostResourceCreationAdapter : public TestDeviceResourceCreationAdapter + { + virtual ComPtr CreateStubD3D11Device() override + { + auto stubD3DDevice = Make(); + + stubD3DDevice->GetDeviceRemovedReasonMethod.AllowAnyCall( + [] { return DXGI_ERROR_DEVICE_REMOVED; }); + + return stubD3DDevice; + } + }; + + class DeviceLostFixture + { + std::shared_ptr m_resourceCreationAdapter; + + public: + std::shared_ptr DeviceManager; + + DeviceLostFixture() + : m_resourceCreationAdapter(std::make_shared()) + , DeviceManager(std::make_shared(m_resourceCreationAdapter)) + { + } + }; + + TEST_METHOD_EX(CanvasDeviceLostTests_IsDeviceLost_DeviceRemovedHr_DeviceIsLost_ReturnsTrue) + { + DeviceLostFixture f; + auto canvasDevice = f.DeviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + for (HRESULT hr : deviceRemovedHResults) + { + boolean isDeviceLost; + Assert::AreEqual(S_OK, canvasDevice->IsDeviceLost(hr, &isDeviceLost)); + Assert::IsTrue(!!isDeviceLost); + } + } + + TEST_METHOD_EX(CanvasDeviceLostTests_IsDeviceLost_SomeArbitraryHr_DeviceIsLost_ReturnsFalse) + { + DeviceLostFixture f; + auto canvasDevice = f.DeviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + boolean isDeviceLost; + Assert::AreEqual(S_OK, canvasDevice->IsDeviceLost(E_INVALIDARG, &isDeviceLost)); + Assert::IsFalse(!!isDeviceLost); + } + + TEST_METHOD_EX(CanvasDeviceLostTests_IsDeviceLost_DeviceRemovedHr_DeviceNotActuallyLost_ReturnsFalse) + { + auto canvasDevice = m_deviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + for (HRESULT hr : deviceRemovedHResults) + { + boolean isDeviceLost; + Assert::AreEqual(S_OK, canvasDevice->IsDeviceLost(hr, &isDeviceLost)); + Assert::IsFalse(!!isDeviceLost); + } + } + + TEST_METHOD_EX(CanvasDeviceLostTests_IsDeviceLost_SomeArbitraryHr_DeviceNotActuallyLost_ReturnsFalse) + { + auto canvasDevice = m_deviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + boolean isDeviceLost; + Assert::AreEqual(S_OK, canvasDevice->IsDeviceLost(E_INVALIDARG, &isDeviceLost)); + Assert::IsFalse(!!isDeviceLost); + } + + TEST_METHOD_EX(CanvasDeviceLostTests_RaiseDeviceLost_RaisesSubscribedHandlers_DeviceActuallyLost) + { + DeviceLostFixture f; + auto canvasDevice = f.DeviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + MockEventHandler deviceLostHandler(L"DeviceLost"); + deviceLostHandler.SetExpectedCalls(1); + + EventRegistrationToken token{}; + Assert::AreEqual(S_OK, canvasDevice->add_DeviceLost(deviceLostHandler.Get(), &token)); + + Assert::AreEqual(S_OK, canvasDevice->RaiseDeviceLost()); + } + + TEST_METHOD_EX(CanvasDeviceLostTests_RaiseDeviceLost_RaisesSubscribedHandlers_DeviceNotActuallyLost) + { + // + // The unit tests testing the DeviceLost event do not exhaustively test + // everything concerning adding/removing events, because DeviceLost is + // implemented directly on top of EventSource<...>, which + // already has coverage elsewhere. + // + auto canvasDevice = m_deviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + MockEventHandler deviceLostHandler(L"DeviceLost"); + deviceLostHandler.SetExpectedCalls(1); + + EventRegistrationToken token{}; + Assert::AreEqual(S_OK, canvasDevice->add_DeviceLost(deviceLostHandler.Get(), &token)); + + Assert::AreEqual(S_OK, canvasDevice->RaiseDeviceLost()); + } + + TEST_METHOD_EX(CanvasDeviceLostTests_RemoveEventThen_RaiseDeviceLost_DoesNotInvokeHandler) + { + auto canvasDevice = m_deviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + MockEventHandler deviceLostHandler(L"DeviceLost"); + deviceLostHandler.SetExpectedCalls(0); + + EventRegistrationToken token{}; + Assert::AreEqual(S_OK, canvasDevice->add_DeviceLost(deviceLostHandler.Get(), &token)); + Assert::AreEqual(S_OK, canvasDevice->remove_DeviceLost(token)); + + Assert::AreEqual(S_OK, canvasDevice->RaiseDeviceLost()); + } + + TEST_METHOD_EX(CanvasDeviceLostTests_RaiseDeviceLost_HasCorrectSenderAndArgs) + { + DeviceLostFixture f; + auto canvasDevice = f.DeviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + MockEventHandler deviceLostHandler(L"DeviceLost"); + deviceLostHandler.SetExpectedCalls(1, + [&](ICanvasDevice* sender, IInspectable* args) + { + Assert::AreEqual(static_cast(canvasDevice.Get()), sender); + Assert::IsNull(args); + return S_OK; + }); + + EventRegistrationToken token{}; + Assert::AreEqual(S_OK, canvasDevice->add_DeviceLost(deviceLostHandler.Get(), &token)); + + Assert::AreEqual(S_OK, canvasDevice->RaiseDeviceLost()); + } + + TEST_METHOD_EX(CanvasDeviceLostTests_RaiseDeviceLost_ExceptionFromHandlerIsPropagated) + { + DeviceLostFixture f; + auto canvasDevice = f.DeviceManager->Create(CanvasDebugLevel::None, CanvasHardwareAcceleration::On); + + MockEventHandler deviceLostHandler(L"DeviceLost"); + deviceLostHandler.SetExpectedCalls(1, + [&](ICanvasDevice* sender, IInspectable* args) + { + return E_UNEXPECTED; + }); + + EventRegistrationToken token{}; + Assert::AreEqual(S_OK, canvasDevice->add_DeviceLost(deviceLostHandler.Get(), &token)); + + Assert::AreEqual(E_UNEXPECTED, canvasDevice->RaiseDeviceLost()); + } +}; \ No newline at end of file diff --git a/winrt/test.internal/mocks/MockCanvasDevice.h b/winrt/test.internal/mocks/MockCanvasDevice.h index ee233276..0daf2121 100644 --- a/winrt/test.internal/mocks/MockCanvasDevice.h +++ b/winrt/test.internal/mocks/MockCanvasDevice.h @@ -91,6 +91,35 @@ namespace canvas return E_NOTIMPL; } + IFACEMETHODIMP add_DeviceLost( + DeviceLostHandlerType* value, + EventRegistrationToken* token) + { + Assert::Fail(L"Unexpected call to add_DeviceLost"); + return E_NOTIMPL; + } + + IFACEMETHODIMP remove_DeviceLost( + EventRegistrationToken token) + { + Assert::Fail(L"Unexpected call to remove_DeviceLost"); + return E_NOTIMPL; + } + + IFACEMETHODIMP IsDeviceLost( + int hresult, + boolean* value) + { + Assert::Fail(L"Unexpected call to IsDeviceLost"); + return E_NOTIMPL; + } + + IFACEMETHODIMP RaiseDeviceLost() + { + Assert::Fail(L"Unexpected call to RaiseDeviceLost"); + return E_NOTIMPL; + } + // // ICanvasResourceCreator // diff --git a/winrt/test.internal/stubs/TestDeviceResourceCreationAdapter.h b/winrt/test.internal/stubs/TestDeviceResourceCreationAdapter.h index 5c09a696..f5cb26f2 100644 --- a/winrt/test.internal/stubs/TestDeviceResourceCreationAdapter.h +++ b/winrt/test.internal/stubs/TestDeviceResourceCreationAdapter.h @@ -63,7 +63,7 @@ public: // Stub device is used here, because, in execution of these tests, product code will // actually call methods on the factory and expect them to succeed. - *device = Make(); + *device = CreateStubD3D11Device(); return true; } } @@ -81,6 +81,11 @@ public: m_allowHardware = enable; } + virtual ComPtr CreateStubD3D11Device() + { + return Make(); + } + // These are left public because it's test code and it's convenient to do so. int m_numD2DFactoryCreationCalls; CanvasDebugLevel m_debugLevel; diff --git a/winrt/test.managed/Shared/DeviceLost.cs b/winrt/test.managed/Shared/DeviceLost.cs new file mode 100644 index 00000000..169b261c --- /dev/null +++ b/winrt/test.managed/Shared/DeviceLost.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use these files except in compliance with the License. You may obtain +// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using Microsoft.Graphics.Canvas; + +namespace test.managed +{ + [TestClass] + public class DeviceRemovedTests_WithoutControl + { + bool m_deviceLostRaised; + + [TestMethod] + public void DeviceRaisesDeviceLostEvent() + { + CanvasDevice device = new CanvasDevice(); + + // Verifies that the event is raised, even if the native + // underlying device was not lost. + Assert.IsFalse(device.IsDeviceLost(0)); + + device.DeviceLost += device_DeviceLost; + + device.RaiseDeviceLost(); + + Assert.IsTrue(m_deviceLostRaised); + + device.DeviceLost -= device_DeviceLost; + + // If the event was actually removed, shout not hit the + // assert at the top of device_DeviceLost. + device.RaiseDeviceLost(); + } + + void device_DeviceLost(CanvasDevice sender, object args) + { + Assert.IsFalse(m_deviceLostRaised); + + Assert.IsNull(args); + + m_deviceLostRaised = true; + } + } +} diff --git a/winrt/test.managed/Shared/winrt.test.managed.Shared.projitems b/winrt/test.managed/Shared/winrt.test.managed.Shared.projitems index 3634dc73..65f4e90e 100644 --- a/winrt/test.managed/Shared/winrt.test.managed.Shared.projitems +++ b/winrt/test.managed/Shared/winrt.test.managed.Shared.projitems @@ -9,10 +9,11 @@ test.managed + - + \ No newline at end of file