-
Notifications
You must be signed in to change notification settings - Fork 186
Encapsulating Graphics Work
Whether you are designing a library to be used by others or modularizing your code, it is important to be able to encapsulate graphics code in a way that offers the user code as much flexibility as possible.
The following pattern I call the "middleware pattern" and works well both in separate libraries and just regular modules.
Middleware is a piece of software that fits unintrusively into an existing application, giving some extra functionality. In the case of wgpu, middleware are libraries that use the wgpu context that the user provides to do their work. If a library creates the wgpu adapter, device, etc for you, it isn't middleware, but would more likely be called a framework. A partial list of existing wgpu middleware is available on the Applications and Libraries page.
There are two main api patterns that make up the middleware pattern, external and internal render data.
This does not have to be the extent of the api, you may have more (or different) arguments or more functions, but this is the jist of the core of the interactions with wgpu.
impl MiddlewareRenderer {
fn new(&Device, &TextureFormat, ..) -> Self;
// Prepare for rendering, create all resources used during render, storing render data in PerFrameData
fn prepare(&self, ..) -> PerFrameData;
// Render using data in PerFrameData and user provided renderpass
fn render<'rpass>(&'rpass self, &mut RenderPass<'pass>, &'rpass PerFrameData);
}
impl MiddlewareRenderer {
fn new(&Device, &TextureFormat, ..) -> Self;
// Prepare for rendering, create all resources used during render, storing render data internally
fn prepare(&mut self, ..);
// Render using internal data and user provided renderpass
fn render<'rpass>(&'rpass mut self, &mut RenderPass<'pass>);
}
The goal of this api is to use a fewest possible render passes as possible. Reducing render passes is a very important for lower end hardware. These GPUs use a method called "tiled rendering" where there is significant cost to ending a render pass.
Middleware should not call queue.submit
unless absolutely necessary. It is an extremely expensive function and should only be called once per frame. If the middleware generates a CommandBuffer, hand that buffer back to the user to submit themselves.
fn new(&Device, &TextureFormat, ..) -> Self;
This is where you create your renderer and set up all the static resources. Things like pipelines, buffers, or textures should be created and uploaded here. Favor accepting a TextureFormat
, width
, and height
over a SwapchainDescriptor
, as the user may not be rendering to the swapchain image.
fn prepare(&self, ..) -> PerFrameData;
fn prepare(&mut self, ..);
The split between prepare and render is extremely important. Because render passes need all data they use to last as long as they do, all resources that are going to be used in the render pass need to be created ahead of time. By having a prepare function that gets everything ready ahead of time, the data (stored in either self
or PerFrameData
) will live the right amount of time. This also allows the user to control where exactly they want your middleware to do its work.
While both methods are valid, having external data is a way to encode the need for prepare
to be called before render
directly in the api. This makes it harder to use the api improperly. At the same time, there are many situations where no resources have to be recreated, but only modified. In this case it would make sense to have internal data.
Ideally there should be a minimal amount of resources created per frame, but that is often hard to avoid.
fn render<'rpass>(&'rpass self, &mut RenderPass<'pass>, &'rpass PerFrameData);
fn render<'rpass>(&'rpass self, &mut RenderPass<'pass>);
This is where the magic happens! Using the render data created during prepare
, render everything using the provided render pass. All data is guaranteed to live long enough by the lifetimes.
Note that the &mut
on the render pass is not the same lifetime. It is general advice that &'a mut Thing<'a>
is always wrong, but specifically we want to say that things are going to be flowing into the render pass, not that our borrow of the render pass needs to last as long as the render pass does.
If your piece of middleware has to render to multiple targets, it is pretty unavoidable to have multiple render targets. As much as is possible, this pattern should be used as a guideline for the design of your api, but it doesn't work for every possible piece of middleware out there.
Todo...
Todo...