-
Notifications
You must be signed in to change notification settings - Fork 344
Description
We've heard from the Metal team that aliasing render targets is important for memory use.
The Problem
Imagine a WebGPU application that renders to a texture, then reads from that texture to render into another texture, then reads from that other texture to render into yet another texture. Long chains like this are common in games with post-processing.
The flow would look like the following:
[ ]
| Renders into
V
Texture A
|
| Renders into
V
Texture B
|
| Renders into
V
Texture C
|
| Renders into
V
The screen
With the current design, the application code would look like:
let textureA = device.createTexture();
let textureB = device.createTexture();
let textureC = device.createTexture();
let targetA = createRenderPassDescriptor(textureA);
commandEncoder.beginRenderPass(targetA);
commandEncoder.draw();
commandEncoder.endRenderPass();
let targetB = createRenderPassDescriptor(textureB);
commandEncoder.beginRenderPass(targetB);
commandEncoder.attachResource(textureA);
commandEncoder.draw();
commandEncoder.endRenderPass();
let targetC = createRenderPassDescriptor(textureC);
commandEncoder.beginRenderPass(targetC);
commandEncoder.attachResource(textureB);
commandEncoder.draw();
commandEncoder.endRenderPass();
commandEncoder.attachResource(theScreen);
commandEncoder.attachResource(textureC);
commandEncoder.draw();
commandEncoder.endRenderPass();
queue.submit([commandEncoder]);
This approach means that textures A and C both exist in memory at the same time. However; they don't need to - they are never accessed at the same time. In fact, they should be able to live in the same region of memory.
This is important for mobile devices because they usually have big and fairly high density screens, but not that much memory. Therefore, wasting memory as big as a frame buffer is quite unfortunate.
An application often can't simply reuse the same resource in multiple places in their frame (meaning: textures A and C usually can't literally be the same resource), because the resources may be required to be different sizes or formats.
Solution A: The Vulkan-Subpass Metaphor
One way to solve this problem is for the application to submit to us a plan of what the inputs and outputs of every future render pass will be so that the browser can figure out which targets can be aliased. The application would have to do this at the beginning of each frame.
This approach is unfortunate for a few reasons:
- This is how Vulkan subpasses work, and we have heard from IHVs that applications usually don't use these facilities. We can only assume that lack of adoption wasn't an accident.
- A scene graph renderer may not know ahead of time exactly how the frame will be laid out. For example, upon encountering a particular node, the engine may realize that they need a temporary buffer (e.g. to blur something).
- Figuring out how to alias render targets is essentially the graph coloring problem, which is NP-complete. We don't want the browser to have to do this analysis.
Solution B: Heaps
This solution would look like
interface WebGPUTextureHeap {
WebGPUTexture createTexture(WebGPUTextureDescriptor descriptor);
}
partial interface WebGPUDevice {
WebGPUTextureHeap createTextureHeap();
}And the application code would look like
let heap = device.createTextureHeap();
let textureA = heap.createTexture();
let textureB = device.createTexture(); // This one comes from the device because it shouldn't be shared with anything
let textureC = heap.createTexture();
[and the rest is all the same]
Here, any textures created from the same heap would be able to alias. This is effectively making the programmer provide the solution to the graph coloring problem at resource creation time.
However, this is unfortunate because a particular coloring is relevant to a particular frame, not a particular resource. Frame plans change over time as the scene graph changes, and forcing a particular coloring for the lifetime of the entire resource would lead to suboptimal utilization.
Solution C: Purgability
Recall that the runtime (browser) needs to be able to evict resources due to OS pressure without notifying the running WebGPU program. This means that a WebGPU texture isn't pinned to a specific address, and may be relocated by the runtime at any time, without the application knowing.
Therefore, the problem isn't about "aliasing," per se. A browser can freely move resources around wherever it wants, and if it moves a resource on top of another resource, the other resource's contents gets clobbered. If the application wasn't using that resource, then there is no problem.
In the example above, the only reason the browser has to keep all the resources around at the same time is because the application may suddenly decide to use all the textures as inputs in one additional draw call. Therefore, the browser doesn't know that a resource is able to be clobbered.
So, this solution would simply add a function on WebGPUTexture:
partial interface WebGPUTexture {
void purge();
}This lets the browser know that the application is done using a resource. A purged resource will be guaranteed to have all its contents reset to 0s, and, therefore, the browser is free to place other resources on top of it.
A purged resource may be used, just like normal, at any point after that, but doing so will un-purge the resource. Un-purging a resource forces the browser to move any resources located on top of it.
There is no undefined behavior because purging a resource will fill it with 0s at the time it's next read, even if no other resource was relocated on top of it.
So, using purgeability, the above example would become:
let textureA = device.createTexture();
let textureB = device.createTexture();
let textureC = device.createTexture();
...
// At the beginning, everything is purged
let targetA = createRenderPassDescriptor(textureA);
commandEncoder.beginRenderPass(targetA); // Un-purges target A
commandEncoder.draw();
commandEncoder.endRenderPass();
let targetB = createRenderPassDescriptor(textureB);
commandEncoder.beginRenderPass(targetB); // Un-purges target B
commandEncoder.attachResource(textureA);
commandEncoder.draw();
commandEncoder.endRenderPass();
textureA.purge();
let targetC = createRenderPassDescriptor(textureC);
commandEncoder.beginRenderPass(targetC); // Un-purges target C
commandEncoder.attachResource(textureB);
commandEncoder.draw();
commandEncoder.endRenderPass();
textureB.purge();
commandEncoder.attachResource(theScreen);
commandEncoder.attachResource(textureC);
commandEncoder.draw();
commandEncoder.endRenderPass();
textureC.purge();
queue.submit([commandEncoder]);
Notice that textures A and C are never un-purged at the same time, so the implementation is free to make them alias. However, if some surprising draw call comes along and uses them both at once, the implementation will happily de-alias them and continue on.