Introduction
Please Note
This text is wildly work in progress.
Disclaimer
There are some things to keep in mind when reading this tutorial:
-
I am not an expert. I will do my best to provide useful and correct information but ultimately I am only drawing from my experience and so will eventually be wrong. As always it is good practice to draw from many sources of information rather than relying on just one.
-
There is no one way to do things. The approaches I present here are just patterns I have landed on in my personal work and that I think are useful, but for any problem there are many solutions with different pros and cons, and I encourage you to explore them.
Code snippets
Each rust snippet in this tutorial is included from a compiling example. You can see the full context of a snippet by pressing this button:

Getting Started
To discuss
- winit + glutin vs sdl2
- creating a window
- create a context, loading gl functions
- debug callbacks + debug context
- framework for rest of tutorial
- to cope with differences between winit/sdl2 if I choose to provide both
Before you can start rendering anything, first you need a window and an OpenGL context.
There are several ways to do this each with different pros and cons, but for the sake of simplicity I will just be sticking to sdl2.
SDL2 is a C library that has been used for windowing and co since antiquity, so it has the advantage that it is very easy to get started with. However, being a C library means that including it in your project will require either you have the SDL2 library available on your system, or you have cmake and a C compiler installed. Using C libraries in rust projects always adds some friction, so I will be structuring my examples such that SDL2 can be swapped out for something else later if need be.
The alternative, rust-only option is to use the winit and glutin crates - which would make building much easier, but in my opinion are significantly more difficult to get started with, so I will skip over them for now.
Getting SDL2 set up
To start you need to add the sdl2 crate as a dependency. So in your Cargo.toml add the following:
[dependencies.sdl2]
version = "0.35"
features = ["bundled", "static-link"]
The version is not super important - 0.35 is just the latest minor version available to me at time of writing. The important part here are the features: bundled and static-link. Long story short, these features together instruct the sdl2 crate to fetch the SDL2 source, build it with the local compiler, and statically link it into your binary. As mentioned previously, this requires that you have cmake and a C compiler available on your system, but will ensure that we can build our project on any platform that SDL2 supports without much hassle.
Create a window
Next its time to actually use sdl2. Add the following to your main.rs:
#[allow(dead_code)] pub fn main() { // These two calls are more or less equivalent to `SDL_Init(SDL_INIT_VIDEO)` let sdl_ctx = sdl2::init().expect("SDL init failed"); let sdl_video = sdl_ctx.video().expect("video subsystem init failed"); let _window = sdl_video.window("Tutorial", 1366, 768) .position_centered() .build() .expect("Failed to create window"); std::thread::sleep(std::time::Duration::from_secs(3)); }
If everything is set up correctly, then when you build and run this you should see a window appear for three seconds.
Create an OpenGL context
Next we have to create an OpenGL context. This happens in a few parts:
- First, before we make a window we have to describe to
sdl2what kind of context we want.- e.g., what version? what profile? whats our backbuffer format? etc etc.
- Then, after we've created our window, we can go about actually asking for the context.
- Note that before we use it we have to make it 'current'.
- Finally, once our new context has been created and made current, we can load our OpenGL functions from it.
Describing the context
#[allow(dead_code)] pub fn main() { let sdl_ctx = sdl2::init().expect("SDL init failed"); let sdl_video = sdl_ctx.video().expect("video subsystem init failed"); // Describe the version of OpenGL we want a context for _before_ creating the window. // We are using modern OpenGL, so 4.5+ and core profile is what we want. let gl_attr = sdl_video.gl_attr(); gl_attr.set_context_profile(sdl2::video::GLProfile::Core); gl_attr.set_context_version(4, 5); let window = sdl_video.window("Tutorial", 1366, 768) .position_centered() .resizable() .opengl() // This is important! .build() .expect("Failed to create window"); // Create our context let gl_ctx = window.gl_create_context().unwrap(); window.gl_make_current(&gl_ctx).unwrap(); // Load GL functions gl::load_with(|s| sdl_video.gl_get_proc_address(s) as *const _); // Render an empty frame to make sure _something_ is working. unsafe { gl::ClearColor(1.0, 0.0, 1.0, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); } window.gl_swap_window(); std::thread::sleep(std::time::Duration::from_secs(3)); }
Creating the context
#[allow(dead_code)] pub fn main() { let sdl_ctx = sdl2::init().expect("SDL init failed"); let sdl_video = sdl_ctx.video().expect("video subsystem init failed"); // Describe the version of OpenGL we want a context for _before_ creating the window. // We are using modern OpenGL, so 4.5+ and core profile is what we want. let gl_attr = sdl_video.gl_attr(); gl_attr.set_context_profile(sdl2::video::GLProfile::Core); gl_attr.set_context_version(4, 5); let window = sdl_video.window("Tutorial", 1366, 768) .position_centered() .resizable() .opengl() // This is important! .build() .expect("Failed to create window"); // Create our context let gl_ctx = window.gl_create_context().unwrap(); window.gl_make_current(&gl_ctx).unwrap(); // Load GL functions gl::load_with(|s| sdl_video.gl_get_proc_address(s) as *const _); // Render an empty frame to make sure _something_ is working. unsafe { gl::ClearColor(1.0, 0.0, 1.0, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); } window.gl_swap_window(); std::thread::sleep(std::time::Duration::from_secs(3)); }
Loading functions
#[allow(dead_code)] pub fn main() { let sdl_ctx = sdl2::init().expect("SDL init failed"); let sdl_video = sdl_ctx.video().expect("video subsystem init failed"); // Describe the version of OpenGL we want a context for _before_ creating the window. // We are using modern OpenGL, so 4.5+ and core profile is what we want. let gl_attr = sdl_video.gl_attr(); gl_attr.set_context_profile(sdl2::video::GLProfile::Core); gl_attr.set_context_version(4, 5); let window = sdl_video.window("Tutorial", 1366, 768) .position_centered() .resizable() .opengl() // This is important! .build() .expect("Failed to create window"); // Create our context let gl_ctx = window.gl_create_context().unwrap(); window.gl_make_current(&gl_ctx).unwrap(); // Load GL functions gl::load_with(|s| sdl_video.gl_get_proc_address(s) as *const _); // Render an empty frame to make sure _something_ is working. unsafe { gl::ClearColor(1.0, 0.0, 1.0, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); } window.gl_swap_window(); std::thread::sleep(std::time::Duration::from_secs(3)); }
Making sure it works
#[allow(dead_code)] pub fn main() { let sdl_ctx = sdl2::init().expect("SDL init failed"); let sdl_video = sdl_ctx.video().expect("video subsystem init failed"); // Describe the version of OpenGL we want a context for _before_ creating the window. // We are using modern OpenGL, so 4.5+ and core profile is what we want. let gl_attr = sdl_video.gl_attr(); gl_attr.set_context_profile(sdl2::video::GLProfile::Core); gl_attr.set_context_version(4, 5); let window = sdl_video.window("Tutorial", 1366, 768) .position_centered() .resizable() .opengl() // This is important! .build() .expect("Failed to create window"); // Create our context let gl_ctx = window.gl_create_context().unwrap(); window.gl_make_current(&gl_ctx).unwrap(); // Load GL functions gl::load_with(|s| sdl_video.gl_get_proc_address(s) as *const _); // Render an empty frame to make sure _something_ is working. unsafe { gl::ClearColor(1.0, 0.0, 1.0, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); } window.gl_swap_window(); std::thread::sleep(std::time::Duration::from_secs(3)); }
Debug contexts
More resources
Speed Run A Rectangle
There are a few things we need to do before we can start rendering triangles, the meatiest of which is shader compilation. There are many ways to get triangles onto the screen in modern OpenGL but all of them require shaders so we will start there.
Shaders
To render triangles in modern OpenGL at least one shader program is required.
What's a shader program?
A shader program in opengl is an assembly of one or more 'shaders' which are 'linked' together.
Shaders are pieces of code that are sent to and execute on the GPU to perform different functions within a pipeline. They are where the bulk of your customisability comes from in your rendering pipeline.
You can read details on the Khronos OpenGL wiki.
To get started we'll need two shaders: a vertex shader, a fragment shader, and some boilerplate for compiling these two shaders and linking them into a 'program'.
For the sake of getting something rendering quickly I will not start with the typical 'hello world' that most tutorials start with, but with something a little more advanced so we can skip some of the other boilerplate we'd normally need.
Try to follow if you can, but if you don't understand it right away don't worry. Understanding can come once we have something to look at.
The Vertex Shader
Lets start at the start of the pipeline, the vertex shader - a small program that is run for each vertex we want to render. We have some creative liberty as to what constitutes a 'vertex' here, but for now lets just start with code:
#version 450
// Our vertex positions - in clip space.
const vec2[4] g_positions = {
{-0.5,-0.5},
{ 0.5,-0.5},
{ 0.5, 0.5},
{-0.5, 0.5},
};
// Indices into g_positions. Each three indices defines a triangle
// in counter-clockwise winding order.
const uint g_indices[6] = {0, 1, 2, 0, 2, 3};
// The entry point for our shader.
// It will be called once for every vertex.
void main() {
const uint position_index = g_indices[gl_VertexID];
const vec2 position = g_positions[position_index];
gl_Position = vec4(position, 0.0, 1.0);
}
This shader will generate the vertices we need to render two triangles.
This shader is a little unusual but hopefully should be small enough to follow. To fully understand it however, you need to be aware of two special variables:
gl_VertexID: Holds the vertex index. When used withglDrawArrays(_, _, num_vertices), will hold a value between zero andnum_vertices.gl_Position: The output of the vertex shader. The final position of our vertex (in clip space) should be written here.
See https://www.khronos.org/opengl/wiki/Vertex_Shader for more detail.
The Fragment Shader
Next the fragment shader - a program that is run for each fragment (opengl speak for "maybe a pixel but not quite") and determines (among other things) the colour to be written to the backbuffer. We will be using this shader for now:
#version 450
out vec4 o_color;
// The entry point for our fragment shader.
void main() {
o_color = vec4(1.0, 0.0, 1.0, 1.0);
}
This shader will write magenta to the backbuffer for each fragment generated during rasterisation - or put more simply, our triangles will be pink.
out vec4 o_color; specifies an output for the shader. The name can be anything, only its type and the fact that it is an output matters. Whatever we write to this variable during an invocation of this shader will be (potentially) written to the backbuffer.
This shader will become more involved later, but for now it will do.
See https://www.khronos.org/opengl/wiki/Fragment_Shader for more detail.
Shader Compilation
This part is a bit gnarly, but its something that only needs to be done once. Shader compilation is not complex, just messy due to the error checking required, and messier still due to needing to deal with nul terminated strings thanks to OpenGL being designed for C.
That said, the process for compiling shaders can be summarised as such:
- For each desired shader,
- Create a shader object -
glCreateShader - Pass its source code to OpenGL -
glShaderSource - Compile it -
glCompileShader - Check for errors, and bail if any are encountered
- Create a shader object -
- Create a shader program -
glCreateProgram - Attach every shader to the program -
glAttachShader - Finally link the program -
glLinkProgram, and - Check for errors
After this, assuming no errors, you will have a usable program.
Though it is worth noting that once linked, shader objects are no longer required and can be cleaned up.
It can be detached from the program (glDetachShader) and optionally deleted (glDeleteShader).
Enough summarising, time for code:
use framework::prelude::*; fn main() { framework::run("ch01 - speedrun a rectangle", Example::new); } struct Example { main_shader: u32, } impl Example { fn new() -> anyhow::Result<Example> { // Create a shader program. let main_shader = unsafe { // Compile our shaders, bailing if we hit any problems. let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?; let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?; // Create our program and attach our shaders to it for linking. let program = gl::CreateProgram(); gl::AttachShader(program, vert_shader); gl::AttachShader(program, frag_shader); gl::LinkProgram(program); // Clean up the shaders we compiled above since we no longer need // them after linking, even if linking failed. for shader in [vert_shader, frag_shader] { gl::DetachShader(program, shader); gl::DeleteShader(shader); } check_program_status(program)?; program }; // Create our empty VAO to allow glDrawArrays to draw without buffers bound. unsafe { let mut vao = 0; gl::CreateVertexArrays(1, &mut vao); gl::BindVertexArray(vao); } Ok(Example { main_shader, }) } } impl framework::App for Example { fn draw(&mut self, state: &framework::State) { unsafe { let size = state.backbuffer_size(); gl::Viewport(0, 0, size.x, size.y); gl::ClearColor(0.1, 0.1, 0.1, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); // This corresponds to the number of indices in our vertex shader. let num_vertices = 6; gl::UseProgram(self.main_shader); gl::DrawArrays(gl::TRIANGLES, 0, num_vertices); } } } fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> { let src_c = std::ffi::CString::new(src)?; unsafe { let shader = gl::CreateShader(ty); gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null()); gl::CompileShader(shader); // NOTE: on error, this will technically leak `shader`. // But we don't care for now since we're planning to // bail immediately on error check_shader_status(shader)?; Ok(shader) } } fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetShaderInfoLog( shader_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Shader failed to compile: {error_msg}"); } } Ok(()) } fn check_program_status(program_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetProgramInfoLog( program_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Program failed to link: {error_msg}"); } } Ok(()) }
This snippet represents step one of the above process: compiling and error checking an individual shader.
Next we can create a program and start linking:
use framework::prelude::*; fn main() { framework::run("ch01 - speedrun a rectangle", Example::new); } struct Example { main_shader: u32, } impl Example { fn new() -> anyhow::Result<Example> { // Create a shader program. let main_shader = unsafe { // Compile our shaders, bailing if we hit any problems. let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?; let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?; // Create our program and attach our shaders to it for linking. let program = gl::CreateProgram(); gl::AttachShader(program, vert_shader); gl::AttachShader(program, frag_shader); gl::LinkProgram(program); // Clean up the shaders we compiled above since we no longer need // them after linking, even if linking failed. for shader in [vert_shader, frag_shader] { gl::DetachShader(program, shader); gl::DeleteShader(shader); } check_program_status(program)?; program }; // Create our empty VAO to allow glDrawArrays to draw without buffers bound. unsafe { let mut vao = 0; gl::CreateVertexArrays(1, &mut vao); gl::BindVertexArray(vao); } Ok(Example { main_shader, }) } } impl framework::App for Example { fn draw(&mut self, state: &framework::State) { unsafe { let size = state.backbuffer_size(); gl::Viewport(0, 0, size.x, size.y); gl::ClearColor(0.1, 0.1, 0.1, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); // This corresponds to the number of indices in our vertex shader. let num_vertices = 6; gl::UseProgram(self.main_shader); gl::DrawArrays(gl::TRIANGLES, 0, num_vertices); } } } fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> { let src_c = std::ffi::CString::new(src)?; unsafe { let shader = gl::CreateShader(ty); gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null()); gl::CompileShader(shader); // NOTE: on error, this will technically leak `shader`. // But we don't care for now since we're planning to // bail immediately on error check_shader_status(shader)?; Ok(shader) } } fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetShaderInfoLog( shader_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Shader failed to compile: {error_msg}"); } } Ok(()) } fn check_program_status(program_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetProgramInfoLog( program_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Program failed to link: {error_msg}"); } } Ok(()) }
check_program_status in the above snippet is very similar to check_shader_status barre some different names
use framework::prelude::*; fn main() { framework::run("ch01 - speedrun a rectangle", Example::new); } struct Example { main_shader: u32, } impl Example { fn new() -> anyhow::Result<Example> { // Create a shader program. let main_shader = unsafe { // Compile our shaders, bailing if we hit any problems. let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?; let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?; // Create our program and attach our shaders to it for linking. let program = gl::CreateProgram(); gl::AttachShader(program, vert_shader); gl::AttachShader(program, frag_shader); gl::LinkProgram(program); // Clean up the shaders we compiled above since we no longer need // them after linking, even if linking failed. for shader in [vert_shader, frag_shader] { gl::DetachShader(program, shader); gl::DeleteShader(shader); } check_program_status(program)?; program }; // Create our empty VAO to allow glDrawArrays to draw without buffers bound. unsafe { let mut vao = 0; gl::CreateVertexArrays(1, &mut vao); gl::BindVertexArray(vao); } Ok(Example { main_shader, }) } } impl framework::App for Example { fn draw(&mut self, state: &framework::State) { unsafe { let size = state.backbuffer_size(); gl::Viewport(0, 0, size.x, size.y); gl::ClearColor(0.1, 0.1, 0.1, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); // This corresponds to the number of indices in our vertex shader. let num_vertices = 6; gl::UseProgram(self.main_shader); gl::DrawArrays(gl::TRIANGLES, 0, num_vertices); } } } fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> { let src_c = std::ffi::CString::new(src)?; unsafe { let shader = gl::CreateShader(ty); gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null()); gl::CompileShader(shader); // NOTE: on error, this will technically leak `shader`. // But we don't care for now since we're planning to // bail immediately on error check_shader_status(shader)?; Ok(shader) } } fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetShaderInfoLog( shader_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Shader failed to compile: {error_msg}"); } } Ok(()) } fn check_program_status(program_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetProgramInfoLog( program_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Program failed to link: {error_msg}"); } } Ok(()) }
Once this is done you should have a program ready to render with. We only need to do a couple more things before we can get something on screen.
Finally Rendering?
Rendering our generated, magenta rectangle should now just be a matter of binding our program and emitting the appropriate drawcall. That would look something like this:
use framework::prelude::*; fn main() { framework::run("ch01 - speedrun a rectangle", Example::new); } struct Example { main_shader: u32, } impl Example { fn new() -> anyhow::Result<Example> { // Create a shader program. let main_shader = unsafe { // Compile our shaders, bailing if we hit any problems. let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?; let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?; // Create our program and attach our shaders to it for linking. let program = gl::CreateProgram(); gl::AttachShader(program, vert_shader); gl::AttachShader(program, frag_shader); gl::LinkProgram(program); // Clean up the shaders we compiled above since we no longer need // them after linking, even if linking failed. for shader in [vert_shader, frag_shader] { gl::DetachShader(program, shader); gl::DeleteShader(shader); } check_program_status(program)?; program }; // Create our empty VAO to allow glDrawArrays to draw without buffers bound. unsafe { let mut vao = 0; gl::CreateVertexArrays(1, &mut vao); gl::BindVertexArray(vao); } Ok(Example { main_shader, }) } } impl framework::App for Example { fn draw(&mut self, state: &framework::State) { unsafe { let size = state.backbuffer_size(); gl::Viewport(0, 0, size.x, size.y); gl::ClearColor(0.1, 0.1, 0.1, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); // This corresponds to the number of indices in our vertex shader. let num_vertices = 6; gl::UseProgram(self.main_shader); gl::DrawArrays(gl::TRIANGLES, 0, num_vertices); } } } fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> { let src_c = std::ffi::CString::new(src)?; unsafe { let shader = gl::CreateShader(ty); gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null()); gl::CompileShader(shader); // NOTE: on error, this will technically leak `shader`. // But we don't care for now since we're planning to // bail immediately on error check_shader_status(shader)?; Ok(shader) } } fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetShaderInfoLog( shader_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Shader failed to compile: {error_msg}"); } } Ok(()) } fn check_program_status(program_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetProgramInfoLog( program_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Program failed to link: {error_msg}"); } } Ok(()) }
However, if you were to try this now (and you've set up debug callbacks properly) you should get OpenGL complaining at you and refusing to draw anything.
Perhaps it will look something like this:
GL ERROR!
Source: api
Severity: high
Type: error
Message: GL_INVALID_OPERATION in glDrawArrays
Why is that?
Because we are missing one final piece of state - a Vertex Array Object.
A VAO is an object that more or less describes how to feed vertices to the GPU. It does so via a number of 'vertex attributes'. Each attribute represents a stream of data that describes some aspect of a 'vertex' (or an instance), like position, color, uvs, etc. On top of information about attributes, a VAO also potentially contains references to buffers of vertex data and information about to turn them into attributes, and information about which attributes should draw from buffers at all.
This last part is why we need a VAO. We don't have any buffers to draw data from, our vertex shader doesn't have any attributes to feed. BUT glDrawArrays still needs to query the VAO to determine if there are any buffers it needs to read. In effect, we just need a VAO that says "no, you don't need to pull data from anywhere, just draw".
Lucky for us this is easy to communicate, as this is the default state of a new VAO.
use framework::prelude::*; fn main() { framework::run("ch01 - speedrun a rectangle", Example::new); } struct Example { main_shader: u32, } impl Example { fn new() -> anyhow::Result<Example> { // Create a shader program. let main_shader = unsafe { // Compile our shaders, bailing if we hit any problems. let vert_shader = compile_shader(gl::VERTEX_SHADER, include_str!("shaders/vert.glsl"))?; let frag_shader = compile_shader(gl::FRAGMENT_SHADER, include_str!("shaders/frag.glsl"))?; // Create our program and attach our shaders to it for linking. let program = gl::CreateProgram(); gl::AttachShader(program, vert_shader); gl::AttachShader(program, frag_shader); gl::LinkProgram(program); // Clean up the shaders we compiled above since we no longer need // them after linking, even if linking failed. for shader in [vert_shader, frag_shader] { gl::DetachShader(program, shader); gl::DeleteShader(shader); } check_program_status(program)?; program }; // Create our empty VAO to allow glDrawArrays to draw without buffers bound. unsafe { let mut vao = 0; gl::CreateVertexArrays(1, &mut vao); gl::BindVertexArray(vao); } Ok(Example { main_shader, }) } } impl framework::App for Example { fn draw(&mut self, state: &framework::State) { unsafe { let size = state.backbuffer_size(); gl::Viewport(0, 0, size.x, size.y); gl::ClearColor(0.1, 0.1, 0.1, 1.0); gl::Clear(gl::COLOR_BUFFER_BIT); // This corresponds to the number of indices in our vertex shader. let num_vertices = 6; gl::UseProgram(self.main_shader); gl::DrawArrays(gl::TRIANGLES, 0, num_vertices); } } } fn compile_shader(ty: u32, src: &str) -> anyhow::Result<u32> { let src_c = std::ffi::CString::new(src)?; unsafe { let shader = gl::CreateShader(ty); gl::ShaderSource(shader, 1, &src_c.as_ptr(), std::ptr::null()); gl::CompileShader(shader); // NOTE: on error, this will technically leak `shader`. // But we don't care for now since we're planning to // bail immediately on error check_shader_status(shader)?; Ok(shader) } } fn check_shader_status(shader_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetShaderiv(shader_handle, gl::COMPILE_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetShaderiv(shader_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetShaderInfoLog( shader_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Shader failed to compile: {error_msg}"); } } Ok(()) } fn check_program_status(program_handle: u32) -> anyhow::Result<()> { unsafe { let mut status = 0; gl::GetProgramiv(program_handle, gl::LINK_STATUS, &mut status); if status == 0 { let mut length = 0; gl::GetProgramiv(program_handle, gl::INFO_LOG_LENGTH, &mut length); let mut buffer = vec![0u8; length as usize]; gl::GetProgramInfoLog( program_handle, length, std::ptr::null_mut(), buffer.as_mut_ptr() as *mut _ ); let error_msg = String::from_utf8_lossy(&buffer[..buffer.len()-1]); anyhow::bail!("Program failed to link: {error_msg}"); } } Ok(()) }
The above snippet only needs to run once during setup, and that should be enough to allow us to draw. Normally you'd want to bind your vao just before you draw, but since we only have one its fine to just bind once at startup. This code is only temporary so don't worry too much about it just yet.
So if you try calling glDrawArrays one more time, you should be presented with something like this:

More Resources
The Khronos wiki has a lot of decent information and is worth reading through:
- https://www.khronos.org/opengl/wiki/Shader
- https://www.khronos.org/opengl/wiki/Shader_Compilation
- https://www.khronos.org/opengl/wiki/Vertex_Shader
- https://www.khronos.org/opengl/wiki/Fragment_Shader
The OpenGL 4.5 Quick Reference Card also contains a reference for GLSL 450 which is worthwhile looking through if you're not already familiar with GLSL
Texturing
To discuss
imagecrate- intuition about coordinate systems
- why do we want to flip coordinates? how everything fits together
- glTextureStorage, glTextureSubImage2D, glBindTextureUnit
- NOTE: immutable storage
- opengl texture objects, texture units
- texture atlasses
- sampling textures in shaders + uvs
- maybe srgb?
Building Meshes
To discuss