Skip to content

Commit 329c0e6

Browse files
Kangzkainino0x
andauthored
Explainer: Object validity and destroyedness (#1595)
* Explainer: Object validity and destroyedness * copyedit, move img into repo * run optipng, 52k -> 16k Co-authored-by: Kai Ninomiya <kainino@chromium.org>
1 parent 91ae1c2 commit 329c0e6

2 files changed

Lines changed: 142 additions & 4 deletions

File tree

15.6 KB
Loading

explainer/index.bs

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,152 @@ See [Introduction](https://gpuweb.github.io/gpuweb/#introduction).
2626

2727
See [Malicious use considerations](https://gpuweb.github.io/gpuweb/#malicious-use).
2828

29+
# Additional Background # {#background}
30+
31+
## Sandboxed GPU Processes in Web Browsers ## {#gpu-process}
32+
33+
A major design constraint for WebGPU is that it must be implementable and efficient in browsers that use a GPU-process architecture.
34+
GPU drivers need access to additional kernel syscalls than what's otherwise used for Web content, and many GPU drivers are prone to hangs or crashes.
35+
To improve stability and sandboxing, browsers use a special process that contains the GPU driver and talks with the rest of the browser through asynchronous IPC.
36+
GPU processes are (or will be) used in Chromium, Gecko, and WebKit.
37+
38+
GPU processes are less sandboxed than content processes, and they are typically shared between multiple origins.
39+
Therefore, they must validate all messages, for example to prevent a compromised content process from being able to look at the GPU memory used by another content process.
40+
Most of WebGPU's validation rules are necessary to ensure it is secure to use, so all the validation needs to happen in the GPU process.
41+
42+
Likewise, all GPU driver objects only live in the GPU process, including large allocations (like buffers and textures) and complex objects (like pipelines).
43+
In the content process, WebGPU types (`GPUBuffer`, `GPUTexture`, `GPURenderPipeline`, ...) are mostly just "handles" that identify objects that live in the GPU process.
44+
This means that the CPU and GPU memory used by WebGPU object isn't necessarily known in the content process.
45+
A `GPUBuffer` object can use maybe 150 bytes of CPU memory in the content process but hold a 1GB allocation of GPU memory.
46+
47+
See also the description of [the content and device timelines in the specification](https://gpuweb.github.io/gpuweb/#programming-model-timelines).
48+
2949
# JavaScript API # {#api}
3050

3151
## Bitflags ## {#bitflags}
3252

53+
## Object Validity and Destroyed-ness ## {#invalid-and-destroyed}
54+
55+
### WebGPU's Error Monad ### {#error-monad}
56+
57+
A.k.a. Contagious Internal Nullability.
58+
A.k.a. transparent [promise pipelining](http://erights.org/elib/distrib/pipeline.html).
59+
60+
WebGPU is a very chatty API, with some applications making tens of thousands of calls per frame to render complex scenes.
61+
We have seen that the GPU processes needs to validate the commands to satisfy their security property.
62+
To avoid the overhead of validating commands twice in both the GPU and content process, WebGPU is designed so Javascript calls can be forwarded directly to the GPU process and validated there.
63+
See the error section for more details on what's validated where and how errors are reported.
64+
65+
At the same time, during a single frame WebGPU objects can be created that depend on one another.
66+
For example a `GPUCommandBuffer` can be recorded with commands that use temporary `GPUBuffer`s created in the same frame.
67+
In this example, because of the performance constraint of WebGPU, it is not possible to send the message to create the `GPUBuffer` to the GPU process and synchronously wait for its processing before continuing Javascript execution.
68+
69+
Instead, in WebGPU all objects (like `GPUBuffer`) are created immediately on the content timeline and returned to JavaScript.
70+
The validation is almost all done asynchronously on the "device timeline".
71+
In the good case, when no errors occur (validation or out-of-memory), everything looks to JS as if it is synchronous.
72+
However, when an error occurs in a call, it becomes a no-op (aside from error reporting).
73+
If the call returns an object (like `createBuffer`), the object is tagged as "invalid" on the GPU process side.
74+
75+
All WebGPU calls validate that all their arguments are valid objects.
76+
As a result, if a call takes one WebGPU object and returns a new one, the new object is also invalid (hence the term "contagious").
77+
78+
<figure>
79+
<figcaption>
80+
Timeline diagram of messages passing between processes, demonstrating how errors are propagated without synchronization.
81+
</figcaption>
82+
<img alt="diagram" src="img/error_monad_timeline_diagram.png">
83+
</figure>
84+
85+
<div class=example>
86+
Using the API when doing only valid calls looks like a synchronous API:
87+
88+
<pre highlight="js">
89+
const srcBuffer = device.createBuffer({
90+
size: 4,
91+
usage: GPUBufferUsage.COPY_SRC
92+
});
93+
94+
const dstBuffer = ...;
95+
96+
const encoder = device.createCommandEncoder();
97+
encoder.copyBufferToBuffer(srcBuffer, 0, dstBuffer, 0, 4);
98+
99+
const commands = encoder.finish();
100+
device.queue.submit([commands]);
101+
</pre>
102+
</div>
103+
104+
<div class=example>
105+
Errors propagate contagiously when creating objects:
106+
107+
<pre highlight="js">
108+
// The size of the buffer is too big, this causes an OOM and srcBuffer is invalid.
109+
const srcBuffer = device.createBuffer({
110+
size: BIG_NUMBER,
111+
usage: GPUBufferUsage.COPY_SRC
112+
});
113+
114+
const dstBuffer = ...;
115+
116+
// The encoder starts as a valid object.
117+
const encoder = device.createCommandEncoder();
118+
// Special case: an invalid object is used when encoding commands so the encoder
119+
// becomes invalid.
120+
encoder.copyBufferToBuffer(srcBuffer, 0, dstBuffer, 0, 4);
121+
122+
// commands, the this argument to GPUCommandEncoder.finish is invalid
123+
// so the call returns an invalid object.
124+
const commands = encoder.finish();
125+
// The command references an invalid object so it becomes a noop.
126+
device.queue.submit([commands]);
127+
</pre>
128+
</div>
129+
130+
#### Mental Models #### {#error-monad-mental-model}
131+
132+
One way to interpret WebGPU's semantics is that every WebGPU object is actually a `Promise` internally and that all WebGPU methods are `async` and `await` before using each of the WebGPU objects it gets as argument.
133+
However the execution of the async code is outsourced to the GPU process (where it is actually done synchronously).
134+
135+
Another way, closer to actual implementation details, is to imagine that each `GPUFoo` JS object maps to a `gpu::InternalFoo` C++/Rust object on the GPU process that contains a `bool isValid`.
136+
Then during the validation of each command on the GPU process, the `isValid` are all checked and a new, invalid object is returned if validation fails.
137+
On the content process side, the `GPUFoo` implementation doesn't know if the object is valid or not.
138+
139+
### Early Destruction of WebGPU Objects ### {#early-destroy}
140+
141+
Most of the memory usage of WebGPU objects is in the GPU process: it can be GPU memory held by objects like `GPUBuffer` and `GPUTexture`, serialized commands held in CPU memory by `GPURenderBundles`, or complex object graphs for the WGSL AST in `GPUShaderModule`.
142+
The JavaScript garbage collector (GC) is in the renderer process and doesn't know about the memory usage in the GPU process.
143+
Browsers have many heuristics to trigger GCs but a common one is that it should be triggered on memory pressure scenarios.
144+
However a single WebGPU object can hold on to MBs or GBs of memory without the GC knowing and never trigger the memory pressure event.
145+
146+
It is important for WebGPU applications to be able to directly free the memory used by some WebGPU objects without waiting for the GC.
147+
For example applications might create temporary textures and buffers each frame and without the explicit `.destroy()` call they would quickly run out of GPU memory.
148+
That's why WebGPU has a `.destroy()` method on those object types which can hold on to arbitrary amount of memory.
149+
It signals that the application doesn't need the content of the object anymore and that it can be freed as soon as possible.
150+
Of course, it becomes a validation to use the object after the call to `.destroy()`.
151+
152+
<div class=example>
153+
<pre highlight="js">
154+
const dstBuffer = device.createBuffer({
155+
size: 4
156+
usage: GPUBufferUsage.COPY_DST
157+
});
158+
159+
// The buffer is not destroyed (and valid), success!
160+
device.queue.writeBuffer(dstBuffer, 0, myData);
161+
162+
buffer.destroy();
163+
164+
// The buffer is now destroyed, commands using that would use its
165+
// content produce validation errors.
166+
device.queue.writeBuffer(dstBuffer, 0, myData);
167+
</pre>
168+
</div>
169+
170+
Note that, while this looks somewhat similar to the behavior of an invalid buffer, it is distinct.
171+
Unlike invalidity, destroyed-ness can change after creation, is not contagious, and is validated only when work is actually submitted (e.g. `queue.writeBuffer()` or `queue.submit()`), not when creating dependent objects (like command encoders, see above).
172+
173+
## Errors ## {#errors}
174+
33175
## Adapter Selection and Device Init ## {#initialization}
34176

35177
## Adapter and Device Loss ## {#device-loss}
@@ -42,10 +184,6 @@ See [Malicious use considerations](https://gpuweb.github.io/gpuweb/#malicious-us
42184

43185
## Pipelines ## {#pipelines}
44186

45-
## Object Validity and Destroyed-ness ## {#invalid-and-destroyed}
46-
47-
## Errors ## {#errors}
48-
49187
## Image, Video, and Canvas input ## {#image-input}
50188

51189
## Canvas Output ## {#canvas-output}

0 commit comments

Comments
 (0)