Dawn produces errors for several reasons. The most common is validation errors, indicating that a given descriptor, configuration, state, or action is not valid according to the WebGPU spec. Errors can also be produced during exceptional circumstances such as the system running out of GPU memory or the device being lost.
The messages attached to these errors will frequently be one of the primary tools developers use to debug problems their applications, so it is important that the messages Dawn returns are useful.
Following the guidelines in document will help ensure that Dawn's errors are clear, informative, and consistent.
Since errors are expected to be an exceptional case, it‘s important that code that produces an error doesn’t adversely impact the performance of the error-free path. The best way to ensure that is to make sure that all errors are returned from within an if statement that uses the DAWN_UNLIKELY() macro to indicate that the expression is not expected to evaluate to true. For example:
if (DAWN_UNLIKELY(offset > buffer.size)) { return DAWN_VALIDATION_ERROR("Offset (%u) is larger than the size (%u) of %s." offset, buffer.size, buffer); }
To simplify producing validation errors, it's strongly suggested that the DAWN_INVALID_IF() macro is used, which will wrap the expression in the DAWN_UNLIKELY() macro for you:
// This is equivalent to the previous example. DAWN_INVALID_IF(offset > buffer.size, "Offset (%u) is larger than the size (%u) of %s." offset, buffer.size, buffer);
// TODO: Cover MaybeError, ResultOrError<T>, DAWN_TRY(_ASSIGN), DAWN_TRY_CONTEXT, etc...
Errors returned from DAWN_INVALID_IF() or DAWN_VALIDATION_ERROR() should follow these guidelines:
Write error messages as complete sentences. (First word capitalized, ends with a period, etc.)
Command encoding has already finished.encoder finishedError messages should be in the present tense.
Buffer is not large enough...Buffer was not large enough...When possible any values mentioned should be immediately followed in parentheses by the given value.
("Array stride (%u) is not...", stride)Array stride (16) is not...When possible any object or descriptors should be represented by the object formatted as a string.
("The %s size (%s) is...", buffer, buffer.size)The [Buffer] size (512) is... or The [Buffer "Label"] size (512) is...Enum and bitmask values should be formatted as strings rather than integers or hex values.
("The %s format (%s) is...", texture, texture.format)The [Texture "Label"] format (TextureFormat::RGBA8Unorm) is...When possible state both the given value and the expected value or limit.
("Offset (%u) is larger than the size (%u) of %s.", offset, buffer.size, buffer)Offset (256) is larger than the size (144) of [Buffer "Label"].State errors in terms of what failed, rather than how to satisfy the rule.
Binding size (3) is less than the minimum binding size (32).Binding size (3) must not be less than the minimum binding size (32).Don't repeat information given in context.
When calling functions that perform validation consider if calling DAWN_TRY_CONTEXT() rather than DAWN_TRY() is appropriate. Context messages, when provided, will be appended to any validation errors as a type of human readable “callstack”. An error with context messages appears will be formatted as:
<Primary error message.> - While <context message lvl 2> - While <context message lvl 1> - While <context message lvl 0>
For example, if a validation error occurs while validating the creation of a BindGroup, the message may be:
Binding size (256) is larger than the size (80) of [Buffer "View Matrix"]. - While validating entries[1] as a Buffer - While validating [BindGroupDescriptor "Frame Bind Group"] against [BindGroupLayout] - While calling CreateBindGroup
// TODO: Guidelines about when to include context
Context messages should follow these guidelines:
Begin with the action being taken, starting with a lower case. - While will be appended by Dawn.
("validating primitive state")- While validating primitive stateWhen looping through arrays, indicate the array name and index.
("validating buffers[%u]", i)- While validating buffers[2]Indicate which descriptors or objects are being examined in as high-level a context as possible.
("validating % against %", descriptor, descriptor->layout)- While validating [BindGroupDescriptor "Label"] against [BindGroupLayout]When possible, indicate the function call being made as the top-level context, as well as the parameters passed.
("calling %s.CreatePipelineLayout(%s).", this, descriptor)- While calling [Device].CreatePipelineLayout([PipelineLayoutDescriptor]).There are multiple levels of validation and error handling which happen in Dawn. There is WebGPU API validation which directly validates the user input. There is backend validation to make sure the provided user input can execute on a given backend, there is backend error handling (e.g. when a request to the Vulkan API fails due to Out of Memory).
Within all of this, Dawn may need to execute backend functionality in service of a user API call. For example, when uploading a texture, Dawn internally may need to allocate a transfer buffer, copy the user data into that buffer and then upload. (This is what is referred to as reentrant in Dawn). Any validation error in this reentrant behaviour must be upgraded to a DeviceLost because the system internally is in an invalid state.
Internally Dawn currently supports 6 types of error, they are all listed in src/dawn/native/Error.h inside the InternalErrorType enumeration.
The content of an error is stored in a primary ErrorData class. That class is then wrapped by the error classes, UnknownError, UnrecoverableError, ValidationError.
The ErrorData class stores all the actual error information, the type, the message, any other content. Each of the Error classes stores a unique_ptr to an ErrorData.
In general, code preferably produces a Validation or Unrecoverable error. In the case where we have both validation and unrecoverable errors being produced, the Unknown error is used. UnknownError stores both Validation and other types of error types. In general using UnknownError should be used as a last resort. The code should be structured in such a way that validation is followed by non-validation errors. There are cases, like in the backends where we have a “validation sandwich” where we must validate, perform backend operation, validate, perform operation, and these require UnknownError usage.
The UnknownError provides a way to retrieve the error data as a Validation or Unrecoverable error but the InternalErrorType must match. It must be Validation to retrieve a Validation error and it must be something other than Validation for an Unrecoverable error.
An UnknownError can be initialized with either a Validation or Unrecoverable error.
An UnrecoverableError can be initialized with an Unknown error, but if that error was Validation it will become Unrecoverable.
The Maybe classes are Result<void, E> types, the ResultOr classes are Result<T, E> types which wrap the matching error type related to the name. They allow returning errors from methods and work with the various macros to pass errors up the system.
There are certain conversion restrictions between the result types.
Validation error and a UnrecoverableError can be placed in a MaybeUnknown or ResultOrUnknown error.Validation error can be placed into a MaybeError or ResultOrError but it will convert to Unrecoverable and no longer be a validation error.Unrecoverable error cannot be placed into a Validation error. This will trigger a compile error.The ErrorAdapter is used to convert between various error types. The adapter will be created templated on one of the Error classes. This template is then used to determine which of the conversion methods are available. (The adapter has an operator <type> overload for each of the Result, Maybe entries and the unique_ptr for each Error type.
Most of the work with the error classes is done through macros in the src/dawn/native/Error.h file. Some of the more interesting macros are:
DAWN_VALIDATION_ERROR -- Creates a Validation Validation error.
DAWN_PIPELINE_UNCATEGORIZED_ERROR -- Makes a PipelineUncategorized Unrecoverable error.
DAWN_BACKEND_DEVICE_LOST_ERROR -- Makes a BackendDeviceLost Unrecoverable error.
DAWN_UNRECOVERABLE_ERROR -- Makes an Unrecoverable Unrecoverable error.
DAWN_OUT_OF_MEMORY_ERROR -- Makes an OutOfMemory Unrecoverable error.
DAWN_INVALID_IF -- Creates a validation error if the given condition is false
DAWN_PIPELINE_UNCATEGORIZED_IF -- Creates the pipeline error if the condition is false
DAWN_UNRECOVERABLE_ERROR_IF -- Creates an unrecoverable error if the condition is false
DAWN_TRY -- Will execute the given command. If the command returns a Result set to an error it will use the ErrorAdapter to return the correct type for the parent method.
DAWN_TRY_ASSIGN -- Similar to the above, but will move the success value out of the Result and into the given variable.
Once an error has propagated, the ErrorSink methods are used to consume the error. (The Device, Instance and EncoderContext are all ErrorSink subclasses). Each of the subclass implements a ConsumeErrorImpl method which takes an UnknownError and emits it in some fashion.