Round Egg 2: Setup and wgpu
2023-06-01
Update: The original idea of the project was to make our own custom render loop. At some point later down the line we discarded that idea. This post explores our initial attempt at understanding wgpu's inner workings.
Throughout the project, we have gone back and forth in our implementation decisions, for example, we have later switched to nightly rust. Furthermore we have gone through several major Bevy releases.
We commence by referring to the wgpu
documentation. It explains how we can hook into winit
's
event loop, and use env_logger
for the purpose of logging within wgpu.
pub fn run() {
env_logger::init(); // Necessary for logging within WGPU
let event_loop = EventLoop::new(); // Loop provided by winit for handling window events
let window = WindowBuilder::new().build(&event_loop).unwrap();
// ...
}
Then we need to initialize the wgpu backend, this by doing so we can specify the requirements of the platform.
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
dx12_shader_compiler: wgpu::Dx12Compiler::Dxc {
dxc_path: None,
dxil_path: None,
},
});
We also need to create a few other resources, a surface and an adapter (we
utilize pollster
to block waiting for the adapter)
let surface = unsafe { instance.create_surface(&window) }.unwrap();
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
}))
.unwrap();
request_device
grants us a device
which will manage the GPU resources such as buffers or textures, and a queue
associated with it. The queue will be our way to submit rendering commands to the GPU.
let (device, queue) = pollster::block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
limits: wgpu::Limits::default(),
},
None, // Trace path
))
.unwrap();
Finally we configure our surface. We don't particularly care about the settings right now, we just want to be able to render something (anything).
let size = window.inner_size();
surface.configure(
&device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: wgpu::TextureFormat::Bgra8Unorm, /* BGRA is the most widely supported format */
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: wgpu::CompositeAlphaMode::Opaque,
view_formats: Vec::<wgpu::TextureFormat>::new(),
},
);
That is it for the setup boilerplate.
We still need one more bit, the event loop. As we mentioned we can hook up to
winit
's event loop.
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
// ...
}
Event::RedrawRequested(_) => {
// ...
}
_ => (),
}
Mainly we want to redirect winit's redraw events to wgpu, and trigger a render pass. For now we will simply draw a plain green image.
match event {
// ...
Event::RedrawRequested(_) => {
// ...
let output = surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
// Create and configure the render pass
{
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.9,
b: 0.3,
a: 1.0,
}),
store: true,
},
})],
depth_stencil_attachment: None,
});
}
// Submit the rendering command
queue.submit(std::iter::once(encoder.finish()));
output.present();
}
_ => (),
}
We can finally run our code and check that everything works. This is one of these cases where rust's type system actually came in very helpful. The whole time it felt like putting pieces of a puzzle together, as opposed to hopping that the order in we are calling functions in the correct one and nothing explodes.
Now the only thing left is a big refactor, we can create a State
object that
will hold the rendering state of our program.
pub struct RenderingState {
pub(crate) surface: wgpu::Surface,
pub(crate) device: wgpu::Device,
pub(crate) queue: wgpu::Queue,
pub(crate) config: wgpu::SurfaceConfiguration,
pub(crate) size: winit::dpi::PhysicalSize<u32>,
pub(crate) window: winit::window::Window,
}
Finally we also chose to replace
pollster
withtokio
. This was always the plan but we were using the former for convenience.
We made some attempts at making the code compile to Web assembly, however we quickly realized that it would add some extra complexity which was out of the scope of the project.
For example, WASM does not fully support async - as the browser is a single threaded
environment. Though there exist libraries such as wasm-bindgen-futures
which convert Rust's
Futures into Promises.
At the end, our main function looks something like this.
#[tokio::main]
async fn main() {
let (window, event_loop) = init_window();
let state = State::new(window).await;
event_loop.run(event_handler(state));
}