Wgpu The Swapchain

First, some house keeping: State

For convenience we’re going to pack all the fields into a struct, and create some methods on that.

为了方便起见,我们将把所有字段打包到一个结构中,并在此基础上创建一些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// main.rs
use winit::window::Window;

struct State {
surface: wgpu::Surface,
device: wgpu::Device,
queue: wgpu::Queue,
sc_desc: wgpu::SwapChainDescriptor,
swap_chain: wgpu::SwapChain,
size: winit::dpi::PhysicalSize<u32>,
}

impl State {
// Creating some of the wgpu types requires async code
async fn new(window: &Window) -> Self {
todo!()
}

fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
todo!()
}

fn input(&mut self, event: &WindowEvent) -> bool {
todo!()
}

fn update(&mut self) {
todo!()
}

fn render(&mut self) -> Result<(), wgpu::SwapChainError> {
todo!()
}
}

I’m glossing over States fields, but they’ll make more sense as I explain the code behind the methods.

我对States字段进行了润色,但当我解释方法背后的代码时,它们会更有意义。

State::new()

The code for this is pretty straight forward, but let’s break this down a bit.

这方面的代码非常简单,但是让我们把它分解一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
impl State {
// ...
async fn new(window: &Window) -> Self {
let size = window.inner_size();

// The instance is a handle to our GPU
// BackendBit::PRIMARY => Vulkan + Metal + DX12 + Browser WebGPU
let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY);
let surface = unsafe { instance.create_surface(window) };
let adapter = instance.request_adapter(
&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
},
).await.unwrap();

The surface is used to create the swap_chain. Our window needs to implement raw-window-handle’s HasRawWindowHandle trait to access the native window implementation for wgpu to properly create the graphics backend. Fortunately, winit’s Window fits the bill. We also need it to request our adapter.

surface用于创建swap_chain。我们的窗口需要实现原始raw-window-handle’s HasRawWindowHandle特性,以便wgpu访问本机窗口实现,从而正确创建图形后端。幸运的是,winit的窗口符合要求。我们还需要它来请求适配器。

The options I’ve passed to request_adapter aren’t guaranteed to work for all devices, but will work for most of them. If you want to get all adapters for a particular backend you can use enumerate_adapters. This will give you an iterator that you can loop over to check if one of the adapters works for your needs.
我传递给request_adapter的选项不能保证对所有设备都有效,但对大多数设备都有效。如果要获取特定后端的所有适配器,可以使用enumerate_adapters。这将为您提供一个迭代器,您可以循环检查其中一个适配器是否满足您的需要。

1
2
3
4
5
6
7
8
let adapter = instance
.enumerate_adapters(wgpu::BackendBit::PRIMARY)
.filter(|adapter| {
// Check if this adapter supports our surface
adapter.get_swap_chain_preferred_format(&surface).is_some()
})
.first()
.unwrap()

For more fields you can use to refine you’re search check out the docs.
有关可用于优化搜索的更多字段,请查看文档

We need the adapter to create the device and queue.
我们需要适配器来创建设备和队列。

1
2
3
4
5
6
7
8
let (device, queue) = adapter.request_device(
&wgpu::DeviceDescriptor {
features: wgpu::Features::empty(),
limits: wgpu::Limits::default(),
label: None,
},
None, // Trace path
).await.unwrap();

The features field on DeviceDescriptor, allows us to specify what extra features we want. For this simple example, I’ve decided not to use any extra features.
DeviceDescriptor上的features字段允许我们指定所需的额外功能。对于这个简单的例子,我决定不使用任何额外的特性。

The device you have limits the features you can use. If you want to use certain features you may need to limit what devices you support, or provide work arounds.
You can get a list of features supported by your device using adapter.features(), or device.features().
You can view a full list of features here.

您拥有的设备限制了您可以使用的功能。如果您想使用某些功能,您可能需要限制您支持的设备,或提供解决方案。
您可以使用adapter.features()或device.features()获取设备支持的功能列表。
您可以在此处查看功能的完整列表

The limits field describes the limit of certain types of resource we can create. We’ll use the defaults for this tutorial, so we can support most devices. You can view a list of limits here.

limits字段描述了我们可以创建的特定类型资源的限制。本教程将使用默认设置,因此我们可以支持大多数设备。您可以在此处查看limits列表

1
2
3
4
5
6
7
8
let sc_desc = wgpu::SwapChainDescriptor {
usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
format: adapter.get_swap_chain_preferred_format(&surface).unwrap(),
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
};
let swap_chain = device.create_swap_chain(&surface, &sc_desc);

Here we are defining and creating the swap_chain. The usage field describes how the swap_chain’s underlying textures will be used. RENDER_ATTACHMENT specifies that the textures will be used to write to the screen (we’ll talk about more TextureUsages later).

这里我们定义并创建swap_chain。usage字段描述如何使用swap_chain的基础纹理。RENDER_ATTACHMENT指定纹理将用于写入屏幕(稍后我们将讨论更多纹理图像)。

The format defines how the swap_chains textures will be stored on the gpu. Different displays prefer different formats. We use adapter.get_swap_chain_preferred_format() to figure out the best format to use.

该格式定义如何将swap_chains纹理存储在gpu上。不同的显示器喜欢不同的格式。我们使用adapter.get_swap_chain_preferred_format()来找出最佳的使用格式。

width and height, are the width and height in pixels of the swap chain. This should usually be the width and height of the window.

宽度和高度是swap_chain的宽度和高度(以像素为单位)。这通常应该是窗口的宽度和高度。

The present_mode uses the wgpu::PresentMode enum which determines how to sync the swap chain with the display. You can see all the options in the docs

当前模式使用wgpu::PresentMode枚举,该枚举确定如何将swap_chain与显示同步。您可以在文档中看到所有选项

At the end of the method, we simply return the resulting struct.

在方法的末尾,我们只返回结构体结果。

1
2
3
4
5
6
7
8
9
10
11
        Self {
surface,
device,
queue,
sc_desc,
swap_chain,
size,
}
}
// ...
}

We’ll want to call this in our main method before we enter the event loop.

在进入事件循环之前,我们希望在main方法中调用它。

1
2
// Since main can't be async, we're going to need to block
let mut state = pollster::block_on(State::new(&window));

You can use heavier libraries like async_std and tokio to make main async, so you can await futures. I’ve elected not to use these crates as this tutorial is not about writing an async application, and the futures created by wgpu do not require special executor support. We just need some way to interact with wgpu’s async functions, and the pollster crate is enough for that.

您可以使用较重的库(如async_std和tokio)来实现main函数异步,因此您可以等待futures。我选择不使用这些crates,因为本教程不是关于编写异步应用程序的,而且wgpu创建的crates不需要特殊的执行器支持。我们只需要一些与wgpu的异步函数交互的方法,the pollster crate就足够了。

resize()

If we want to support resizing in our application, we’re going to need to recreate the swap_chain everytime the window’s size changes. That’s the reason we stored the physical size and the sc_desc used to create the swapchain. With all of these, the resize method is very simple.

如果我们想在应用程序中支持调整大小,我们需要在每次窗口大小更改时重新创建swap_chain。这就是我们存储用于创建swap_chain的物理大小和sc_desc的原因。有了所有这些,调整大小的方法非常简单。

1
2
3
4
5
6
7
8
9
// impl State
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.size = new_size;
self.sc_desc.width = new_size.width;
self.sc_desc.height = new_size.height;
self.swap_chain = self.device.create_swap_chain(&self.surface, &self.sc_desc);
}
}

There’s nothing really different here from creating the swap_chain initially, so I won’t get into it.

这里与最初创建swap_chain没有什么不同,所以我就不谈了。

We call this method in main() in the event loop for the following events.

对于以下事件,我们在main函数的事件循环的中调用此方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
match event {
// ...

} if window_id == window.id() => if !state.input(event) {
match event {
// ...

WindowEvent::Resized(physical_size) => {
state.resize(*physical_size);
}
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
// new_inner_size is &&mut so we have to dereference it twice
state.resize(**new_inner_size);
}
// ...
}

input()

input() returns a bool to indicate whether an event has been fully processed. If the method returns true, the main loop won’t process the event any further.

input函数返回一个bool以指示事件是否已被完全处理。如果该方法返回true,则主循环将不再进一步处理该事件。

We’re just going to return false for now because we don’t have any events we want to capture.

我们现在只返回false,因为我们没有任何要捕获的事件。

1
2
3
4
// impl State
fn input(&mut self, event: &WindowEvent) -> bool {
false
}

We need to do a little more work in the event loop. We want State to have priority over main(). Doing that (and previous changes) should have your loop looking like this.

我们需要在事件循环中做更多的工作。我们希望State优先于main()。这样做(以及之前的更改)应该使循环看起来像这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// main()
event_loop.run(move |event, _, control_flow| {
match event {
Event::WindowEvent {
ref event,
window_id,
} if window_id == window.id() => if !state.input(event) { // UPDATED!
match event {
WindowEvent::CloseRequested
| WindowEvent::KeyboardInput {
input:
KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(VirtualKeyCode::Escape),
..
},
..
} => *control_flow = ControlFlow::Exit,
WindowEvent::Resized(physical_size) => {
state.resize(*physical_size);
}
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
state.resize(**new_inner_size);
}
_ => {}
}
}
_ => {}
}
});

update()

We don’t have anything to update yet, so leave the method empty.

1
2
3
fn update(&mut self) {
// remove `todo!()`
}

render()

Here’s where the magic happens. First we need to get a frame to render to. This will include a wgpu::Texture and wgpu::TextureView that will hold the actual image we’re drawing to (we’ll cover this more when we talk about textures).

这就是魔法发生的地方。首先,我们需要得到一个帧来渲染。这将包括wgpu::Texture和wgpu::TextureView,它们将保存我们正在绘制的实际图像(我们将在讨论纹理时详细介绍这一点)。

1
2
3
4
5
6
7
// impl State

fn render(&mut self) -> Result<(), wgpu::SwapChainError> {
let frame = self
.swap_chain
.get_current_frame()?
.output;

We also need to create a CommandEncoder to create the actual commands to send to the gpu. Most modern graphics frameworks expect commands to be stored in a command buffer before being sent to the gpu. The encoder builds a command buffer that we can then send to the gpu.

我们还需要创建CommandEncoder来创建发送到gpu的实际命令。大多数现代图形框架都希望命令在发送到gpu之前存储在命令缓冲区中。编码器构建一个命令缓冲区,然后我们可以将其发送到gpu。

1
2
3
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});

Now we can actually get to clearing the screen (long time coming). We need to use the encoder to create a RenderPass. The RenderPass has all the methods to do the actual drawing. The code for creating a RenderPass is a bit nested, so I’ll copy it all here, and talk about the pieces.

现在我们可以开始清理屏幕了(很长一段时间就要到了)。我们需要使用编码器来创建一个RenderPass。RenderPass具有进行实际绘制的所有方法。创建RenderPass的代码有点嵌套,所以我将在这里全部复制,并讨论各个部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    {
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[
wgpu::RenderPassColorAttachment {
view: &frame.view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: true,
}
}
],
depth_stencil_attachment: None,
});
}

// submit will accept anything that implements IntoIter
self.queue.submit(std::iter::once(encoder.finish()));

Ok(())
}

First things first, let’s talk about the {}. encoder.begin_render_pass(…) borrows encoder mutably (aka &mut self). We can’t call encoder.finish() until we release that mutable borrow. The {} around encoder.begin_render_pass(…) tells rust to drop any variables within them when the code leaves that scope thus releasing the mutable borrow on encoder and allowing us to finish() it. If you don’t like the {}, you can also use drop(render_pass) to achieve the same effect.

首先,让我们先谈谈{}包括的代码。 encoder.begin_render_pass(…)使encoder可变(aka &mut self)。在释放该可变借用之前,我们无法调用encoder.finish()。使用{}包括encoder.begin_render_pass(…)告诉rust在代码离开该范围时删除其中的任何变量,从而释放借用的可变编码器,并允许我们完成它。如果不喜欢{},也可以使用drop(render_pass)来实现相同的效果。

We can get the same results by removing the {}, and the let _render_pass = line, but we need access to the _render_pass in the next tutorial, so we’ll leave it as is.

我们可以通过删除{}和let _render_pass=line来获得相同的结果,但是我们需要在下一个教程中访问_render_pass,所以我们将保持原样。

The last lines of the code tell wgpu to finish the command buffer, and to submit it to the gpu’s render queue.

代码的最后几行告诉wgpu完成命令缓冲区,并将其提交到gpu的渲染队列。

We need to update the event loop again to call this method. We’ll also call update before it too.

我们需要再次更新事件循环以调用此方法。我们也会在更新之前调用update。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main()
event_loop.run(move |event, _, control_flow| {
match event {
// ...
Event::RedrawRequested(_) => {
state.update();
match state.render() {
Ok(_) => {}
// Recreate the swap_chain if lost
Err(wgpu::SwapChainError::Lost) => state.resize(state.size),
// The system is out of memory, we should probably quit
Err(wgpu::SwapChainError::OutOfMemory) => *control_flow = ControlFlow::Exit,
// All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e),
}
}
Event::MainEventsCleared => {
// RedrawRequested will only trigger once, unless we manually
// request it.
window.request_redraw();
}
// ...
}
});

Wait, what’s going on with RenderPassDescriptor?

Some of you may be able to tell what’s going on just by looking at it, but I’d be remiss if I didn’t go over it. Let’s take a look at the code again.

你们中的一些人可能仅仅通过看它就能知道发生了什么,但如果我不仔细看一下,那我就是失职了。让我们再看一遍代码。

1
2
3
4
5
6
7
&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[
// ...
],
depth_stencil_attachment: None,
}

A RenderPassDescriptor only has three fields: label, color_attachments and depth_stencil_attachment. The color_attachements describe where we are going to draw our color to. We’ll use depth_stencil_attachment later, but we’ll set it to None for now.

RenderPassDescriptor只有三个字段:label、color_attachments和depth_stencil_attachment。color_attachments描述了我们将在何处绘制颜色。稍后我们将使用depth_stencil_attachment,但现在将其设置为None。

1
2
3
4
5
6
7
8
9
10
11
12
13
wgpu::RenderPassColorAttachment {
view: &frame.view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}),
store: true,
}
}

The RenderPassColorAttachment has the view field which informs wgpu what texture to save the colors to. In this case we specify frame.view that we created using swap_chain.get_current_frame(). This means that any colors we draw to this attachment will get drawn to the screen.

RenderPassColorAttachment有一个视图字段,用于通知wgpu要保存颜色的纹理。在本例中,我们指定使用swap_chain.get_current_frame()创建的frame.view。这意味着我们在此附件中绘制的任何颜色都将被绘制到屏幕上。

The resolve_target is the texture that will receive the resolved output. This will be the same as attachment unless multisampling is enabled. We don’t need to specify this, so we leave it as None.

resolve_target是将接收解析输出的纹理。除非启用了多重采样,否则这将与attachment相同。我们不需要指定它,所以我们将其保留为None。

The ops field takes a wpgu::Operations object. This tells wgpu what to do with the colors on the screen (specified by frame.view). The load field tells wgpu how to handle colors stored from the previous frame. Currently we are clearing the screen with a bluish color.

ops字段接受一个wpgu::Operations对象。这告诉wgpu如何处理屏幕上的颜色(由frame.view指定)。load字段告诉wgpu如何处理从上一帧存储的颜色。目前,我们正在清除带有蓝色的屏幕。

Challenge

Modify the input() method to capture mouse events, and update the clear color using that. Hint: you’ll probably need to use WindowEvent::CursorMoved.

修改input()方法以捕获鼠标事件,并使用该方法更新清除颜色。提示:您可能需要使用WindowEvent::CursorMoved。

Check out the code!