Skip to content

Commit d7dee5b

Browse files
committed
Split dealing-with-errors from playing-with-buffers
1 parent 7bd90e9 commit d7dee5b

File tree

4 files changed

+229
-208
lines changed

4 files changed

+229
-208
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
Dealing with errors <span class="bullet">🟠</span>
2+
===================
3+
4+
```{lit-setup}
5+
:tangle-root: 018 - Dealing with errors - Next
6+
:parent: 017 - Playing with buffers - Next
7+
:debug:
8+
```
9+
10+
*Resulting code:* [`step018-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step018-next)
11+
12+
> 😃 **I did not have any trouble**, I followed the wonderful instructions in previous chapters and it **just worked**!
13+
14+
Congrats, good for you! **But** as you'll gradually want to **experiment by yourselves**, it is import to **get familiar with the cases of error** that can happen when playing with buffers.
15+
16+
Debugging a WebGPU error
17+
------------------------
18+
19+
In this chapter, **we intentionally create errors** in the result of previous chapters and see what error message it gives. This is a good practice when learning an API, so that we can then **more easily recognize these messages later on**, when they occur in more complex scenarios.
20+
21+
### Wrong usage
22+
23+
A very typical issue is to **forget about one of the usages** of a buffer. Let us introduce an error by **forgetting the `CopySrc` usage**:
24+
25+
```C++
26+
//bufferDescA.usage = WGPUBufferUsage_MapWrite | WGPUBufferUsage_CopySrc; // GOOD
27+
bufferDescA.usage = WGPUBufferUsage_MapWrite; // BAD
28+
```
29+
30+
If we run our program now, the **uncaptured error callback** that we have defined in [*The Device*](adapter-and-device/the-device.md) gets invoked twice:
31+
32+
```
33+
Uncaptured error in device 00000058E3F3F1A0: type 2 ([Buffer "Buffer A"] usage (BufferUsage::MapWrite) doesn't include BufferUsage::CopySrc.
34+
- While validating source [Buffer "Buffer A"] usage.
35+
- While encoding [CommandEncoder "My command encoder"].CopyBufferToBuffer([Buffer "Buffer A"], 16, [Buffer "Buffer B"], 0, 32).
36+
- While finishing [CommandEncoder "My command encoder"].
37+
)
38+
Submitting command...
39+
Uncaptured error in device 00000058E3F3F190: type 2 ([Invalid CommandBuffer "Command buffer" from CommandEncoder "My command encoder"] is invalid.
40+
- While calling [Queue "The Default Queue"].Submit([[Invalid CommandBuffer "Command buffer" from CommandEncoder "My command encoder"]])
41+
)
42+
```
43+
44+
```{note}
45+
The very format of error message **varies with the implementation**. Here I report what I get using Dawn.
46+
```
47+
48+
### Reading error messages
49+
50+
In general, always **investigate the first error first**. In all likelihood, the other errors are consequences of the first one. Let's focus on that first one then, and let me recall **the content of our error callback**:
51+
52+
```C++
53+
// Content of our error callback:
54+
std::cout
55+
<< "Uncaptured error in device " << device << ": type " << type
56+
<< " (" << toStdStringView(message) << ")"
57+
<< std::endl;
58+
```
59+
60+
The first thing that comes is the **device pointer** `00000058E3F3F1A0`. We only have a single device anyways, so it is not that useful in our case.
61+
62+
Then comes the **error type**, which is just "2". Not very informative, until we inspect the definition of the `WGPUErrorType` enum in `webgpu.h`:
63+
64+
```C++
65+
// The enum WGPUErrorType as defined in webgpu.h
66+
enum WGPUErrorType {
67+
WGPUErrorType_NoError = 0x00000001,
68+
WGPUErrorType_Validation = 0x00000002,
69+
WGPUErrorType_OutOfMemory = 0x00000003,
70+
WGPUErrorType_Internal = 0x00000004,
71+
WGPUErrorType_Unknown = 0x00000005,
72+
WGPUErrorType_Force32 = 0x7FFFFFFF
73+
};
74+
```
75+
76+
```{note}
77+
You may want to write a utility function that **maps each value to a string name** of the error type. Later in this guide I introduce [`magic_enum`](https://github.com/Neargye/magic_enum) which does that automatically for any enum.
78+
```
79+
80+
Here we have a **validation** error (2). This means that what we are trying to do is **not the correct use** of the API, and this is something that **the WebGPU implementation** (Dawn, wgpu-native, etc.) checks to prevent you the trouble of the cascade of unintended consequences that it could have in lower level layers.
81+
82+
Lastly, and this is the most helpful part, we have the **error message**. In Dawn, the error message **reads top down** to go **from the specific issue to the broader context** in which the error occurred. The first lines (the specific one) is already quite helpful:
83+
84+
```
85+
[Buffer "Buffer A"] usage (BufferUsage::MapWrite) doesn't include BufferUsage::CopySrc.
86+
```
87+
88+
```{note}
89+
The name `"Buffer A"` here comes directly from us, it is **the label we have assigned** to the buffer that is causing trouble. **Always label your objects**, it will save you a lot of time!
90+
```
91+
92+
The message is quite clear: the usage we declared, namely `(BufferUsage::MapWrite)`, does not include `CopySrc`. Why does this matter? The **context lines** bellow tell us why!
93+
94+
The error happens because we WebGPU tries to validate the usage of the buffer that **we use as the source** of a `CopyBufferToBuffer` operation:
95+
96+
```
97+
- While validating source [Buffer "Buffer A"] usage.
98+
- While encoding [CommandEncoder "My command encoder"].CopyBufferToBuffer([Buffer "Buffer A"], 16, [Buffer "Buffer B"], 0, 32).
99+
- While finishing [CommandEncoder "My command encoder"].
100+
```
101+
102+
Nice and helpful! And again, naming things really helps.
103+
104+
> 🤔 But **I don't remember** where I call this `CopyBufferToBuffer`. Or maybe this **happens multiple times** in my program and **I don't know which one is causing the issue**...
105+
106+
There is a solution to that, let's go further with debugging then!
107+
108+
### Investigating the call stack
109+
110+
The error handler is a function that we wrote ourselves and that lives in our code. The good thing about this is that we can then use **breakpoints** to get **insights about the execution of our program** at the time of the error.
111+
112+
```{tip}
113+
I strongly encourage you to always **set up a breakpoint** in your device's *uncaptured error callback*.
114+
```
115+
116+
In a visual IDE, adding a breakpoint is typically done by **clicking in the margin** of the source code view (see screenshot below). You can also use **command line debuggers** (e.g., [`gdb`](https://sourceware.org/gdb/)), in which case you must type in the file name and line number where to break.
117+
118+
```{figure} /images/playing-with-buffers/ide-breakpoint.jpg
119+
:align: center
120+
:class: with-shadow
121+
Once **debugging symbols** are enabled (1), we can use **breakpoints** (2) to **inspect our program's state** whenever there is an error.
122+
```
123+
124+
```{note}
125+
The screenshots used here demonstrate debugging with **Visual Studio**, but **all major C++ IDEs and debugger** provide the same concept of **breakpoint** and **call stack**. Refer to your IDE's documentation for more details.
126+
127+
To **enable debugging** symbols with build tools that **do not support multiple Debug/Release configuration** (e.g., make, ninja), you must configure your build with the `-DCMAKE_BUILD_TYPE=Debug` option **when invoking CMake**.
128+
```
129+
130+
Once you have a breakpoint, run your program as usual, and it should **automatically pause** when the execution process reaches the line with a breakpoint. Once this is the case, you can **inspect the call stack** that gives information about the context of the error:
131+
132+
```{figure} /images/playing-with-buffers/ide-callstack.jpg
133+
:align: center
134+
:class: with-shadow
135+
When our application reaches the breakpoint, it pauses and the IDE displays the current **call stack** and the current **value of variables**.
136+
```
137+
138+
```{admonition} Visual Studio
139+
If you do not find the *Call Stack* panel in Visual studio, go to "Debug > Windows > Call Stack".
140+
```
141+
142+
Here again, the stack **reads top-down** to go from specific to generic. The top-most row is the very line of your breakpoint, the next one tells you **what function call led you here**, and what parent call led you to that call, and so on.
143+
144+
**In my screenshot above**, we see that the breakpoint is located inside **a lambda function** (the `onDeviceError` lambda we passed to the device descriptor), which is **called by some `[External Code]`** (namely internal bits of the WebGPU implementation, which was built without debug symbols so it's just called "external code"), which is in turned **called by line 181 in my `main()` function**.
145+
146+
Going to that line (**double clicking** on the row in the call stack may quickly take you there), we finally **locate the guilty line of code**!
147+
148+
```{figure} /images/playing-with-buffers/ide-line181.jpg
149+
:align: center
150+
:class: with-shadow
151+
We found **the very line of code** that triggered the error!
152+
```
153+
154+
As you can see, the error that triggered the failed buffer usage verification is a call to `wgpuCommandEncoderFinish()`. This **exactly matches the bottom-most line of the WebGPU error message**! Overall, if we **glue together** the WebGPU error and the IDE call stack, we have a **full view of the chain of events** that led to our error.
155+
156+
Other examples
157+
--------------
158+
159+
### Accessing a buffer that is not mapped
160+
161+
Okey, let is try a new intentional error: what if we **forget to map the buffer A at creation**, but still try to write into its content?
162+
163+
```C++
164+
//bufferDescA.mappedAtCreation = true; // GOOD
165+
bufferDescA.mappedAtCreation = false; // BAD
166+
```
167+
168+
This time, **no invocation of the uncaptured error callback**. But no result neither: **our program crashes**! More specifically, it raises a **segmentation fault** with a message that may look like "*Access violation writing location 0x0000000000000000*".
169+
170+
If you use a debugger or if you print in the console the content of your variables, you will realize that `bufferDataA` is a **null pointer**! Indeed, this is the way `wgpuBufferGetMappedRange` **reports that it could not map** the requested buffer (see [documentation](https://webgpu-native.github.io/webgpu-headers/BufferMapping.html#MappedRangeBehavior)).
171+
172+
In this case, the error is **not as clear as previously**, so that is why we are doing this exercise of intentionally triggering errors. Remember that when `wgpuBufferGetMappedRange` returns a null pointer, **you probably forgot to map it** (or already unmapped it).
173+
174+
### Wrong buffer bounds
175+
176+
Another example? Try messing up with the arguments of `wgpuCommandEncoderCopyBufferToBuffer`!
177+
178+
For instance, if I set a size that exceeds the length of the destination buffer (e.g., replace `bufferDescB.size` with `bufferDescB.size + 10` as the last argument), my **error callback** gets invoked with the following message:
179+
180+
```
181+
Copy range (offset: 0, size: 42) does not fit in [Buffer "Buffer B"] size (32).
182+
- While validating destination [Buffer "Buffer B"] copy size.
183+
- While encoding [CommandEncoder "My command encoder"].CopyBufferToBuffer([Buffer "Buffer A"], 16, [Buffer "Buffer B"], 0, 42).
184+
- While finishing [CommandEncoder "My command encoder"].
185+
```
186+
187+
The error is clear since we **correctly labeled our objects**: the range we are trying to copy does not fit in the destination buffer B. And we know how to precisely locate the location of the error now, so we can easily fix it!
188+
189+
### Failing to unmap a buffer
190+
191+
What if we forget to unmap buffer A before the copy?
192+
193+
```C++
194+
//wgpuBufferUnmap(bufferA);
195+
```
196+
197+
We get a clear error message:
198+
199+
```
200+
[Buffer "Buffer A"] used in submit while mapped.
201+
- While calling [Queue "The Default Queue"].Submit([[CommandBuffer "Command buffer" from CommandEncoder "My command encoder"]])
202+
```
203+
204+
Note that the error occurs **while submitting the command buffer**. It is totally possible to build this command buffer (using the command encoder) while the buffer is still mapped, what matters is that it is no longer mapped **when the instructions are actually sent** to the device.
205+
206+
### Wrong limits
207+
208+
**WIP** TODO And maybe move the whole troubleshooting section to a dedicated chapter.
209+
210+
Conclusion
211+
----------
212+
213+
```{note}
214+
To **learn more about error handling in WebGPU**, you can consult [the dedicated article](https://webgpu-native.github.io/webgpu-headers/Errors.html) in the official documentation of the WebGPU C API.
215+
```
216+
217+
*Resulting code:* [`step018-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step018-next)

next/getting-started/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ hello-webgpu
1212
adapter-and-device/index
1313
the-command-queue
1414
playing-with-buffers
15+
dealing-with-errors
1516
our-first-shader
1617
```
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Out first shader <span class="bullet">🔴</span>
1+
Our first shader <span class="bullet">🔴</span>
22
================
33

44
**NB** *This is a new chapter to introduce, where we run a compute shader.*

0 commit comments

Comments
 (0)