Path to Rendering a Triangle
Aug 4, 2026 • 4minA guide to basic setup and rendering a single triangle on screen.
TL;DR
You can find the source code here: draw_triangle.

Setup & Boilerplate
This is the most minimal setup anyone can have to work with GPUs, as we need a window to draw on, a way to capture user input, and a global application state and manager that manages all windows and input.
We might add more setups for:
- Debug GUIs using the
eguicrate if they become necessary in the future.- We would surely need a physics library, but I’ve never worked on one and am not sure how complex it will be.
Window Setup
- Responsible for starting the app and keeping it running
- Provide window/user events that our Application can bind to.
- Manage window resizes that affect fullscreen textures and the window surface
- Focused on the
winitcrate, but you can use any windowing system that supports raw-window-handle. You can find a complete list here on crates.io.
impl ApplicationHandler for App {
fn resumed(&mut self, ..) {
// Initialize AppState like Window Surface and GPU instances etc.
}
fn window_event(&mut self, ..) {
match event {
WindowEvent::Resized(PhysicalSize { width, height }) => {
// Handle resize(width, height);
// and Request for another render loop calling request_redraw();
}
WindowEvent::RedrawRequested => {
// Render Loop or Quit
}
WindowEvent::CloseRequested => {
// I need to figure out how to close this using systems in future.
event_loop.exit();
}
_ => {}
}
}
}Application Setup
Nothing much to discuss here. Please check the code for this.
Application setup is required to create a bridge between various components in our app. For example, Application will provide an API interface library that our main application (bins) will use to set up the complex stuff behind the scenes. After that, the main source code will be very focused on the Scene development and how to render those scenes.
Even though it’s not important in a small context like ours, when a project grows, such design patterns help a lot.
pub struct App<T: EngineState> {
config: AppConfig,
state: T,
app_state: AppState,
}
impl<T: EngineState> App<T> {
// ..
}
impl<T: EngineState> ApplicationHandler for App<T> {
// ..
}Input Setup
This is important, but I’ll discuss this in the next chapter, as we will integrate Camera and our next code version will include this change for easier reference. But the basic idea for this is simple.
We need a Manager struct that helps convert the winit events into our own project events. For larger projects, this provides a better architecture to abstract away the windowing backend with our own input manager.
Window Surface Setup
A wgpu surface is a platform-specific canvas. Think of it as an HTML canvas. Instead of a browser creating the canvas, we have to manually get hold of this canvas for each platform. wgpu and the cross-platform windowing backends make it easy to get hold of this canvas/surface for different operating systems.
The surface is required since it’s responsible for showing our rendered texture on the user’s display.
Why do we have to care about Surface setup?
To answer this, we first have to answer What problems can we face when working with a user’s screen? A user’s screen can have the following things going on:
- Screens can be of different sizes and DPIs and may support HDR.
- Since we do not directly draw on the screen and instead show our renders on a smaller viewport on the user’s screen, the user may:
- Resize this viewport/window
- Move this window from one screen to another with completely different hardware specs
- May minimize the window
- May hide/blur (moving window behind other windows) window.
- Maybe more things that I’m unaware of…
So, in essence, surface setup is not a one-time thing. It may require reconfiguration or re-instantiation when the above scenarios affect the window.
wgpu broadly divides the above scenarios into the following SurfaceStatus values (as of v30):
Good: everything is all right; no reconfiguration is requiredSuboptimal: related to underlying swap chains, but reconfiguration is requiredTimeout,Outdated: these may need reconfigurationOccluded: hidden behind windows or minimized, etc. We can skip reconfiguration for now, but we would need to reconfigure when it is not occluded anymoreLost,Validation: not sure what these mean
So, from all this, we understand that:
- Window resizes always need surface reconfiguration before we render our frame
- Based on my findings from here
- We need to reconfigure our surface for
SuboptimalandOutdatedstatuses - We need to re-instantiate our surface for the
Loststatus - We also need to care for the
Occludedstatus. Even though we have to do nothing here, it’s possible our window may be affected when it comes back into focus, due to which we need to reconfigure or re-instantiate again.
- We need to reconfigure our surface for