Welcome back!

Previously, we have loaded OpenGL context for SDL window, to handle user input and output pixels.

In this lesson, we will work towards rendering the classic OpenGL triangle. Classic, because every every OpenGL tutorial does this.

But first, we will learn how to create safe abstractions in Rust, and we will build tools for compiling a shader and linking a program.

“Modern” OpenGL

We will be using what is called “modern OpenGL”. Turns out, long ago, it wasn’t so modern. We won’t discuss graphics pipeline here from the beginning: instead, I suggest you follow along using another “modern OpenGL tutorial”. I will be using this awesome tutorial as the base. This lesson will cover more Rust-y side of “Hello Triangle” part.

Additional OpenGL context setup

Previous part of learnopengl.com lesson talks about creating a window. We’ve mostly done that, except I forgot a few things.

First, we need to specify the minimal OpenGL version to use. In our case, we can do that using SDL gl attributes:

(main.rs, incomplete)

let gl_attr = video_subsystem.gl_attr();

gl_attr.set_context_profile(sdl2::video::GLProfile::Core);
gl_attr.set_context_version(4, 5);

let window = ... // create the window

I will be using quite recent (Core 4.5) version here, but feel free to dial it down if your context can not be created, down to Core 3.3 used in learnopengl.com tutorial.

Second, we need to set up our viewport. We can do it once, just before ClearColor:

(main.rs, incomplete)

unsafe {
    gl::Viewport(0, 0, 900, 700); // set viewport
    gl::ClearColor(0.3, 0.3, 0.5, 1.0);
}

I see unsafe code

Yeah, me too. Let’s continue.

Shader

We will make a helper function to compile a shader from string, and then another function to link compiled shaders into a program.

First try:

(main.rs, at the end)

fn shader_from_source(source: &str) -> gl::types::GLuint {
    // continue here
}

Given a source code, this function should return shader id (as an int).

However, the shader creation may fail, and we may want to retrieve the error message. For that, we will change return type to Result<gl::types::GLuint, String>. If Result is Ok, we will get shader id, otherwise we can extract an error message from Err.

fn shader_from_source(source: &str) -> Result<gl::types::GLuint, String> {
    ...
}

Let’s start by obtaining shader object id:

fn shader_from_source(source: &str) -> Result<gl::types::GLuint, String> {
    let id = unsafe { gl::CreateShader(gl::VERTEX_SHADER) };

    // continue here
}

Ha! We need to specify the shader type. Let’s improve the function signature and add the shader type to it. type is a reserved keyword in Rust, but we can name it kind:

fn shader_from_source(
    source: &str,
    kind: gl::types::GLuint
) -> Result<gl::types::GLuint, String> {
    let id = unsafe { gl::CreateShader(kind) };

    // continue here
}

Next, we need to set the source for the shader object using glShaderSource function.

However, it is C function, so the third string argument passed to it has to be zero terminated C string. Rust strings are not zero terminated, and they can even contain 0 values inside.

There are two ways to deal with this: convert Rust string slice &str to CString inside this function, or let function caller deal with that. I am going to choose the second option, because CStrings can also be created directly from bytes, and we will later load our shaders from files, which can be read as ASCII into byte arrays.

Let’s change function parameter from &str to &std::ffi::CStr:

// import namespace to avoid repeating `std::ffi` everywhere
use std::ffi::{CString, CStr};

fn shader_from_source(
    source: &CStr, // modified
    kind: gl::types::GLenum
) -> Result<gl::types::GLuint, String> {
    let id = unsafe { gl::CreateShader(kind) };

    // continue here
}

&CStr can be borrowed from owned CString the same way a &str is borrowed from a String. Oh, and by the way, knowing basics about Rust’s ownership and borrowing would be required from now on.

With that, we can set shader source and compile it:

unsafe {
    gl::ShaderSource(id, 1, &source.as_ptr(), std::ptr::null());
    gl::CompileShader(id);
}

// continue here

We could return Ok(id) now and move on, however, we really need to see a proper error message if the shader fails to compile.

Therefore, we obtain the shader compilation status:

let mut success: gl::types::GLint = 1;
unsafe {
    gl::GetShaderiv(id, gl::COMPILE_STATUS, &mut success);
}

And if it is 0, we will return an error string, otherwise Ok(id):

if success == 0 {
    // continue here
}

Ok(id)

We will need to write returned error to buffer, therefore we need to know the required length of this buffer.

We get the len by querying GL_INFO_LOG_LENGTH for the shader object:

let mut len: gl::types::GLint = 0;
unsafe {
    gl::GetShaderiv(id, gl::INFO_LOG_LENGTH, &mut len);
}

// continue here

For our buffer, we allocate Vec of the correct length and fill it with spaces. Then we create CString from it (which is going to reuse the same allocation, as well as append 0 at the end):

// allocate buffer of correct size
let mut buffer: Vec<u8> = Vec::with_capacity(len as usize + 1);
// fill it with len spaces
buffer.extend([b' '].iter().cycle().take(len as usize));
// convert buffer to CString
let error: CString = unsafe { CString::from_vec_unchecked(buffer) };

// continue here

This code might be a tad confusing:

[b' '].iter().cycle().take(len as usize)
  • [b' '] is a single-item stack-allocated array which contains ASCII “space” byte.
  • .iter() obtains iterator over it.
  • .cycle() repeats this iterator forever, yielding infinite number of spaces.
  • .take(len as usize) limits returned items to len.

The buffer.extend(...) for its argument accepts iterator, from which it appends new items. Iterators are zero-cost, they require no additional allocations and compile down to compact instructions.

With that, we can ask OpenGL to write shader info log into our error value:

unsafe {
    gl::GetShaderInfoLog(
        id,
        len,
        std::ptr::null_mut(),
        error.as_ptr() as *mut gl::types::GLchar
    );
}

// continue here

And finally, we can return an error:

return Err(error.to_string_lossy().into_owned());

error.to_string_lossy() converts CString to Rust String, replacing any invalid unicode characters with unicode error character. to_string_lossy is implemented on CStr, but we were able to call it on CString, because it inherits all CStr methods. However, to_string_lossy returns a value that can be either String or &str, so we use into_owned to obtain a definite String. This last unfortunate step requires an additional allocation, but this is the error handling path, so hopefully it is not a big deal.

Congratulations! You have just created your first safe API for a bunch of unsafe functions!

However, there are some things we can do to make this API nicer.

Extracting CString creation from shader_from_source

We can extract the lines that help us create new empty CString to another function. We might need it later:

(main.rs, append at the end)

fn create_whitespace_cstring_with_len(len: usize) -> CString {
    // allocate buffer of correct size
    let mut buffer: Vec<u8> = Vec::with_capacity(len + 1);
    // fill it with len spaces
    buffer.extend([b' '].iter().cycle().take(len));
    // convert buffer to CString
    unsafe { CString::from_vec_unchecked(buffer) }
}

And use it in original location:

let error = create_whitespace_cstring_with_len(len as usize);

Creating a newtype for shader object

Instead of returning a shader object id, we can return a struct named Shader that wraps this id. This returned struct would have no overhead, and will consume exactly the same amount of bytes as gl::types::GLuint, but can be much more convenient to use:

(main.rs, above shader_from_source function)

struct Shader {
    id: gl::types::GLuint,
}

// continue here

And then implement from_source function for Shader:

impl Shader {
    fn from_source(
        source: &CStr,
        kind: gl::types::GLenum
    ) -> Result<Shader, String> {
        let id = shader_from_source(source, kind)?;
        Ok(Shader { id })
    }

    // continue here
}

create does not have self first parameter, and is akin to static functions in Java-like languages. It is called using Shader::from_source syntax and acts as a constructor.

The return type of it is Result<Shader, String>, which may be either the successfully created Shader, or an error String.

The question mark ? at the end of let id = shader_from_source(source, kind)?; line does the same as would this code:

(example)

let id = match shader_from_source(source, kind) {
    Ok(id) => id,
    Err(error) => return Err(error.into()),
};

In case the Result was Ok, this block would unwrap id from Ok and assign it to id variable, and in case of Err, the function would return with the same error value.

There is much more to error handling in Rust, and I highly recommend to not skip learning about it.

Additional constructor methods for Shader

Now our shader can be created like this:

(example)

let shader = Shader::from_source(
    &CStr::from_bytes_with_nul(b"<source code here>\0").unwrap(),
    gl::VERTEX_SHADER
).unwrap();

We can create two more helper methods, from_vert_source and from_frag_source, so that we can skip gl::VERTEX_SHADER parameter:

impl Shader {
    fn from_source(...) { ... }

    fn from_vert_source(source: &CStr) -> Result<Shader, String> {
        Shader::from_source(source, gl::VERTEX_SHADER)
    }

    fn from_frag_source(source: &CStr) -> Result<Shader, String> {
        Shader::from_source(source, gl::FRAGMENT_SHADER)
    }
}

// continue here

Resource cleanup

As implemented, our Shader type leaks shader id without cleaning it up.

To clean it up, we will implement Drop trait for the Shader:

(after impl Shader {} block)

impl Drop for Shader {
    fn drop(&mut self) {
        unsafe {
            gl::DeleteShader(self.id);
        }
    }
}

Rust will ensure that gl::DeleteShader is called exactly once for every shader object id.

Moving Shader implementation into another module

I am going to call new module render_gl, because it will contain utilities such as the shader for gl rendering.

Let’s wrap everything bellow the main function into pub mod render_gl {} block. Modules do not inherit imports from the parent modules. Therefore we will need to add use gl; at the top of child render_gl module. Moreover, by default Rust imports std to the root crate module; therefore we will need use std; in child too.

If you try to use render_gl::Shader from this module in main and compile our app, you will get errors saying that Shader is private; we will need to add pub keywords for anything we want to be visible from the outside of the module. In our case, it is the Shader struct with the create... methods.

With all of that sorted out, the full module code looks like this:

pub mod render_gl {
    use gl;
    use std;
    use std::ffi::{CString, CStr};

    pub struct Shader {
        id: gl::types::GLuint,
    }

    impl Shader {
        pub fn from_source(
            source: &CStr,
            kind: gl::types::GLenum
        ) -> Result<Shader, String> {
            let id = shader_from_source(source, kind)?;
            Ok(Shader { id })
        }

        pub fn from_vert_source(source: &CStr) -> Result<Shader, String> {
            Shader::from_source(source, gl::VERTEX_SHADER)
        }

        pub fn from_frag_source(source: &CStr) -> Result<Shader, String> {
            Shader::from_source(source, gl::FRAGMENT_SHADER)
        }
    }

    impl Drop for Shader {
        fn drop(&mut self) {
            unsafe {
                gl::DeleteShader(self.id);
            }
        }
    }

    fn shader_from_source(
        source: &CStr,
        kind: gl::types::GLenum
    ) -> Result<gl::types::GLuint, String> {
        let id = unsafe { gl::CreateShader(kind) };
        unsafe {
            gl::ShaderSource(id, 1, &source.as_ptr(), std::ptr::null());
            gl::CompileShader(id);
        }

        let mut success: gl::types::GLint = 1;
        unsafe {
            gl::GetShaderiv(id, gl::COMPILE_STATUS, &mut success);
        }

        if success == 0 {
            let mut len: gl::types::GLint = 0;
            unsafe {
                gl::GetShaderiv(id, gl::INFO_LOG_LENGTH, &mut len);
            }

            let error = create_whitespace_cstring_with_len(len as usize);

            unsafe {
                gl::GetShaderInfoLog(
                    id,
                    len,
                    std::ptr::null_mut(),
                    error.as_ptr() as *mut gl::types::GLchar
                );
            }

            return Err(error.to_string_lossy().into_owned());
        }

        Ok(id)
    }

    fn create_whitespace_cstring_with_len(len: usize) -> CString {
        // allocate buffer of correct size
        let mut buffer: Vec<u8> = Vec::with_capacity(len + 1);
        // fill it with len spaces
        buffer.extend([b' '].iter().cycle().take(len));
        // convert buffer to CString
        unsafe { CString::from_vec_unchecked(buffer) }
    }
}

Moving render_gl module into another file

It is quite simple: create a new file named render_gl.rs in src, and move the contents of pub mod render_gl { } block into it. Then, add a seminocol at the end of pub mod render_gl:

pub mod render_gl;

When the Rust compiler encounters a module name with no block {}, it tries to load the contents from a file named with the same name and suffixed with .rs; in this case it would be render_gl.rs. If such a file does not exist, it will then try to load contents from render_gl/mod.rs file (this is useful to further subdivide submodules into smaller ones). If that also fails, then we get a compilation error.

Module references usually appear near the top of the file, bellow extern crate references. We can move pub mod render_gl; there.

Program

Linking program requires these OpenGL calls:

(example)

let program_id = unsafe { gl::CreateProgram() };

unsafe {
    gl::AttachShader(program_id, vert_shader_id);
    gl::AttachShader(program_id, frag_shader_id);
    gl::LinkProgram(program_id);
}

We can add a function to out Shader struct to retrieve shader id:

(render_gl.rs, at the end of impl Shader block)

impl Shader {
    ...

    pub fn id(&self) -> gl::types::GLuint {
        self.id
    }
}

This makes Shader struct useful even if we never create abstraction for linking a program:

(example)

let vert_shader = Shader::from_vert_source(..)?;
let frag_shader = Shader::from_frag_source(..)?;

let program_id = unsafe { gl::CreateProgram() };

unsafe {
    gl::AttachShader(program_id, vert_shader.id());
    gl::AttachShader(program_id, frag_shader.id());
    gl::LinkProgram(program_id);
}

One small thing: glDeleteShader won’t delete the shader if it is still attached to program. Therefore we detach the shader after linking it:

(example)

let vert_shader = Shader::from_vert_source(..)?;
let frag_shader = Shader::from_frag_source(..)?;

let program_id = unsafe { gl::CreateProgram() };

unsafe {
    gl::AttachShader(program_id, vert_shader.id());
    gl::AttachShader(program_id, frag_shader.id());
    gl::LinkProgram(program_id);
    gl::DetachShader(program_id, vert_shader.id());
    gl::DetachShader(program_id, frag_shader.id());
}

We will follow the same pattern as for Shader struct for our new Program struct. We will wrap the OpenGL program object id, add a static method that creates the program and links it to specified shaders, and a drop function that deletes the program object:

(at the top of render_gl.rs file, bellow use statements)

pub struct Program {
    id: gl::types::GLuint,
}

impl Program {
    pub fn from_shaders(shaders: &[Shader]) -> Result<Program, String> {
        let program_id = unsafe { gl::CreateProgram() };

        for shader in shaders {
            unsafe { gl::AttachShader(program_id, shader.id()); }
        }

        unsafe { gl::LinkProgram(program_id); }

        // continue with error handling here

        for shader in shaders {
            unsafe { gl::DetachShader(program_id, shader.id()); }
        }

        Ok(Program { id: program_id })
    }

    pub fn id(&self) -> gl::types::GLuint {
        self.id
    }
}

impl Drop for Program {
    fn drop(&mut self) {
        unsafe {
            gl::DeleteProgram(self.id);
        }
    }
}

from_shaders has a single shaders parameter, that takes a slice (&[]) of Shader structs. They are required just for the linking.

We also need the error handling, which is very similar to error handling of a shader program:

(render_gl.rs, bellow gl::LinkProgram)

let mut success: gl::types::GLint = 1;
unsafe {
    gl::GetProgramiv(program_id, gl::LINK_STATUS, &mut success);
}

if success == 0 {
    let mut len: gl::types::GLint = 0;
    unsafe {
        gl::GetProgramiv(program_id, gl::INFO_LOG_LENGTH, &mut len);
    }

    let error = create_whitespace_cstring_with_len(len as usize);

    unsafe {
        gl::GetProgramInfoLog(
            program_id,
            len,
            std::ptr::null_mut(),
            error.as_ptr() as *mut gl::types::GLchar
        );
    }

    return Err(error.to_string_lossy().into_owned());
}

Instead of GetShaderiv, we use GetProgramiv, instead of GetShaderInfoLog we use GetProgramInfoLog, and instead of COMPILE_STATUS we use LINK_STATUS.

Using our Shader and Program structs

Let’s add glUseProgram for our Program struct:

(render_gl.rs, impl Program)

pub fn set_used(&self) {
    unsafe {
        gl::UseProgram(self.id);
    }
}

I picked the name set_used, because use is unfortunately a reserved keyword.

Then, right before the 'main loop, we can compile our vertex and shader programs:

(main.rs, before loop)

use std::ffi::CString;

let vert_shader = render_gl::Shader::from_vert_source(
    &CString::new(include_str!("triangle.vert")).unwrap()
).unwrap();

let frag_shader = render_gl::Shader::from_frag_source(
    &CString::new(include_str!("triangle.frag")).unwrap()
).unwrap();

// continue here
  • We may include use block anywhere. I add use std::ffi::CString close to CString usage, because ideally we would move this code together with the use to another function or file.
  • When be borrow & CString, it is coerced into &CStr. This is the same mechanism that makes &String or &Vec<T> arguments work for &str or &[T] parameters.
  • include_str! macro embeds a UTF-8 file contents as a static &str value. Essentially the file gets compiled into our executable as string.
  • .unwrap for CString::new is needed because Rust’s UTF-8 strings may contain 0 in the middle and be valid. This should basically never happen, so we use unwrap to panic the program if it does.
  • .unwarp on Shader::from_*_source_... functions will panic and terminate our program with a debug message if shaders fail to compile. This is temporary way we handle the compilation.

Then, we link our shaders:

(main.rs, before loop)

let shader_program = render_gl::Program::from_shaders(
    &[vert_shader, frag_shader]
).unwrap();

// continue here

And finally, use them:

(main.rs, before loop)

shader_program.set_used();

Add triangle.vert to src:

(triangle.vert)

#version 330 core

layout (location = 0) in vec3 Position;

void main()
{
    gl_Position = vec4(Position, 1.0);
}

As well as triangle.frag:

(triangle.frag)

#version 330 core

out vec4 Color;

void main()
{
    Color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

However, we won’t see anything on screen yet, because we are not sending any draw commands to OpenGL yet.

We will remedy that the next time.

As always, the code for this part is available on github.