From 53893a3d774be0417642af5e0308d5bd327f5c2d Mon Sep 17 00:00:00 2001 From: Loko Kung Date: Tue, 28 Feb 2023 04:34:32 +0000 Subject: [PATCH] Adds error promoting to device loss when disallowed error occurs in a scope. - Defaults consume error calls to only allow validation and device loss errors. - Allows OOM errors on Buffers, QuerySets, and Textures only. - Adds initial suite of unit tests (and any necessary updates to mock framework). Bug: dawn:1336 Change-Id: I82112ea6c147e894280e605bf8ae0ce00488c9f3 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/119800 Reviewed-by: Austin Eng Commit-Queue: Loko Kung Kokoro: Kokoro --- src/dawn/native/Device.cpp | 55 ++- src/dawn/native/Device.h | 83 ++-- src/dawn/native/EncodingContext.cpp | 2 +- src/dawn/native/Error.cpp | 60 +++ src/dawn/native/Error.h | 25 +- src/dawn/native/ErrorData.h | 12 + src/dawn/native/Pipeline.cpp | 2 + src/dawn/native/TintUtils.cpp | 2 +- src/dawn/native/d3d12/SwapChainD3D12.cpp | 2 +- src/dawn/native/metal/SwapChainMTL.mm | 2 +- src/dawn/native/opengl/SwapChainGL.cpp | 2 +- src/dawn/native/vulkan/SwapChainVk.cpp | 2 +- src/dawn/tests/BUILD.gn | 1 + .../unittests/native/AllowedErrorTests.cpp | 367 ++++++++++++++++++ .../unittests/native/mocks/BufferMock.cpp | 9 + .../tests/unittests/native/mocks/BufferMock.h | 5 + .../unittests/native/mocks/DeviceMock.cpp | 7 + 17 files changed, 574 insertions(+), 64 deletions(-) create mode 100644 src/dawn/tests/unittests/native/AllowedErrorTests.cpp diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp index d7132d6234..28956d0b12 100644 --- a/src/dawn/native/Device.cpp +++ b/src/dawn/native/Device.cpp @@ -209,6 +209,7 @@ DeviceBase::DeviceBase(AdapterBase* adapter, } DeviceBase::DeviceBase() : mState(State::Alive), mToggles(ToggleStage::Device) { + GetDefaultLimits(&mLimits.v1); mFormatTable = BuildFormatTable(this); } @@ -465,9 +466,12 @@ void DeviceBase::APIDestroy() { Destroy(); } -void DeviceBase::HandleError(InternalErrorType type, - const char* message, +void DeviceBase::HandleError(std::unique_ptr error, + InternalErrorType additionalAllowedErrors, WGPUDeviceLostReason lost_reason) { + InternalErrorType allowedErrors = + InternalErrorType::Validation | InternalErrorType::DeviceLost | additionalAllowedErrors; + InternalErrorType type = error->GetType(); if (type == InternalErrorType::DeviceLost) { mState = State::Disconnected; @@ -481,10 +485,12 @@ void DeviceBase::HandleError(InternalErrorType type, // A real device lost happened. Set the state to disconnected as the device cannot be // used. Also tags all commands as completed since the device stopped running. AssumeCommandsComplete(); - } else if (type == InternalErrorType::Internal) { - // If we receive an internal error, assume the backend can't recover and proceed with - // device destruction. We first wait for all previous commands to be completed so that - // backend objects can be freed immediately, before handling the loss. + } else if (!(allowedErrors & type)) { + // If we receive an error which we did not explicitly allow, assume the backend can't + // recover and proceed with device destruction. We first wait for all previous commands to + // be completed so that backend objects can be freed immediately, before handling the loss. + error->AppendContext("handling unexpected error type %s when allowed errors are %s.", type, + allowedErrors); // Move away from the Alive state so that the application cannot use this device // anymore. @@ -503,6 +509,9 @@ void DeviceBase::HandleError(InternalErrorType type, type = InternalErrorType::DeviceLost; } + // TODO(lokokung) Update call sites that take the c-string to take string_view. + const std::string messageStr = error->GetFormattedMessage(); + const char* message = messageStr.c_str(); if (type == InternalErrorType::DeviceLost) { // The device was lost, call the application callback. if (mDeviceLostCallback != nullptr) { @@ -533,10 +542,11 @@ void DeviceBase::HandleError(InternalErrorType type, } } -void DeviceBase::ConsumeError(std::unique_ptr error) { +void DeviceBase::ConsumeError(std::unique_ptr error, + InternalErrorType additionalAllowedErrors) { ASSERT(error != nullptr); AppendDebugLayerMessages(error.get()); - HandleError(error->GetType(), error->GetFormattedMessage().c_str()); + HandleError(std::move(error), additionalAllowedErrors); } void DeviceBase::APISetLoggingCallback(wgpu::LoggingCallback callback, void* userdata) { @@ -651,7 +661,10 @@ void DeviceBase::APIForceLoss(wgpu::DeviceLostReason reason, const char* message if (mState != State::Alive) { return; } - HandleError(InternalErrorType::Internal, message, ToAPI(reason)); + // Note that since we are passing None as the allowedErrors, an additional message will be + // appended noting that the error was unexpected. Since this call is for testing only it is not + // too important, but useful for users to understand where the extra message is coming from. + HandleError(DAWN_INTERNAL_ERROR(message), InternalErrorType::None, ToAPI(reason)); } DeviceBase::State DeviceBase::GetState() const { @@ -1033,8 +1046,8 @@ BindGroupLayoutBase* DeviceBase::APICreateBindGroupLayout( } BufferBase* DeviceBase::APICreateBuffer(const BufferDescriptor* descriptor) { Ref result = nullptr; - if (ConsumedError(CreateBuffer(descriptor), &result, "calling %s.CreateBuffer(%s).", this, - descriptor)) { + if (ConsumedError(CreateBuffer(descriptor), &result, InternalErrorType::OutOfMemory, + "calling %s.CreateBuffer(%s).", this, descriptor)) { ASSERT(result == nullptr); return BufferBase::MakeError(this, descriptor); } @@ -1090,8 +1103,8 @@ PipelineLayoutBase* DeviceBase::APICreatePipelineLayout( } QuerySetBase* DeviceBase::APICreateQuerySet(const QuerySetDescriptor* descriptor) { Ref result; - if (ConsumedError(CreateQuerySet(descriptor), &result, "calling %s.CreateQuerySet(%s).", this, - descriptor)) { + if (ConsumedError(CreateQuerySet(descriptor), &result, InternalErrorType::OutOfMemory, + "calling %s.CreateQuerySet(%s).", this, descriptor)) { return QuerySetBase::MakeError(this, descriptor); } return result.Detach(); @@ -1174,8 +1187,8 @@ SwapChainBase* DeviceBase::APICreateSwapChain(Surface* surface, } TextureBase* DeviceBase::APICreateTexture(const TextureDescriptor* descriptor) { Ref result; - if (ConsumedError(CreateTexture(descriptor), &result, "calling %s.CreateTexture(%s).", this, - descriptor)) { + if (ConsumedError(CreateTexture(descriptor), &result, InternalErrorType::OutOfMemory, + "calling %s.CreateTexture(%s).", this, descriptor)) { return TextureBase::MakeError(this, descriptor); } return result.Detach(); @@ -1191,12 +1204,14 @@ BufferBase* DeviceBase::APICreateErrorBuffer(const BufferDescriptor* desc) { // MapppedAtCreation == false. MaybeError maybeError = ValidateBufferDescriptor(this, &fakeDescriptor); if (maybeError.IsError()) { - ConsumedError(maybeError.AcquireError(), "calling %s.CreateBuffer(%s).", this, desc); + ConsumedError(maybeError.AcquireError(), InternalErrorType::OutOfMemory, + "calling %s.CreateBuffer(%s).", this, desc); } else { const DawnBufferDescriptorErrorInfoFromWireClient* clientErrorInfo = nullptr; FindInChain(desc->nextInChain, &clientErrorInfo); if (clientErrorInfo != nullptr && clientErrorInfo->outOfMemory) { - ConsumedError(DAWN_OUT_OF_MEMORY_ERROR("Failed to allocate memory for buffer mapping")); + ConsumedError(DAWN_OUT_OF_MEMORY_ERROR("Failed to allocate memory for buffer mapping"), + InternalErrorType::OutOfMemory); } } @@ -1377,12 +1392,12 @@ void DeviceBase::APIInjectError(wgpu::ErrorType type, const char* message) { // This method should only be used to make error scope reject. For DeviceLost there is the // LoseForTesting function that can be used instead. if (type != wgpu::ErrorType::Validation && type != wgpu::ErrorType::OutOfMemory) { - HandleError(InternalErrorType::Validation, - "Invalid injected error, must be Validation or OutOfMemory"); + HandleError( + DAWN_VALIDATION_ERROR("Invalid injected error, must be Validation or OutOfMemory")); return; } - HandleError(FromWGPUErrorType(type), message); + HandleError(DAWN_MAKE_ERROR(FromWGPUErrorType(type), message), InternalErrorType::OutOfMemory); } void DeviceBase::APIValidateTextureDescriptor(const TextureDescriptor* desc) { diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h index ddb0e20f97..ba3c7c754d 100644 --- a/src/dawn/native/Device.h +++ b/src/dawn/native/Device.h @@ -69,23 +69,28 @@ class DeviceBase : public RefCountedWithExternalCount { // Handles the error, causing a device loss if applicable. Almost always when a device loss // occurs because of an error, we want to call the device loss callback with an undefined // reason, but the ForceLoss API allows for an injection of the reason, hence the default - // argument. - void HandleError(InternalErrorType type, - const char* message, + // argument. The `additionalAllowedErrors` mask allows specifying additional errors are allowed + // (on top of validation and device loss errors). Note that "allowed" is defined as surfacing to + // users as the respective error rather than causing a device loss instead. + void HandleError(std::unique_ptr error, + InternalErrorType additionalAllowedErrors = InternalErrorType::None, WGPUDeviceLostReason lost_reason = WGPUDeviceLostReason_Undefined); - bool ConsumedError(MaybeError maybeError) { + bool ConsumedError(MaybeError maybeError, + InternalErrorType additionalAllowedErrors = InternalErrorType::None) { if (DAWN_UNLIKELY(maybeError.IsError())) { - ConsumeError(maybeError.AcquireError()); + ConsumeError(maybeError.AcquireError(), additionalAllowedErrors); return true; } return false; } template - bool ConsumedError(ResultOrError resultOrError, T* result) { + bool ConsumedError(ResultOrError resultOrError, + T* result, + InternalErrorType additionalAllowedErrors = InternalErrorType::None) { if (DAWN_UNLIKELY(resultOrError.IsError())) { - ConsumeError(resultOrError.AcquireError()); + ConsumeError(resultOrError.AcquireError(), additionalAllowedErrors); return true; } *result = resultOrError.AcquireSuccess(); @@ -93,47 +98,52 @@ class DeviceBase : public RefCountedWithExternalCount { } template - bool ConsumedError(MaybeError maybeError, const char* formatStr, const Args&... args) { + bool ConsumedError(MaybeError maybeError, + InternalErrorType additionalAllowedErrors, + const char* formatStr, + const Args&... args) { if (DAWN_UNLIKELY(maybeError.IsError())) { std::unique_ptr error = maybeError.AcquireError(); if (error->GetType() == InternalErrorType::Validation) { - std::string out; - absl::UntypedFormatSpec format(formatStr); - if (absl::FormatUntyped(&out, format, {absl::FormatArg(args)...})) { - error->AppendContext(std::move(out)); - } else { - error->AppendContext( - absl::StrFormat("[Failed to format error: \"%s\"]", formatStr)); - } + error->AppendContext(formatStr, args...); } - ConsumeError(std::move(error)); + ConsumeError(std::move(error), additionalAllowedErrors); return true; } return false; } + template + bool ConsumedError(MaybeError maybeError, const char* formatStr, Args&&... args) { + return ConsumedError(std::move(maybeError), InternalErrorType::None, formatStr, + std::forward(args)...); + } + + template + bool ConsumedError(ResultOrError resultOrError, + T* result, + InternalErrorType additionalAllowedErrors, + const char* formatStr, + const Args&... args) { + if (DAWN_UNLIKELY(resultOrError.IsError())) { + std::unique_ptr error = resultOrError.AcquireError(); + if (error->GetType() == InternalErrorType::Validation) { + error->AppendContext(formatStr, args...); + } + ConsumeError(std::move(error), additionalAllowedErrors); + return true; + } + *result = resultOrError.AcquireSuccess(); + return false; + } + template bool ConsumedError(ResultOrError resultOrError, T* result, const char* formatStr, - const Args&... args) { - if (DAWN_UNLIKELY(resultOrError.IsError())) { - std::unique_ptr error = resultOrError.AcquireError(); - if (error->GetType() == InternalErrorType::Validation) { - std::string out; - absl::UntypedFormatSpec format(formatStr); - if (absl::FormatUntyped(&out, format, {absl::FormatArg(args)...})) { - error->AppendContext(std::move(out)); - } else { - error->AppendContext( - absl::StrFormat("[Failed to format error: \"%s\"]", formatStr)); - } - } - ConsumeError(std::move(error)); - return true; - } - *result = resultOrError.AcquireSuccess(); - return false; + Args&&... args) { + return ConsumedError(std::move(resultOrError), result, InternalErrorType::None, formatStr, + std::forward(args)...); } MaybeError ValidateObject(const ApiObjectBase* object) const; @@ -489,7 +499,8 @@ class DeviceBase : public RefCountedWithExternalCount { void SetWGSLExtensionAllowList(); - void ConsumeError(std::unique_ptr error); + void ConsumeError(std::unique_ptr error, + InternalErrorType additionalAllowedErrors = InternalErrorType::None); // Each backend should implement to check their passed fences if there are any and return a // completed serial. Return 0 should indicate no fences to check. diff --git a/src/dawn/native/EncodingContext.cpp b/src/dawn/native/EncodingContext.cpp index 3f2e796788..8628e282fe 100644 --- a/src/dawn/native/EncodingContext.cpp +++ b/src/dawn/native/EncodingContext.cpp @@ -87,7 +87,7 @@ void EncodingContext::HandleError(std::unique_ptr error) { mError = std::move(error); } } else { - mDevice->HandleError(error->GetType(), error->GetFormattedMessage().c_str()); + mDevice->HandleError(std::move(error)); } } diff --git a/src/dawn/native/Error.cpp b/src/dawn/native/Error.cpp index 2d06da24b6..40d9b2adb9 100644 --- a/src/dawn/native/Error.cpp +++ b/src/dawn/native/Error.cpp @@ -61,4 +61,64 @@ InternalErrorType FromWGPUErrorType(wgpu::ErrorType type) { } } +absl::FormatConvertResult +AbslFormatConvert(InternalErrorType value, + const absl::FormatConversionSpec& spec, + absl::FormatSink* s) { + if (spec.conversion_char() == absl::FormatConversionChar::s) { + if (!static_cast(value)) { + s->Append("None"); + return {true}; + } + + bool moreThanOneBit = !HasZeroOrOneBits(value); + if (moreThanOneBit) { + s->Append("("); + } + + bool first = true; + if (value & InternalErrorType::Validation) { + if (!first) { + s->Append("|"); + } + first = false; + s->Append("Validation"); + value &= ~InternalErrorType::Validation; + } + if (value & InternalErrorType::DeviceLost) { + if (!first) { + s->Append("|"); + } + first = false; + s->Append("DeviceLost"); + value &= ~InternalErrorType::DeviceLost; + } + if (value & InternalErrorType::Internal) { + if (!first) { + s->Append("|"); + } + first = false; + s->Append("Internal"); + value &= ~InternalErrorType::Internal; + } + if (value & InternalErrorType::OutOfMemory) { + if (!first) { + s->Append("|"); + } + first = false; + s->Append("OutOfMemory"); + value &= ~InternalErrorType::OutOfMemory; + } + + if (moreThanOneBit) { + s->Append(")"); + } + } else { + s->Append(absl::StrFormat( + "%u", static_cast::type>(value))); + } + return {true}; +} + } // namespace dawn::native diff --git a/src/dawn/native/Error.h b/src/dawn/native/Error.h index 4523a547f4..bb24abc9f2 100644 --- a/src/dawn/native/Error.h +++ b/src/dawn/native/Error.h @@ -19,7 +19,6 @@ #include #include -#include "absl/strings/str_format.h" #include "dawn/common/Result.h" #include "dawn/native/ErrorData.h" #include "dawn/native/Toggles.h" @@ -27,7 +26,13 @@ namespace dawn::native { -enum class InternalErrorType : uint32_t { Validation, DeviceLost, Internal, OutOfMemory }; +enum class InternalErrorType : uint32_t { + None = 0, + Validation = 1, + DeviceLost = 2, + Internal = 4, + OutOfMemory = 8 +}; // MaybeError and ResultOrError are meant to be used as return value for function that are not // expected to, but might fail. The handling of error is potentially much slower than successes. @@ -204,6 +209,22 @@ void IgnoreErrors(MaybeError maybeError); wgpu::ErrorType ToWGPUErrorType(InternalErrorType type); InternalErrorType FromWGPUErrorType(wgpu::ErrorType type); +absl::FormatConvertResult +AbslFormatConvert(InternalErrorType value, + const absl::FormatConversionSpec& spec, + absl::FormatSink* s); + } // namespace dawn::native +// Enable dawn enum bitmask for error types. +namespace dawn { + +template <> +struct IsDawnBitmask { + static constexpr bool enable = true; +}; + +} // namespace dawn + #endif // SRC_DAWN_NATIVE_ERROR_H_ diff --git a/src/dawn/native/ErrorData.h b/src/dawn/native/ErrorData.h index 936252f827..a633e25df0 100644 --- a/src/dawn/native/ErrorData.h +++ b/src/dawn/native/ErrorData.h @@ -18,8 +18,10 @@ #include #include #include +#include #include +#include "absl/strings/str_format.h" #include "dawn/common/Compiler.h" namespace wgpu { @@ -50,6 +52,16 @@ class [[nodiscard]] ErrorData { }; void AppendBacktrace(const char* file, const char* function, int line); void AppendContext(std::string context); + template + void AppendContext(const char* formatStr, const Args&... args) { + std::string out; + absl::UntypedFormatSpec format(formatStr); + if (absl::FormatUntyped(&out, format, {absl::FormatArg(args)...})) { + AppendContext(std::move(out)); + } else { + AppendContext(absl::StrFormat("[Failed to format error: \"%s\"]", formatStr)); + } + } void AppendDebugGroup(std::string label); void AppendBackendMessage(std::string message); diff --git a/src/dawn/native/Pipeline.cpp b/src/dawn/native/Pipeline.cpp index 854d6a887e..37d0928bad 100644 --- a/src/dawn/native/Pipeline.cpp +++ b/src/dawn/native/Pipeline.cpp @@ -141,6 +141,8 @@ MaybeError ValidateProgrammableStage(DeviceBase* device, WGPUCreatePipelineAsyncStatus CreatePipelineAsyncStatusFromErrorType(InternalErrorType error) { switch (error) { + case InternalErrorType::None: + return WGPUCreatePipelineAsyncStatus_Success; case InternalErrorType::Validation: return WGPUCreatePipelineAsyncStatus_ValidationError; case InternalErrorType::DeviceLost: diff --git a/src/dawn/native/TintUtils.cpp b/src/dawn/native/TintUtils.cpp index f2e08a66a4..6e15b1f72c 100644 --- a/src/dawn/native/TintUtils.cpp +++ b/src/dawn/native/TintUtils.cpp @@ -30,7 +30,7 @@ thread_local DeviceBase* tlDevice = nullptr; void TintICEReporter(const tint::diag::List& diagnostics) { if (tlDevice) { - tlDevice->HandleError(InternalErrorType::Internal, diagnostics.str().c_str()); + tlDevice->HandleError(DAWN_INTERNAL_ERROR(diagnostics.str())); #if DAWN_ENABLE_ASSERTS for (const tint::diag::Diagnostic& diag : diagnostics) { if (diag.severity >= tint::diag::Severity::InternalCompilerError) { diff --git a/src/dawn/native/d3d12/SwapChainD3D12.cpp b/src/dawn/native/d3d12/SwapChainD3D12.cpp index 32116c48b3..fa562f20c8 100644 --- a/src/dawn/native/d3d12/SwapChainD3D12.cpp +++ b/src/dawn/native/d3d12/SwapChainD3D12.cpp @@ -99,7 +99,7 @@ TextureBase* OldSwapChain::GetNextTextureImpl(const TextureDescriptor* descripto DawnSwapChainNextTexture next = {}; DawnSwapChainError error = im.GetNextTexture(im.userData, &next); if (error) { - device->HandleError(InternalErrorType::Internal, error); + device->HandleError(DAWN_INTERNAL_ERROR(error)); return nullptr; } diff --git a/src/dawn/native/metal/SwapChainMTL.mm b/src/dawn/native/metal/SwapChainMTL.mm index f7f4538e01..7097b3af49 100644 --- a/src/dawn/native/metal/SwapChainMTL.mm +++ b/src/dawn/native/metal/SwapChainMTL.mm @@ -47,7 +47,7 @@ TextureBase* OldSwapChain::GetNextTextureImpl(const TextureDescriptor* descripto DawnSwapChainNextTexture next = {}; DawnSwapChainError error = im.GetNextTexture(im.userData, &next); if (error) { - GetDevice()->HandleError(InternalErrorType::Internal, error); + GetDevice()->HandleError(DAWN_INTERNAL_ERROR(error)); return nullptr; } diff --git a/src/dawn/native/opengl/SwapChainGL.cpp b/src/dawn/native/opengl/SwapChainGL.cpp index 8501ee7ace..2c4e3bd3fe 100644 --- a/src/dawn/native/opengl/SwapChainGL.cpp +++ b/src/dawn/native/opengl/SwapChainGL.cpp @@ -35,7 +35,7 @@ TextureBase* SwapChain::GetNextTextureImpl(const TextureDescriptor* descriptor) DawnSwapChainNextTexture next = {}; DawnSwapChainError error = im.GetNextTexture(im.userData, &next); if (error) { - GetDevice()->HandleError(InternalErrorType::Internal, error); + GetDevice()->HandleError(DAWN_INTERNAL_ERROR(error)); return nullptr; } GLuint nativeTexture = next.texture.u32; diff --git a/src/dawn/native/vulkan/SwapChainVk.cpp b/src/dawn/native/vulkan/SwapChainVk.cpp index c452488ae7..d5139dfeca 100644 --- a/src/dawn/native/vulkan/SwapChainVk.cpp +++ b/src/dawn/native/vulkan/SwapChainVk.cpp @@ -59,7 +59,7 @@ TextureBase* OldSwapChain::GetNextTextureImpl(const TextureDescriptor* descripto DawnSwapChainError error = im.GetNextTexture(im.userData, &next); if (error) { - GetDevice()->HandleError(InternalErrorType::Internal, error); + GetDevice()->HandleError(DAWN_INTERNAL_ERROR(error)); return nullptr; } diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn index f42980a16c..a87fc1e953 100644 --- a/src/dawn/tests/BUILD.gn +++ b/src/dawn/tests/BUILD.gn @@ -309,6 +309,7 @@ dawn_test("dawn_unittests") { "unittests/ToBackendTests.cpp", "unittests/TypedIntegerTests.cpp", "unittests/UnicodeTests.cpp", + "unittests/native/AllowedErrorTests.cpp", "unittests/native/BlobTests.cpp", "unittests/native/CacheRequestTests.cpp", "unittests/native/CommandBufferEncodingTests.cpp", diff --git a/src/dawn/tests/unittests/native/AllowedErrorTests.cpp b/src/dawn/tests/unittests/native/AllowedErrorTests.cpp new file mode 100644 index 0000000000..118d11ee3a --- /dev/null +++ b/src/dawn/tests/unittests/native/AllowedErrorTests.cpp @@ -0,0 +1,367 @@ +// Copyright 2023 The Dawn Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file 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. + +#include + +#include +#include + +#include "dawn/tests/MockCallback.h" +#include "dawn/webgpu_cpp.h" +#include "mocks/BufferMock.h" +#include "mocks/ComputePipelineMock.h" +#include "mocks/DawnMockTest.h" +#include "mocks/DeviceMock.h" +#include "mocks/ExternalTextureMock.h" +#include "mocks/PipelineLayoutMock.h" +#include "mocks/RenderPipelineMock.h" +#include "mocks/ShaderModuleMock.h" +#include "mocks/TextureMock.h" + +namespace dawn::native { +namespace { + +using ::testing::_; +using ::testing::ByMove; +using ::testing::HasSubstr; +using ::testing::MockCallback; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::StrictMock; +using ::testing::Test; + +static constexpr char kOomErrorMessage[] = "Out of memory error"; + +static constexpr std::string_view kComputeShader = R"( + @compute @workgroup_size(1) fn main() {} + )"; + +static constexpr std::string_view kVertexShader = R"( + @vertex fn main() -> @builtin(position) vec4f { + return vec4f(0.0, 0.0, 0.0, 0.0); + } + )"; + +class AllowedErrorTests : public DawnMockTest { + public: + AllowedErrorTests() : DawnMockTest() { + device.SetDeviceLostCallback(mDeviceLostCb.Callback(), mDeviceLostCb.MakeUserdata(this)); + device.SetUncapturedErrorCallback(mDeviceErrorCb.Callback(), + mDeviceErrorCb.MakeUserdata(this)); + } + + ~AllowedErrorTests() override { device = nullptr; } + + protected: + // Device mock callbacks used throughout the tests. + StrictMock> mDeviceLostCb; + StrictMock> mDeviceErrorCb; +}; + +// +// Exercise APIs where OOM errors cause a device lost. +// + +TEST_F(AllowedErrorTests, QueueSubmit) { + EXPECT_CALL(*(mDeviceMock->GetQueueMock()), SubmitImpl) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + + // Expect the device lost because of the error. + EXPECT_CALL(mDeviceLostCb, + Call(WGPUDeviceLostReason_Undefined, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + device.GetQueue().Submit(0, nullptr); +} + +TEST_F(AllowedErrorTests, QueueWriteBuffer) { + BufferDescriptor desc = {}; + desc.size = 1; + desc.usage = wgpu::BufferUsage::CopyDst; + BufferMock* bufferMock = new NiceMock(mDeviceMock, &desc); + wgpu::Buffer buffer = wgpu::Buffer::Acquire(ToAPI(bufferMock)); + + EXPECT_CALL(*(mDeviceMock->GetQueueMock()), WriteBufferImpl) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + + // Expect the device lost because of the error. + EXPECT_CALL(mDeviceLostCb, + Call(WGPUDeviceLostReason_Undefined, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + constexpr uint8_t data = 8; + device.GetQueue().WriteBuffer(buffer, 0, &data, 0); +} + +TEST_F(AllowedErrorTests, QueueWriteTexture) { + TextureDescriptor desc = {}; + desc.size.width = 1; + desc.size.height = 1; + desc.usage = wgpu::TextureUsage::CopyDst; + desc.format = wgpu::TextureFormat::RGBA8Unorm; + TextureMock* textureMock = new NiceMock(mDeviceMock, &desc); + wgpu::Texture texture = wgpu::Texture::Acquire(ToAPI(textureMock)); + + EXPECT_CALL(*(mDeviceMock->GetQueueMock()), WriteTextureImpl) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + + // Expect the device lost because of the error. + EXPECT_CALL(mDeviceLostCb, + Call(WGPUDeviceLostReason_Undefined, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + constexpr uint8_t data[] = {1, 2, 4, 8}; + wgpu::ImageCopyTexture dest = {}; + dest.texture = texture; + wgpu::TextureDataLayout layout = {}; + wgpu::Extent3D size = {1, 1}; + device.GetQueue().WriteTexture(&dest, &data, 4, &layout, &size); +} + +// Even though OOM is allowed in buffer creation, when creating a buffer in internal workaround the +// OOM should be masked as a device loss. +TEST_F(AllowedErrorTests, QueueCopyTextureForBrowserOomBuffer) { + wgpu::TextureDescriptor desc = {}; + desc.size = {4, 4}; + desc.usage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst | + wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding; + desc.format = wgpu::TextureFormat::RGBA8Unorm; + + wgpu::ImageCopyTexture src = {}; + src.texture = device.CreateTexture(&desc); + wgpu::ImageCopyTexture dst = {}; + dst.texture = device.CreateTexture(&desc); + wgpu::Extent3D size = {4, 4}; + wgpu::CopyTextureForBrowserOptions options = {}; + + // Copying texture for browser internally allocates a buffer which we will cause to fail here. + EXPECT_CALL(*mDeviceMock, CreateBufferImpl) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + + // Expect the device lost because of the error. + EXPECT_CALL(mDeviceLostCb, + Call(WGPUDeviceLostReason_Undefined, HasSubstr(kOomErrorMessage), this)) + .Times(1); + device.GetQueue().CopyTextureForBrowser(&src, &dst, &size, &options); +} + +// Even though OOM is allowed in buffer creation, when creating a buffer in internal workaround the +// OOM should be masked as a device loss. +TEST_F(AllowedErrorTests, QueueCopyExternalTextureForBrowserOomBuffer) { + wgpu::TextureDescriptor textureDesc = {}; + textureDesc.size = {4, 4}; + textureDesc.usage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst | + wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::TextureBinding; + textureDesc.format = wgpu::TextureFormat::RGBA8Unorm; + + wgpu::TextureViewDescriptor textureViewDesc = {}; + textureViewDesc.format = wgpu::TextureFormat::RGBA8Unorm; + + wgpu::ExternalTextureDescriptor externalTextureDesc = {}; + std::array placeholderConstantArray; + externalTextureDesc.yuvToRgbConversionMatrix = placeholderConstantArray.data(); + externalTextureDesc.gamutConversionMatrix = placeholderConstantArray.data(); + externalTextureDesc.srcTransferFunctionParameters = placeholderConstantArray.data(); + externalTextureDesc.dstTransferFunctionParameters = placeholderConstantArray.data(); + externalTextureDesc.visibleSize = {4, 4}; + externalTextureDesc.plane0 = device.CreateTexture(&textureDesc).CreateView(&textureViewDesc); + + wgpu::ImageCopyExternalTexture src = {}; + src.externalTexture = device.CreateExternalTexture(&externalTextureDesc); + wgpu::ImageCopyTexture dst = {}; + dst.texture = device.CreateTexture(&textureDesc); + wgpu::Extent3D size = {4, 4}; + wgpu::CopyTextureForBrowserOptions options = {}; + + // Copying texture for browser internally allocates a buffer which we will cause to fail here. + EXPECT_CALL(*mDeviceMock, CreateBufferImpl) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + + // Expect the device lost because of the error. + EXPECT_CALL(mDeviceLostCb, + Call(WGPUDeviceLostReason_Undefined, HasSubstr(kOomErrorMessage), this)) + .Times(1); + device.GetQueue().CopyExternalTextureForBrowser(&src, &dst, &size, &options); +} + +// OOM error from synchronously initializing a compute pipeline should result in a device loss. +TEST_F(AllowedErrorTests, CreateComputePipeline) { + Ref csModule = ShaderModuleMock::Create(mDeviceMock, kComputeShader.data()); + + ComputePipelineDescriptor desc = {}; + desc.compute.module = csModule.Get(); + desc.compute.entryPoint = "main"; + + Ref computePipelineMock = ComputePipelineMock::Create(mDeviceMock, &desc); + EXPECT_CALL(*computePipelineMock.Get(), Initialize) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + EXPECT_CALL(*mDeviceMock, CreateUninitializedComputePipelineImpl) + .WillOnce(Return(ByMove(std::move(computePipelineMock)))); + + // Expect the device lost because of the error. + EXPECT_CALL(mDeviceLostCb, + Call(WGPUDeviceLostReason_Undefined, HasSubstr(kOomErrorMessage), this)) + .Times(1); + device.CreateComputePipeline(ToCppAPI(&desc)); +} + +// OOM error from synchronously initializing a render pipeline should result in a device loss. +TEST_F(AllowedErrorTests, CreateRenderPipeline) { + Ref vsModule = ShaderModuleMock::Create(mDeviceMock, kVertexShader.data()); + + RenderPipelineDescriptor desc = {}; + desc.vertex.module = vsModule.Get(); + desc.vertex.entryPoint = "main"; + + Ref renderPipelineMock = RenderPipelineMock::Create(mDeviceMock, &desc); + EXPECT_CALL(*renderPipelineMock.Get(), Initialize) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + EXPECT_CALL(*mDeviceMock, CreateUninitializedRenderPipelineImpl) + .WillOnce(Return(ByMove(std::move(renderPipelineMock)))); + + // Expect the device lost because of the error. + EXPECT_CALL(mDeviceLostCb, + Call(WGPUDeviceLostReason_Undefined, HasSubstr(kOomErrorMessage), this)) + .Times(1); + device.CreateRenderPipeline(ToCppAPI(&desc)); +} + +// +// Exercise async APIs where OOM errors do NOT currently cause a device lost. +// + +// OOM error from asynchronously initializing a compute pipeline should not result in a device loss. +TEST_F(AllowedErrorTests, CreateComputePipelineAsync) { + Ref csModule = ShaderModuleMock::Create(mDeviceMock, kComputeShader.data()); + + ComputePipelineDescriptor desc = {}; + desc.compute.module = csModule.Get(); + desc.compute.entryPoint = "main"; + + Ref computePipelineMock = ComputePipelineMock::Create(mDeviceMock, &desc); + EXPECT_CALL(*computePipelineMock.Get(), Initialize) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + EXPECT_CALL(*mDeviceMock, CreateUninitializedComputePipelineImpl) + .WillOnce(Return(ByMove(std::move(computePipelineMock)))); + + MockCallback cb; + EXPECT_CALL( + cb, Call(WGPUCreatePipelineAsyncStatus_InternalError, _, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + device.CreateComputePipelineAsync(ToCppAPI(&desc), cb.Callback(), cb.MakeUserdata(this)); + device.Tick(); + + // Device lost should only happen because of destruction. + EXPECT_CALL(mDeviceLostCb, Call(WGPUDeviceLostReason_Destroyed, _, this)).Times(1); +} + +// OOM error from asynchronously initializing a render pipeline should not result in a device loss. +TEST_F(AllowedErrorTests, CreateRenderPipelineAsync) { + Ref vsModule = ShaderModuleMock::Create(mDeviceMock, kVertexShader.data()); + + RenderPipelineDescriptor desc = {}; + desc.vertex.module = vsModule.Get(); + desc.vertex.entryPoint = "main"; + + Ref renderPipelineMock = RenderPipelineMock::Create(mDeviceMock, &desc); + EXPECT_CALL(*renderPipelineMock.Get(), Initialize) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + EXPECT_CALL(*mDeviceMock, CreateUninitializedRenderPipelineImpl) + .WillOnce(Return(ByMove(std::move(renderPipelineMock)))); + + MockCallback cb; + EXPECT_CALL( + cb, Call(WGPUCreatePipelineAsyncStatus_InternalError, _, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + device.CreateRenderPipelineAsync(ToCppAPI(&desc), cb.Callback(), cb.MakeUserdata(this)); + device.Tick(); + + // Device lost should only happen because of destruction. + EXPECT_CALL(mDeviceLostCb, Call(WGPUDeviceLostReason_Destroyed, _, this)).Times(1); +} + +// +// Exercise APIs where OOM error are allowed and surfaced. +// + +// OOM error from buffer creation is allowed and surfaced directly. +TEST_F(AllowedErrorTests, CreateBuffer) { + EXPECT_CALL(*mDeviceMock, CreateBufferImpl) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + + // Expect the OOM error. + EXPECT_CALL(mDeviceErrorCb, Call(WGPUErrorType_OutOfMemory, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + wgpu::BufferDescriptor desc = {}; + desc.usage = wgpu::BufferUsage::Uniform; + desc.size = 16; + device.CreateBuffer(&desc); + + // Device lost should only happen because of destruction. + EXPECT_CALL(mDeviceLostCb, Call(WGPUDeviceLostReason_Destroyed, _, this)).Times(1); +} + +// OOM error from texture creation is allowed and surfaced directly. +TEST_F(AllowedErrorTests, CreateTexture) { + EXPECT_CALL(*mDeviceMock, CreateTextureImpl) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + + // Expect the OOM error. + EXPECT_CALL(mDeviceErrorCb, Call(WGPUErrorType_OutOfMemory, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + wgpu::TextureDescriptor desc = {}; + desc.usage = wgpu::TextureUsage::CopySrc; + desc.size = {4, 4}; + desc.format = wgpu::TextureFormat::RGBA8Unorm; + device.CreateTexture(&desc); + + // Device lost should only happen because of destruction. + EXPECT_CALL(mDeviceLostCb, Call(WGPUDeviceLostReason_Destroyed, _, this)).Times(1); +} + +// OOM error from query set creation is allowed and surfaced directly. +TEST_F(AllowedErrorTests, CreateQuerySet) { + EXPECT_CALL(*mDeviceMock, CreateQuerySetImpl) + .WillOnce(Return(DAWN_OUT_OF_MEMORY_ERROR(kOomErrorMessage))); + + // Expect the OOM error. + EXPECT_CALL(mDeviceErrorCb, Call(WGPUErrorType_OutOfMemory, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + wgpu::QuerySetDescriptor desc = {}; + desc.type = wgpu::QueryType::Occlusion; + desc.count = 1; + device.CreateQuerySet(&desc); + + // Device lost should only happen because of destruction. + EXPECT_CALL(mDeviceLostCb, Call(WGPUDeviceLostReason_Destroyed, _, this)).Times(1); +} + +TEST_F(AllowedErrorTests, InjectError) { + // Expect the OOM error. + EXPECT_CALL(mDeviceErrorCb, Call(WGPUErrorType_OutOfMemory, HasSubstr(kOomErrorMessage), this)) + .Times(1); + + device.InjectError(wgpu::ErrorType::OutOfMemory, kOomErrorMessage); + + // Device lost should only happen because of destruction. + EXPECT_CALL(mDeviceLostCb, Call(WGPUDeviceLostReason_Destroyed, _, this)).Times(1); +} + +} // namespace +} // namespace dawn::native diff --git a/src/dawn/tests/unittests/native/mocks/BufferMock.cpp b/src/dawn/tests/unittests/native/mocks/BufferMock.cpp index 788a61bd6c..5cffcec935 100644 --- a/src/dawn/tests/unittests/native/mocks/BufferMock.cpp +++ b/src/dawn/tests/unittests/native/mocks/BufferMock.cpp @@ -16,9 +16,18 @@ namespace dawn::native { +using ::testing::Return; + BufferMock::BufferMock(DeviceMock* device, const BufferDescriptor* descriptor) : BufferBase(device, descriptor) { + mBackingData = std::unique_ptr(new uint8_t[GetSize()]); + mAllocatedSize = GetSize(); + ON_CALL(*this, DestroyImpl).WillByDefault([this]() { this->BufferBase::DestroyImpl(); }); + ON_CALL(*this, GetMappedPointer).WillByDefault(Return(mBackingData.get())); + ON_CALL(*this, IsCPUWritableAtCreation).WillByDefault([this]() { + return (GetUsage() & (wgpu::BufferUsage::MapRead | wgpu::BufferUsage::MapWrite)) != 0; + }); } BufferMock::~BufferMock() = default; diff --git a/src/dawn/tests/unittests/native/mocks/BufferMock.h b/src/dawn/tests/unittests/native/mocks/BufferMock.h index 7ddfc1fa69..2699f93f94 100644 --- a/src/dawn/tests/unittests/native/mocks/BufferMock.h +++ b/src/dawn/tests/unittests/native/mocks/BufferMock.h @@ -15,6 +15,8 @@ #ifndef SRC_DAWN_TESTS_UNITTESTS_NATIVE_MOCKS_BUFFERMOCK_H_ #define SRC_DAWN_TESTS_UNITTESTS_NATIVE_MOCKS_BUFFERMOCK_H_ +#include + #include "gmock/gmock.h" #include "dawn/native/Buffer.h" @@ -38,6 +40,9 @@ class BufferMock : public BufferBase { MOCK_METHOD(void*, GetMappedPointer, (), (override)); MOCK_METHOD(bool, IsCPUWritableAtCreation, (), (const, override)); + + private: + std::unique_ptr mBackingData; }; } // namespace dawn::native diff --git a/src/dawn/tests/unittests/native/mocks/DeviceMock.cpp b/src/dawn/tests/unittests/native/mocks/DeviceMock.cpp index e701c8b221..548e8a890f 100644 --- a/src/dawn/tests/unittests/native/mocks/DeviceMock.cpp +++ b/src/dawn/tests/unittests/native/mocks/DeviceMock.cpp @@ -17,6 +17,7 @@ #include "dawn/tests/unittests/native/mocks/BindGroupLayoutMock.h" #include "dawn/tests/unittests/native/mocks/BindGroupMock.h" #include "dawn/tests/unittests/native/mocks/BufferMock.h" +#include "dawn/tests/unittests/native/mocks/CommandBufferMock.h" #include "dawn/tests/unittests/native/mocks/ComputePipelineMock.h" #include "dawn/tests/unittests/native/mocks/ExternalTextureMock.h" #include "dawn/tests/unittests/native/mocks/PipelineLayoutMock.h" @@ -53,6 +54,12 @@ DeviceMock::DeviceMock() { [this](const BufferDescriptor* descriptor) -> ResultOrError> { return AcquireRef(new NiceMock(this, descriptor)); })); + ON_CALL(*this, CreateCommandBuffer) + .WillByDefault(WithArgs<0, 1>( + [this](CommandEncoder* encoder, const CommandBufferDescriptor* descriptor) + -> ResultOrError> { + return AcquireRef(new NiceMock(this, encoder, descriptor)); + })); ON_CALL(*this, CreateExternalTextureImpl) .WillByDefault(WithArgs<0>([this](const ExternalTextureDescriptor* descriptor) -> ResultOrError> {