tutorial2-swapchain

pull/1/head
Ben Hansen 5 years ago
parent 8ecb859b9f
commit 121037a7c2

11
Cargo.lock generated

@ -144,7 +144,7 @@ name = "code"
version = "0.1.0"
dependencies = [
"image 0.22.3 (registry+https://github.com/rust-lang/crates.io-index)",
"raw-window-handle 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"raw-window-handle 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"wgpu 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winit 0.20.0-alpha3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -755,14 +755,6 @@ dependencies = [
"libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "raw-window-handle"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rayon"
version = "1.2.0"
@ -1316,7 +1308,6 @@ dependencies = [
"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
"checksum raw-window-handle 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "af3d3b2e1053b3ff2171efc29a8bff3439ce6b2ce6a0432695134bc1c7ff8e87"
"checksum raw-window-handle 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e815b85b31e4d397ca9dd8eb1d692e9cb458b9f6ae8ac2232c995dca8236f87"
"checksum rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83a27732a533a1be0a0035a111fe76db89ad312f6f0347004c220c57f209a123"
"checksum rayon-core 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98dcf634205083b17d0861252431eb2acbfb698ab7478a2d20de07954f47ec7b"
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"

@ -0,0 +1,4 @@
# Features
## Planned
* Framebuffer

@ -11,12 +11,12 @@ name = "tutorial1-window"
path = "src/beginner/tutorial1-window.rs"
[[bin]]
name = "tutorial2-device"
path = "src/beginner/tutorial2-device.rs"
name = "tutorial2-swapchain"
path = "src/beginner/tutorial2-swapchain.rs"
[dependencies]
image = "0.22"
raw-window-handle = "0.3"
raw-window-handle = "0.1"
winit = "0.20.0-alpha3"
[dependencies.wgpu]

@ -0,0 +1,162 @@
use winit::{
event::*,
event_loop::{EventLoop, ControlFlow},
window::{Window, WindowBuilder},
};
struct State {
surface: wgpu::Surface,
device: wgpu::Device,
sc_desc: wgpu::SwapChainDescriptor,
swap_chain: wgpu::SwapChain,
hidpi_factor: f64,
size: winit::dpi::LogicalSize,
}
impl State {
fn new(window: &Window) -> Self {
let hidpi_factor = window.hidpi_factor();
let size = window.inner_size();
let physical_size = size.to_physical(hidpi_factor);
let instance = wgpu::Instance::new();
use raw_window_handle::HasRawWindowHandle as _;
let surface = instance.create_surface(window.raw_window_handle());
let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: Default::default(),
});
let device = adapter.request_device(&wgpu::DeviceDescriptor {
extensions: wgpu::Extensions {
anisotropic_filtering: false,
},
limits: Default::default(),
});
let sc_desc = wgpu::SwapChainDescriptor {
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
width: physical_size.width.round() as u32,
height: physical_size.height.round() as u32,
present_mode: wgpu::PresentMode::Vsync,
};
let swap_chain = device.create_swap_chain(&surface, &sc_desc);
Self {
surface,
device,
sc_desc,
swap_chain,
hidpi_factor,
size,
}
}
fn update_hidpi_and_resize(&mut self, new_hidpi_factor: f64) {
self.hidpi_factor = new_hidpi_factor;
self.resize(self.size);
}
fn resize(&mut self, new_size: winit::dpi::LogicalSize) {
let physical_size = new_size.to_physical(self.hidpi_factor);
self.size = new_size;
self.sc_desc.width = physical_size.width.round() as u32;
self.sc_desc.height = physical_size.height.round() as u32;
self.swap_chain = self.device.create_swap_chain(&self.surface, &self.sc_desc);
}
fn input(&mut self, event: &WindowEvent) -> bool {
false
}
fn update(&mut self) {
}
fn render(&mut self) {
let frame = self.swap_chain.get_next_texture();
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
todo: 0,
});
{
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[
wgpu::RenderPassColorAttachmentDescriptor {
attachment: &frame.view,
resolve_target: None,
load_op: wgpu::LoadOp::Clear,
store_op: wgpu::StoreOp::Store,
clear_color: wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
},
}
],
depth_stencil_attachment: None,
});
}
self.device.get_queue().submit(&[
encoder.finish()
]);
}
}
fn main() {
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.build(&event_loop)
.unwrap();
let mut state = State::new(&window);
event_loop.run(move |event, _, control_flow| {
match event {
Event::WindowEvent {
ref event,
window_id,
} if window_id == window.id() => if state.input(event) {
*control_flow = ControlFlow::Wait;
} else {
match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput {
input,
..
} => {
match input {
KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(VirtualKeyCode::Escape),
..
} => *control_flow = ControlFlow::Exit,
_ => *control_flow = ControlFlow::Wait,
}
}
WindowEvent::Resized(logical_size) => {
state.resize(*logical_size);
*control_flow = ControlFlow::Wait;
}
WindowEvent::HiDpiFactorChanged(new_hidpi_factor) => {
state.update_hidpi_and_resize(*new_hidpi_factor);
*control_flow = ControlFlow::Wait;
}
_ => *control_flow = ControlFlow::Wait,
}
}
Event::EventsCleared => {
state.update();
state.render();
*control_flow = ControlFlow::Wait;
}
_ => *control_flow = ControlFlow::Wait,
}
});
}

@ -10,6 +10,7 @@ module.exports = {
children: [
'/beginner/',
'/beginner/tutorial1-window',
'/beginner/tutorial2-swapchain',
],
},
{

@ -9,7 +9,7 @@ For the beginner stuff, we're going to keep things very simple, we'll add things
```toml
[dependencies]
image = "0.22"
raw-window-handle = "0.3"
raw-window-handle = "0.1" # needed to match wgpu's dependencies
winit = "0.20.0-alpha3"
[dependencies.wgpu]

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

@ -0,0 +1,355 @@
# The Swapchain
## First, some house keeping
For convenience we're going to pack all the fields into a struct, and create some methods on that.
```rust
// main.rs
struct State {
surface: wgpu::Surface,
adapter: wgpu::Adapter,
device: wgpu::Device,
sc_desc: wgpu::SwapChainDescriptor,
swap_chain: wgpu::SwapChain,
hidpi_factor: f64,
size: winit::dpi::LogicalSize,
}
impl State {
fn new(window: &Window) -> Self {
unimplemented!()
}
fn update_hidpi_factor_and_resize(&mut self, new_hidpi_factor: f64) {
unimplemented!()
}
fn resize(&mut self, new_size: winit::dpi::LogicalSize) {
unimplemented!()
}
fn input(&mut self, event: &WindowEvent) -> bool {
unimplemented!()
}
fn update(&mut self) {
unimplemented!()
}
fn render(&mut self) {
unimplemented!()
}
}
```
I'm glossing over `State`s fields, but they'll make more sense as I explain the code behind the methods.
## new()
The code for this is pretty straight forward, but let's break this down a bit.
```rust
impl State {
// ...
fn new(window: &Window) -> Self {
let hidpi_factor = window.hidpi_factor();
let size = window.inner_size();
let physical_size = size.to_physical(hidpi_factor);
```
The `hidpi_factor` is used to map "logical pixels" to actual pixels. We need this in tandem with `size` to get our `swap_chain` (more on that later) to be as accurate as possible.
```rust
let instance = wgpu::Instance::new();
use raw_window_handle::HasRawWindowHandle as _;
let surface = instance.create_surface(window.raw_window_handle());
let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: Default::default(),
});
```
The `instance`'s only use is to create a surface and request an `adapter`. We don't even need to save it.
The `surface` is used to create the `swap_chain`. We need the `window`'s `raw_window_handle` to access the native window implementation for `wgpu` to properly create the graphics backend. This is why we needed a window crate that supported [raw-window-handle](https://crates.io/crates/raw-window-handle).
We need the `adapter` to create the device.
```rust
let device = adapter.request_device(&wgpu::DeviceDescriptor {
extensions: wgpu::Extensions {
anisotropic_filtering: false,
},
limits: Default::default(),
});
```
As of writing, the wgpu implementation doesn't allow you to customize much of requesting a device. Eventually the descriptor structs will be filled out more to allow you to find the optimal `device`. Even so, we still need the `device`, so we'll store it in the struct.
```rust
let sc_desc = wgpu::SwapChainDescriptor {
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
width: physical_size.width.round() as u32,
height: physical_size.height.round() as u32,
present_mode: wgpu::PresentMode::Vsync,
};
let swap_chain = device.create_swap_chain(&surface, &sc_desc);
```
Here we defining and creating the `swap_chain`. The `usage` field describes how the `swap_chain`'s underlying textures will be used. `OUTPUT_ATTACHMENT` specifies that the textures will be used to write to the screen (we'll talk about more `TextureUsage`s later).
The `format` defines how the `swap_chain`s textures will be stored on the gpu. Usually you want to specify the format of the display you're using. As of writing, I was unable to find a way to query what format the display has through `wgpu`, so `wgpu::TextureFormat::Bgra8UnormSrgb` will do for now.
`width` and `height`, are self explanatory.
There's no documentation on `present_mode` as of writing, but my guess is that it defines the rate at which you can acquire images from the `swap_chain`.
At the end of the method, we simply return the resulting struct.
```rust
Self {
surface,
device,
sc_desc,
swap_chain,
hidpi_factor,
size,
}
}
// ...
}
```
We'll want to call this in our main method before we enter the event loop.
```rust
let mut state = State::new(&window);
```
## resize() and update_hidpi_factor_and_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 `hidpi_factor`, the logical `size`, and the `sc_desc` used to create the swapchain. With all of these, the resize method is very simple.
```rust
// impl State
fn resize(&mut self, new_size: winit::dpi::LogicalSize) {
let physical_size = new_size.to_physical(self.hidpi_factor);
self.size = new_size;
self.sc_desc.width = physical_size.width.round() as u32;
self.sc_desc.height = physical_size.height.round() as u32;
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.
`update_hidpi_and_resize` is also very simple.
```rust
// impl State
fn update_hidpi_and_resize(&mut self, new_hidpi_factor: f64) {
self.hidpi_factor = new_hidpi_factor;
self.resize(self.size);
}
```
We call both of these methods in `main()` in the event loop when there corresponding events trigger.
```rust
match event {
// ...
WindowEvent::Resized(logical_size) => {
state.resize(*logical_size);
}
WindowEvent::HiDpiFactorChanged(new_hidpi_factor) => {
state.update_hidpi_and_resize(*new_hidpi_factor);
}
// ...
}
```
## 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.
We're just going to return false for now because we don't have any events we want to capture.
```rust
// 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.
```rust
// 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) {
*control_flow = ControlFlow::Wait;
} else {
match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput {
input,
..
} => {
match input {
KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(VirtualKeyCode::Escape),
..
} => *control_flow = ControlFlow::Exit,
_ => *control_flow = ControlFlow::Wait,
}
}
WindowEvent::Resized(logical_size) => {
state.resize(*logical_size);
*control_flow = ControlFlow::Wait;
}
WindowEvent::HiDpiFactorChanged(new_hidpi_factor) => {
state.update_hidpi_and_resize(*new_hidpi_factor);
*control_flow = ControlFlow::Wait;
}
_ => *control_flow = ControlFlow::Wait,
}
}
_ => *control_flow = ControlFlow::Wait,
}
});
```
## update()
We don't have anything to update yet, so leave the method empty.
```rust
fn update(&mut self) {
}
```
## 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).
```rust
// impl State
fn render(&mut self) {
let frame = self.swap_chain.get_next_texture();
```
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.
```rust
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
todo: 0,
});
```
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.
```rust
{
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[
wgpu::RenderPassColorAttachmentDescriptor {
attachment: &frame.view,
resolve_target: None,
load_op: wgpu::LoadOp::Clear,
store_op: wgpu::StoreOp::Store,
clear_color: wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
},
}
],
depth_stencil_attachment: None,
});
}
self.device.get_queue().submit(&[
encoder.finish()
]);
}
```
First things first, let's talk about the `{}`. `encoder.begin_render_pass(...)` borrows `encoder` mutably (aka `&mut self`). `encoder.finish()` also requires a 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.
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.
The last lines of the code tell `wgpu` to finish the command buffer, and to submit it to the gpu's render queue.
We need to update the event loop again to call this method. We'll also call update before it too.
```rust
// main()
event_loop.run(move |event, _, control_flow| {
match event {
// ...
Event::EventsCleared => {
state.update();
state.render();
*control_flow = ControlFlow::Wait;
}
// ...
}
});
```
With all that, you should be getting something that looks like this.
![Window with a blue background](./tutorial2-swapchain-cleared-window.png)
## 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.
```rust
&wgpu::RenderPassDescriptor {
color_attachments: &[
// ...
],
depth_stencil_attachment: None,
}
```
A `RenderPassDescriptor` only has two fields: `color_attachments` and `depth_stencil_attachment`. The `color_attachements` describe where we are going to draw our color too.
We'll use `depth_stencil_attachment` later, but we'll set it to `None` for now.
```rust
wgpu::RenderPassColorAttachmentDescriptor {
attachment: &frame.view,
resolve_target: None,
load_op: wgpu::LoadOp::Clear,
store_op: wgpu::StoreOp::Store,
clear_color: wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
},
}
```
The `RenderPassColorAttachmentDescriptor` has the `attachment` 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_next_texture()`. This means that any colors we draw to this attachment will get drawn to the screen.
There's not much documentation for `resolve_target` at the moment, but it does expect an `Option<&'a TextureView>`. Fortunately, we can use `None`.
`load_op` and `store_op` define what operation to perform when gpu looks to load and store the colors for this color attachment for this render pass. We'll get more into this when we cover alpha blending, but for now we just `LoadOp::Clear` the texture when the render pass starts, and `StoreOp::Store` the colors when it ends.
The last field `clear_color` is just the color to use when `LoadOp::Clear` and/or `StoreOp::Clear` are used. This is where the blue color comes from.
## Final thoughts
In the event loop we're currently using `*control_flow = ControlFlow::Wait` in multiple places. This basically means that our app will wait for new input before drawing anything. In a game, we'd want the loop to update 60 times a second or more, but since we don't need anything to move around on it's own yet we'll leave things as is for now.
## 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`*
Loading…
Cancel
Save