Part 1 - Project Setup and Your First Window Image

#Part 1 - Project Setup and Your First Window

Elliot Forbes Elliot Forbes · May 29, 2026 · 4 min read

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:

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:

src/main.rs
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:

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.