#Part 1 - Project Setup and Your First Window
Welcome to the first part of this series on graphics programming with wgpu in Rust! By the end of this article, you’ll have a Rust program that opens a window and responds to input events. It doesn’t draw anything yet, but it’s the skeleton that everything else will hang from.
What is wgpu?
wgpu is a Rust graphics library that talks to your GPU using whichever native API your platform provides — Vulkan on Linux, Metal on macOS, DirectX 12 on Windows, and WebGPU in the browser. You write one codebase and it runs everywhere.
This is different from older graphics APIs like OpenGL, which abstracts the GPU in a way that’s easier to start with but hides how modern hardware actually works. wgpu is lower-level, which means more setup upfront, but you’ll understand exactly what the GPU is doing at every step.
The Event Loop
Every graphical application is built around an event loop — a loop that sits and waits for things to happen (key presses, mouse moves, window resizes, redraw requests) and dispatches them to your code. winit is the Rust library that gives us this, along with the window itself.
Setting Up the Project
Create a new Rust project:
cargo new graphics-with-wgpu-in-rust
cd graphics-with-wgpu-in-rust
Add our dependencies to Cargo.toml:
[package]
name = "graphics-with-wgpu"
version = "0.1.0"
edition = "2021"
[dependencies]
wgpu = "0.20"
winit = "0.29"
pollster = "0.3"
env_logger = "0.11"
log = "0.4"
- wgpu — the GPU library we’ll use throughout the series
- winit — creates windows and delivers input events
- pollster — runs async code synchronously (wgpu’s initialisation is async; we’ll need this in Part 2)
- env_logger / log — structured logging that wgpu uses internally; useful for debugging
Opening a Window
Replace src/main.rs with:
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
};
fn main() {
env_logger::init();
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
let window = WindowBuilder::new()
.with_title("Graphics with wgpu")
.with_inner_size(winit::dpi::LogicalSize::new(800u32, 600u32))
.build(&event_loop)
.unwrap();
event_loop
.run(move |event, elwt| match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => elwt.exit(),
_ => {}
})
.unwrap();
}
ControlFlow::Poll tells the event loop to call our closure continuously rather than waiting for an event — important for a rendering loop that needs to draw every frame. elwt.exit() shuts the loop down cleanly when the window’s close button is clicked.
The move keyword transfers ownership of window into the closure. In Rust, closures can capture variables from the surrounding scope, and move forces them to take ownership rather than borrow — necessary here because the event loop outlives the scope where window was created.
Running It
cargo run
You should see an 800×600 window with a black background and the title “Graphics with wgpu”. The window responds to the close button. That’s it for Part 1 — a small amount of code, but the scaffolding every graphics program needs.
What’s Next
In Part 2, we initialise the wgpu pipeline and learn how to clear the screen to a colour. That’s where the GPU gets involved for the first time.
Further reading:
- Scope and Ownership in Rust — if the
moveclosure felt unfamiliar, this covers how Rust moves values into closures - Rust Module System — as projects grow larger, modules keep code organised
FAQ
What is wgpu used for? wgpu is used for GPU-accelerated graphics and compute in Rust. It’s the foundation for game engines like Bevy, scientific visualisation tools, and anything that needs to talk to the GPU from Rust.
Does wgpu work on macOS? Yes. wgpu translates to Metal on macOS automatically. The same code runs on Windows (DirectX 12 / Vulkan), Linux (Vulkan), and macOS (Metal) without changes.
Why do we need an event loop? The operating system delivers window events (close, resize, keyboard, mouse) asynchronously. An event loop gives you a single place to receive and respond to all of them without polling manually.
What is ControlFlow::Poll?
It tells winit to call your event handler continuously rather than sleeping between events. For a rendering application you want to draw every frame, so Poll is the right choice. Interactive tools that only need to update on user input would use ControlFlow::Wait to save CPU.
Continue Learning
Part 6 - 2D Transformations
Move, rotate, and scale objects in wgpu using transformation matrices with glam — pass a 4×4 matrix as a uniform to the vertex shader and animate it each frame.
Part 5 - Textures and Image Loading
Load an image file and render it as a texture on a quad with wgpu in Rust — covers texture creation, samplers, UV coordinates, and index buffers.
Part 4 - Colors and Uniforms
Learn how to pass data from your Rust program to the GPU using uniform buffers and bind groups in wgpu — and animate a colour tint in real time.
Part 3 - Drawing Your First Triangle
Draw your first triangle with wgpu in Rust — define vertex data, upload it to the GPU with a vertex buffer, and write your first WGSL shaders.