Skip to content

Commit 0ff3dbf

Browse files
committed
Start next version of Playing with buffers
1 parent 06a8620 commit 0ff3dbf

File tree

5 files changed

+313
-13
lines changed

5 files changed

+313
-13
lines changed

_extensions/sphinx_literate/registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ def register_codeblock(self, lit: CodeBlock, options: BlockOptions = set()) -> N
405405
tangle_root = lit.tangle_root,
406406
source_location = lit.source_location,
407407
target = lit.target,
408+
lexer = lit.lexer,
408409
)
409410
modifier.inserted_location = InsertLocation(placement, pattern)
410411
modifier.inserted_block = lit

_extensions/sphinx_literate/tangle.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,17 @@ def _tangle_rec(
2525
) -> None:
2626
assert(lit is not None)
2727
tangle_info = _get_tangle_info(registry, lit, override_tangle_root)
28+
comment_prefix = {
29+
"c++": "//",
30+
"javascript": "//",
31+
"rust": "//",
32+
"python": "#",
33+
"cmake": "#",
34+
}.get(lit.lexer.lower() if lit.lexer is not None else None, "//")
35+
if lit.lexer is None:
36+
print(f"######## {lit.format()} from {lit.source_location.format()}")
2837
if tangle_info is not None and tangle_info.debug:
29-
tangled_content.append(prefix + f"// {{Begin block {lit.format()}}}")
38+
tangled_content.append(prefix + f"{comment_prefix} {{Begin block {lit.format()}}}")
3039
for line in lit.all_content(registry, override_tangle_root):
3140
# TODO: use parse.parse_block_content here?
3241
subprefix = None
@@ -59,7 +68,7 @@ def _tangle_rec(
5968
else:
6069
tangled_content.append(prefix + line)
6170
if tangle_info is not None and tangle_info.debug:
62-
tangled_content.append(prefix + f"// {{End block {lit.format()}}}")
71+
tangled_content.append(prefix + f"{comment_prefix} {{End block {lit.format()}}}")
6372

6473
#############################################################
6574
# Public

basic-3d-rendering/hello-triangle.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ Other graphics APIs provide access to more programmable stages (geometry shader,
7676
As always, we build a descriptor in order to create the render pipeline:
7777
7878
````{tab} With webgpu.hpp
79-
```{lit} Create Render Pipeline
79+
```{lit} C++, Create Render Pipeline
8080
RenderPipelineDescriptor pipelineDesc;
8181
{{Describe render pipeline}}
8282
RenderPipeline pipeline = device.createRenderPipeline(pipelineDesc);
8383
```
8484
````
8585

8686
````{tab} Vanilla webgpu.h
87-
```{lit} Create Render Pipeline (for tangle root "Vanilla")
87+
```{lit} C++, Create Render Pipeline (for tangle root "Vanilla")
8888
WGPURenderPipelineDescriptor pipelineDesc{};
8989
pipelineDesc.nextInChain = nullptr;
9090
{{Describe render pipeline}}
@@ -587,13 +587,13 @@ private:
587587
````{warning}
588588
Make sure not to locally **shadow** the class-level declaration of `pipeline` and `surfaceFormat` by re-declaring them when calling `createRenderPipeline` and `getPreferredFormat` respectively.
589589
590-
```{lit} Create Render Pipeline (hidden, replace)
590+
```{lit} C++, Create Render Pipeline (hidden, replace)
591591
RenderPipelineDescriptor pipelineDesc;
592592
{{Describe render pipeline}}
593593
pipeline = device.createRenderPipeline(pipelineDesc);
594594
```
595595
596-
```{lit} Create Render Pipeline (hidden, replace, for tangle root "Vanilla")
596+
```{lit} C++, Create Render Pipeline (hidden, replace, for tangle root "Vanilla")
597597
WGPURenderPipelineDescriptor pipelineDesc{};
598598
pipelineDesc.nextInChain = nullptr;
599599
{{Describe render pipeline}}

next/getting-started/playing-with-buffers.md

Lines changed: 288 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,296 @@
1-
Playing with buffers <span class="bullet">🔴</span>
1+
Playing with buffers <span class="bullet">🟠</span>
22
====================
33

4-
**WIP** *In this version of the guide, this chapter moves back in the "getting started" section, between command queue and the first (compute) shader.*
4+
```{lit-setup}
5+
:tangle-root: 017 - Playing with buffers - Next
6+
:parent: 015 - The Command Queue - Next
7+
:debug:
8+
```
9+
10+
*Resulting code:* [`step017-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step017-next)
11+
12+
We have seen how to send **instructions** to the GPU, and we now see how to send **data**.
513

614
In this chapter:
715

8-
- We see **how to create and manipulate buffers**.
9-
- We refine our control of **asynchronous operations**.
16+
- We see **how to allocate data buffers on the GPU**.
17+
- We see our first useful GPU command to **copy buffers**.
18+
- We refine our control of **asynchronous operations** to retrieve data from the GPU.
19+
20+
Memory allocation
21+
-----------------
22+
23+
Allocating memory on the GPU (in the VRAM) is slightly more complex than allocating memory on the CPU (in the RAM).
24+
25+
### Recall about CPU-side allocation
26+
27+
When we talk about (dynamic) memory allocation on CPU, we may think of something like this:
28+
29+
```C++
30+
// Allocation in RAM
31+
char* buffer = (char*)malloc(256); // C
32+
char* buffer = new char[256]; // C++
33+
auto buffer = std::vector<char>(256); // C++ using STL
34+
```
35+
36+
The `malloc` function, and everything that is built on top of is like the C++ `new[]` operator or the **default allocator** from the *standard template library* (STL), is seen as a low-level black box that **asks the OS for some contiguous memory buffer**.
37+
38+
In most cases, the level of abstraction of these is enough, and we do not need control about where/how this memory is allocated.
39+
40+
It is nonetheless possible to go **lower level** and for instance implement a custom [`std::allocator`](https://en.cppreference.com/w/cpp/memory/allocator) so that we can inform the runtime about our intended **usage** of this memory, which it can use to **find a more appropriate place** in its memory.
41+
42+
We will **not** do custom allocation on the C++ side, but I mention all this to help justifying how things work on the GPU side.
43+
44+
```{note}
45+
We talk here about **dynamic allocation**, where the allocation may depend on runtime inputs. This is opposed to static allocation, like when declaring `char buffer[256];`, which is known at **compile time** and thus handled differently.
46+
47+
In case of GPU memory, static allocation happens when compiling shaders (see next chapter), but our C++ code can only interact with dynamically allocated data.
48+
```
49+
50+
### GPU-side allocation
51+
52+
Because GPU programs are highly parallel, they process **a lot of data**, to a point where **data transfers** within the GPU (between computing units and VRAM) is often the **limiting bottleneck** of our programs.
53+
54+
For this reason, we **always specify an intended usage** when allocating data on the GPU!
55+
56+
Let us get practical: to allocate data, we create a `WGPUBuffer` object, which follows the usual object creation idiom:
57+
58+
```{lit} C++, Create buffer A
59+
// 1. We build a descriptor (called 'A' because we will have multiple buffers)
60+
WGPUBufferDescriptor bufferDescA = WGPU_BUFFER_DESCRIPTOR_INIT;
61+
{{Fill in buffer descriptor A}}
62+
63+
// 2. We create the buffer from its descriptor
64+
WGPUBuffer bufferA = wgpuDeviceCreateBuffer(device, &bufferDescA);
65+
```
66+
67+
First of all, we of course need to specify the **size of the buffer**, like we do when calling `malloc`:
68+
69+
```{lit} C++, Fill in buffer descriptor A
70+
bufferDescA.size = 256;
71+
```
72+
73+
Then, as described above, we tell the device **how we intend to use this buffer**. The usage is given as a **bitmask**, i.e., an integer where each bit is a flag, that we can **combine** with others. The following bits are defined (comments are mine):
74+
75+
```C++
76+
// Definition of WGPUBufferUsage bit flags values in webgpu.h
77+
78+
// The buffer can be *mapped* to be *read* on the CPU side
79+
static const WGPUBufferUsage WGPUBufferUsage_MapRead = 0x0000000000000001;
80+
81+
// The buffer can be *mapped* to be *written* on the CPU side
82+
static const WGPUBufferUsage WGPUBufferUsage_MapWrite = 0x0000000000000002;
83+
84+
// The buffer can be used as the *source* of a GPU-side copy operation
85+
static const WGPUBufferUsage WGPUBufferUsage_CopySrc = 0x0000000000000004;
86+
87+
// The buffer can be used as the *destination* of a GPU-side copy operation
88+
static const WGPUBufferUsage WGPUBufferUsage_CopyDst = 0x0000000000000008;
89+
90+
// The buffer can be used as an Index buffer when doing indexed drawing in a render pipeline
91+
static const WGPUBufferUsage WGPUBufferUsage_Index = 0x0000000000000010;
92+
93+
// The buffer can be used as an Vertex buffer when using a render pipeline
94+
static const WGPUBufferUsage WGPUBufferUsage_Vertex = 0x0000000000000020;
95+
96+
// The buffer can be bound to a shader as a uniform buffer
97+
static const WGPUBufferUsage WGPUBufferUsage_Uniform = 0x0000000000000040;
98+
99+
// The buffer can be bound to a shader as a storage buffer
100+
static const WGPUBufferUsage WGPUBufferUsage_Storage = 0x0000000000000080;
101+
102+
// The buffer can store arguments for an indirect draw call
103+
static const WGPUBufferUsage WGPUBufferUsage_Indirect = 0x0000000000000100;
104+
105+
// The buffer can store the result of a timestamp or occlusion query
106+
static const WGPUBufferUsage WGPUBufferUsage_QueryResolve = 0x0000000000000200;
107+
```
108+
109+
```{note}
110+
**Only the first usages** make sens to us for now, we will progressively discover what the others mean in later chapters.
111+
```
112+
113+
The **rule of thumb** is simple: **only specify the flags you really need**. If you miss one, WebGPU will complain with a message that should hint you about the missing usage flag.
114+
115+
At this point, we need a little scenario for our example, so that we can determine a usage.
116+
117+
Simple example
118+
--------------
119+
120+
Let us say we want to **create 2 buffers**, **write** data in the first one (`bufferA`), **copy** it into the second one (`bufferB`) on the GPU-side, and finally **read** `bufferB` back on the CPU.
121+
122+
So, we create a second buffer, with a second descriptor:
123+
124+
```{lit} C++, Create buffer B
125+
// We build a second buffer, called B
126+
WGPUBufferDescriptor bufferDescB = WGPU_BUFFER_DESCRIPTOR_INIT;
127+
{{Fill in buffer descriptor B}}
128+
WGPUBuffer bufferB = wgpuDeviceCreateBuffer(device, &bufferDescB);
129+
```
130+
131+
````{note}
132+
We create buffers **before the command encoding test** from previous chapter:
133+
134+
```{lit} C++, Create things (append)
135+
// Before encoding commands:
136+
{{Create buffer A}}
137+
{{Create buffer B}}
138+
{{Write initial value in Buffer A}}
139+
```
140+
````
141+
142+
To makes things **slightly more interesting**, I will make buffer B shorter than buffer A, so that **we only copy a slice** of buffer A into buffer B.
143+
144+
```{lit} C++, Fill in buffer descriptor B
145+
// buffer B is shorter than buffer A in this example
146+
bufferDescB.size = 32;
147+
```
148+
149+
```{note}
150+
We could also **reuse** the same `WGPUBufferDescriptor` struct for both buffer creations, but using a separate one is clearer for the context of this guide.
151+
```
152+
153+
### Usage
154+
155+
Back to the question of usage, we now know what to specify:
156+
157+
```{lit} C++, Fill in buffer descriptor A (append)
158+
// Buffer A is *written* on CPU, and used as *source* of a GPU-side copy
159+
bufferDescA.usage = WGPUBufferUsage_MapWrite | WGPUBufferUsage_CopySrc;
160+
```
161+
162+
```{lit} C++, Fill in buffer descriptor B (append)
163+
// Buffer B is *read* on CPU, and used as *destination* of a GPU-side copy
164+
bufferDescB.usage = WGPUBufferUsage_MapRead | WGPUBufferUsage_CopyDst;
165+
```
166+
167+
Note how the **pipe operator** (`|`, also called *bitwise OR*) is used to combine the usage flags together.
168+
169+
### Labels
170+
171+
We could stop here with descriptors: specifying a **byte size** and a **usage** is all the device requires to allocate a buffer.
172+
173+
It is however **good practice** to take benefit from the possibility to **name** our objects, especially now that we have 2 objects of the same nature (2 buffers). This greatly **helps understanding error messages**.
174+
175+
```{lit} C++, Fill in buffer descriptor A (append)
176+
bufferDescA.label = toWgpuStringView("Buffer A");
177+
```
178+
179+
```{lit} C++, Fill in buffer descriptor B (append)
180+
bufferDescB.label = toWgpuStringView("Buffer B");
181+
```
182+
183+
### Initial mapping state
184+
185+
There is one **last field** in the buffer descriptor which tells whether the buffer is *mapped* to the CPU side upon its creation. **Mapping a buffer** means to make it temporarily available on the CPU side, although it is a GPU buffer.
186+
187+
The `mappedAtCreation` field is of course only relevant for buffers that have declared a **mapping usage** (`MapWrite` or `MapRead`), and is only really useful for the `MapWrite` case.
188+
189+
In order to write the initial value of our `bufferA`, we are interested in having it mapped at creation:
190+
191+
```{lit} C++, Fill in buffer descriptor A (append)
192+
bufferDescA.mappedAtCreation = true;
193+
```
194+
195+
```{note}
196+
Technically, the field `mappedAtCreation` has type `WGPUBool`, which is actually a `uint32_t` and not a `bool`. The **boolean** type is not a built-in type of C and it sometimes induces special behaviors in C++ (e.g., `std::vector<bool>` is something special) so it is usually not used in C APIs. Use `1` instead of `true` if you compiler complains.
197+
198+
In newer versions of WebGPU, macros `WGPU_TRUE` and `WGPU_FALSE` are defined to clarify this.
199+
```
200+
201+
### Freeing memory
202+
203+
If you ever played with manual allocation using `malloc` or `new`, you must know that **we must always free our dynamically allocated memory**.
204+
205+
In the case of `WGPUBuffer`, its associated memory in VRAM is freed as soon as all references to it are released. We thus simply release our buffers:
206+
207+
```{lit} C++, Release things (prepend)
208+
wgpuBufferRelease(bufferA);
209+
wgpuBufferRelease(bufferB);
210+
```
211+
212+
Sometimes, we want to **force freeing VRAM memory** even if there may **remain references** to our buffers somewhere else in our program (for instance through a **bind group**, an object that we will discover later). For this, we may call `wgpuBufferDestroy(buffer)`. Other references to the buffer are then no longer usable.
213+
214+
Copying buffers
215+
---------------
216+
217+
Our GPU buffers are allocated in VRAM, we can now proceed with the GPU-side copy operation.
218+
219+
### Initial value
220+
221+
Before copying, we need something to copy, so we will **set the initial value of `bufferA`**. Given that we had it **mapped at creation**, we can get **the address in RAM** where it is mapped and simply write in there!
222+
223+
To get this address, we use the following function:
224+
225+
```C++
226+
// The signature of the wgpuBufferGetMappedRange function as it is in webgpu.h
227+
void * wgpuBufferGetMappedRange(WGPUBuffer buffer, size_t offset, size_t size);
228+
```
229+
230+
As you can see, this can be used to map only a sub range of the buffer, but in our case we want to **map the whole buffer**, so we can use the special sentinel value `WGPU_WHOLE_MAP_SIZE` to mean exactly that:
231+
232+
```{lit} C++, Write initial value in Buffer A
233+
uint8_t* bufferDataA = (uint8_t*)wgpuBufferGetMappedRange(bufferA, 0, WGPU_WHOLE_MAP_SIZE);
234+
```
235+
236+
Note that I also **cast** the returned `void*` pointer into something we can actually write to, e.g., a `uint8_t` in this example. We can now simply write whatever we want:
237+
238+
```{lit} C++, Write initial value in Buffer A (append)
239+
// Write 0, 1, 2, 3, ... in bufferA
240+
for (int i = 0 ; i < 256 ; ++i) {
241+
bufferDataA[i] = static_cast<uint8_t>(i);
242+
}
243+
```
244+
245+
```{caution}
246+
In this simple example, we write elements of type `uint8_t` whose **byte size is exactly 1** (and can have values up to 255). If we would use a **larger type** we would need to adapt the byte size of the buffer. For instance, we should allocate `256 * sizeof(int)` bytes if we want our buffer to store 256 integers.
247+
```
248+
249+
**Importantly**, we must **unmap** the buffer once we no longer need it on the CPU side, **before doing anything else** with it!
250+
251+
```{lit} C++, Write initial value in Buffer A (append)
252+
wgpuBufferUnmap(bufferA);
253+
// Do NOT use bufferDataA beyond this point!
254+
```
255+
256+
```{note}
257+
There are **other ways** to set the initial value of a buffer. If you want to **copy from another CPU buffer**, you can use [`wgpuBufferWriteMappedRange`](https://webgpu-native.github.io/webgpu-headers/group__WGPUBufferMethods.html#ga77b3a18397655692488536b0e4186de7) when the buffer is mapped, or even [`wgpuQueueWriteBuffer`](https://webgpu-native.github.io/webgpu-headers/group__WGPUQueueMethods.html#gaaa2dfc15dc7497ea8e9011f940f4dcf0), which only requires usage `CopyDst`.
258+
```
259+
260+
### Copy operation
261+
262+
**WIP** *In this version of the guide, this chapter moves back in the "getting started" section, between command queue and the first (compute) shader.*
263+
264+
```C++
265+
void wgpuCommandEncoderCopyBufferToBuffer(
266+
WGPUCommandEncoder commandEncoder,
267+
WGPUBuffer source,
268+
uint64_t sourceOffset,
269+
WGPUBuffer destination,
270+
uint64_t destinationOffset,
271+
uint64_t size
272+
);
273+
```
274+
275+
```{lit} C++, Add commands (replace)
276+
wgpuCommandEncoderCopyBufferToBuffer(
277+
encoder,
278+
bufferA,
279+
16,
280+
bufferB,
281+
0,
282+
bufferDescB.size
283+
);
284+
```
285+
286+
TODO: FIGURE!
287+
288+
TODO: LIMITS!
289+
290+
Troubleshooting
291+
---------------
10292

11-
Buffers
12-
-------
293+
In this section, we intentionally create errors 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.
13294

14295
Asynchronous operations
15296
-----------------------
@@ -37,3 +318,4 @@ uint64_t timeoutNS = 200 * 1000; // 200 ms
37318
WGPUWaitStatus status = wgpuInstanceWaitAny(instance, 1, &adapterRequest, timeoutNS);
38319
```
39320

321+
*Resulting code:* [`step017-next`](https://github.com/eliemichel/LearnWebGPU-Code/tree/step017-next)

0 commit comments

Comments
 (0)