-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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 unobtrusively 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.
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 gist of the core of the interactions with wgpu.
impl MiddlewareRenderer {
fn new(&Device, &TextureFormat, ..) -> Self;
// Prepare for rendering, create all resources used during render
fn prepare(&mut self, ..);
// Render using user provided renderpass
fn render(&self, &mut RenderPass<'_>);
}
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(&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 will live the right amount of time. This also allows the user to control where exactly they want your middleware to do its work.
Ideally there should be a minimal amount of resources created per frame, but that is often hard to avoid.
fn render(&self, &mut RenderPass<'_>);
This is where the magic happens! Using the render data created during prepare
, render everything using the provided render pass.
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...