An absolute beginners guide to WGPU (2024)

Hi! Trying to learn WGPU without a deep graphics background,like many complex topics, can be extremely challenging. It doesn’thelp that much of the content out there is written by peoplealready with years of experience in the field. I wrote the belowtutorial to get absolute beginners started with graphicsprogramming, but if you already have experience with Rust andgraphics, I would check out the awesome learn-wgpu tutorial whichwill go faster and cover more topics

That said, learning graphics takes time regardless of whattutorial you use, so feel free to skip around and come back tosections.

Background

So you want to get the GPU to do something...

To get your GPU device to do anything, you need to go throughsome OS/device specific APIs to tell your GPU what to do. In thepast, you were required to do a lot of work yourself to supportvarious APIs, even if your physical hardware is the same. That’swhere WGPU comes in.

WGPU is across-platform graphics API written in Rust that’s an abstractionlayer above native APIs like Vulkan, Metal, D3D12, and someothers.

WGPU is based on the WebGPU spec which defines agraphics API that will be exposed in browsers. In fact Firefox uses WGPU for its WebGPU backend. WebGPU will beimplemented eventually inmodern browsers, but for now it’s guarded behind some flags. Thegood news is that once WebGPU ships, the programs you write in WGPUwill be able to run natively in the browser!

You might have heard of OpenGL before coming here, or used itpreviously. OpenGL is another cross-platform API that is much morewidely used than WGPU, but has some limitations since it’s ahigher-level and older API. WGPU is designed to give developersmore control and better match modern hardware.

WebGPU (and by design WGPU) also better matches the design ofmore modern graphics APIs like Vulkan and Metal. The tradeoff isthat these APIs provide a lower-level interface which can beverbose and fragile at times. But don’t be scared! Even though theAPI is verbose, it’s usually not actually doing anything supercomplex that we have to worry about under the hood.

A note on Rust

WGPU is a library written in Rust and so is this tutorial. Iimagine that you’re probably unfamiliar with Rust or haven’t usedit widely so I’ll try to walk through some of the syntax here aswell. This should be relatively easy to follow for anyone with someintermediate programming experience.

First steps

To get started, let’s install Rust! Go to https://www.rust-lang.org/tools/installand follow the instructions for downloading. rustupshould install a bunch of tools (in ~/.cargo/bin)which should automatically be added to your PATHenvironment variable. The main tool we’ll use in this tutorial iscargo.

cargo is a package manager for Rust which we canalso use for compiling and running Rust code.

First off, let’s make a package to hold our code. Run

cargo init wgpu-intro

in the directory of your choice to create thewgpu-intro package.

You should see two files in the wgpu-introdirectory

.├── Cargo.toml # Meta file for the cargo package manager└── src └── main.rs # File with the main() function

First, open the Cargo.toml and add these lines tothe [dependencies] section

[dependencies]winit = "0.26.0"wgpu = "0.12.0"env_logger = "0.9"log = "0.4"pollster = "0.2"

We won’t use all these dependencies now, but they’ll come inhandy later. Here’s a little bit of info on how we’re going to useeach package.

  • winit is used as thecross-platform abstraction of window management. This allows us toeasily make windows and handle window events (such as key presses)without having to do OS-specific work.
  • wgpu is the the WGPUlibrary of course! This library holds all the functions and typesnecessary for communicating with the GPU and translating the APIcalls into the actual Vulkan/Metal/etc. commands.
  • env_logger andlog are used to provide wgpu a way tooutput useful messages to the console instead of just exitingwithout any output.
  • pollster is used torun an async function we’ll talk about later to completion. Rusthas a pretty interesting model of concurrency that requires us to use thisto await completion of a future in the main function.

In the next section we’ll setup everything but for now run

cargo run

anywhere in the wgpu-intro directory and you shouldsee the dependencies downloaded and a “Hello, world!” messageprinted.

Creating a window

Before we can show anything on the screen, we first need tocreate a window. Since we’re using winit as ourwindowing library, we’ll import some utilities and call some setupfunctions in our main.rs file.

use winit::{ event::*, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder,};fn main() { 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(); // Opens the window and starts processing events (although no events are handled yet) event_loop.run(move |event, _, control_flow| {});}

If you cargo run now, you should see a window popupon your screen like

An absolute beginners guide to WGPU (1)

Great! We now have a window ready for us to use. You can useCtrl+C to kill the process in your terminal since thewindow controls will not work. Let’s handle a little user input tomake it easy to close the window. Modify the event loop to looklike this

...event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { Event::WindowEvent { event: WindowEvent::CloseRequested, window_id, } if window_id == window.id() => *control_flow = ControlFlow::Exit, Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, window_id, } if window_id == window.id() => { if input.virtual_keycode == Some(VirtualKeyCode::Escape) { *control_flow = ControlFlow::Exit } } _ => (), }});

We just added a body to the event_loop closure to handle theCloseRequested (hitting the X button onthe window) and the escape key which you can use to close thewinit window.

It took me a little bit to get used to the Rust-y way of writingthings so feel free to take some time to digest all of this. Somequestions I had with some answers are below:

Why do we need to usemove and what does it dohere?

moveis used to capture a closure’s environment by value — meaning thatthe variables defined outside the closure can outlive the contextwhere those variables are defined. Also, the parameter torun is defined to be 'static which causes compiler errors to let usknow that this behavior occurs.

What is the ifstatement doing after thematch value?

This is called a guard and can be used to further filter the arm based on theconditional given. It’s usually used when destructuring structslike the event struct is here.

If you’re still a little lost, I highly recommend taking a lookat https://doc.rust-lang.org/rust-by-example/which is an awesome tutorial on Rust. Otherwise, feel free tocontinue going. I think this was the most confusing syntax-wisepart for me.

Anyways, this gets us through the winit setup thatwe need to make a window. Now let’s use WGPU to do something!

Let’s do some rendering!

Like I said before, the WGPU API is very verbose, but don’tworry — just because it’s a lot of lines, doesn’t mean it’s doinganything super complicated. Additionally, the API uses some jargonthat may not mean exactly what you’re used to, so feel free to lookup anything in the WGPUdocs or the WebGPUspec for help. Both can give some context on why things onnamed the way they are.

First, let’s setup a connection to the GPU.

fn main() { ... let window = WindowBuilder::new().build(&event_loop).unwrap(); let instance = wgpu::Instance::new(wgpu::Backends::all()); let surface = unsafe { instance.create_surface(&window) }; let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::default(), compatible_surface: Some(&surface), force_fallback_adapter: false, })) .unwrap(); 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(); let size = window.inner_size(); surface.configure(&device, &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface.get_preferred_format(&adapter).unwrap(), width: size.width, height: size.height, present_mode: wgpu::PresentMode::Fifo, }); ...}

There’s a lot going on here so take some time to internalizewhat is being done and how the pieces connect together. In summary,we are setting up a connection between the window and GPU device sowe can begin to send commands to the GPU.

  1. wgpu::Instance::new(wpgu::Backends::all()) createsan instance of the WPGU API for all backends. Backendsdefine the actual API (Vulkan, Metal, DX11, etc.) that WGPU selectsto make calls.
  2. instance.create_surface(&window) gets asurface from the window that WGPU can make calls to draw into.Under the hood it uses https://crates.io/crates/raw-window-handleto provide the interoperability between wgpu andwinit libraries. unsafe is used heresince the raw_window_handle must be valid and remainvalid for the lifetime of the surface.
  3. pollster::block_on(instance.request_adapter(...))waits on the WGPU API to get an adapter. We use pollster here topoll the async request adapter function since the main function issynchronous. You can think of requesting an adapter as anintermediate step between getting a reference to the actual device.The options here are pretty self-explanatory.
  4. pollster::block_on(adapter.request_device(...))gets the actual device which represents the GPU onyour system. Also, it returns a queue which we’ll uselater to send draw calls and other commands to thedevice. Be aware that featureshere can be used to define device specific features that you maywant to enable in the future.
  5. surface.configure(&device, ...); makes theconnection between the surface (in the window) and the GPU devicewith some configuration. With this line, the surface initialized toreceive input from the device and draw it on screen.

Now we have a window, wgpu instance, configured surface, anddevice with a queue — all the tools needed to setup a render, butwe’re not actually doing any rendering yet. Let’s fix that. Add thefollowing lines inside your event loop.

fn main() { ... 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"), }); { let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, // Pick any color you want here g: 0.9, b: 0.3, a: 1.0, }), store: true, }, }], depth_stencil_attachment: None, }); } // submit will accept anything that implements IntoIter queue.submit(std::iter::once(encoder.finish())); output.present(); }, ...}

Again, there’s a lot of words that are pretty abstract here.Looking up any terms you’re unfamiliar with can help you understandwhat’s really going on here.

  1. Event::RedrawRequested occurs when the windowrequests a redraw. This only happens once for now since the windowdoes this once itself, but in the future we’ll have to trigger itif we want to draw something else.
  2. surface.get_current_texture() gets the nextsurface texture, which is a wrapper around the actualtexture, to be drawn to the window.
  3. output.texture.create_view(...) gets the nextTextureView that describes the actual texture to be draw to thewindow.
  4. device.create_command_encoder(...) initializes acommand encoder for encoding operations to the GPU. Sendingoperations to the GPU in WGPU involves encoding and then queuing upoperations for the GPU to perform.
  5. encoder.begin_render_pass(...) creates a RenderPass.You can think of a render pass as a series of operations that getqueued up and then submitted to the GPU usingqueue.submit. In this case, we only have one operationwhich is to clear the view usingLoadOp::Clear.
  6. queue.submit(...) actually submits the work to theGPU. Before this, any calls like begin_render_pass arenot actually triggering any processing.
  7. output.present() schedules the texture(written inthe submit call) to be presented on thesurface and subsequently on your screen.

Now for the exciting part, the moment we’ve all been waitingfor, do a cargo run and you should see this

An absolute beginners guide to WGPU (2)

Amazing. It might not look like much but you’re now actuallytelling your GPU to to do something, which is pretty cool!

Now let’s do something fun with this. We’ll create a smoothtransition to from blue 0.0 to blue 1.0and back again using some simple logic.

let mut blue_value = 0; // Newlet mut blue_inc = 0; // Newevent_loop.run(move |event, _, control_flow| { ... load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.9, b: blue_value, // New a: 1.0, }), ... output.present(); // New blue_value += (red_inc as f64) * 0.001; if blue_value > 1.0 { blue_inc = -1; blue_value = 1.0; } else if blue_value < 0.0 { blue_inc = 1; blue_value = 0.0; } }, // New Event::MainEventsCleared => { window.request_redraw(); } ...

Note: Notice the MainEventsClearedlines. Before we were actually only submitting one render passsince we have to trigger a redraw due to how the windowing libraryis implemented.

You should see a smooth transition from green to light blue andback again.

But we’re not done yet, we haven’t even drawn anythinginteresting. Next up, we’ll learn about shaders and pipelines inorder to draw a humble triangle.

Based on the learn-wgpututorial

An absolute beginners guide to WGPU (2024)

References

Top Articles
Latest Posts
Article information

Author: Tuan Roob DDS

Last Updated:

Views: 6123

Rating: 4.1 / 5 (62 voted)

Reviews: 93% of readers found this page helpful

Author information

Name: Tuan Roob DDS

Birthday: 1999-11-20

Address: Suite 592 642 Pfannerstill Island, South Keila, LA 74970-3076

Phone: +9617721773649

Job: Marketing Producer

Hobby: Skydiving, Flag Football, Knitting, Running, Lego building, Hunting, Juggling

Introduction: My name is Tuan Roob DDS, I am a friendly, good, energetic, faithful, fantastic, gentle, enchanting person who loves writing and wants to share my knowledge and understanding with you.