Presentation of the vulkano library

Yesterday I published for the first time the vulkano library on crates.io. I have been actively working on this library since the release of the Vulkan API in february, and I think it is now in a semi-usable state. This means that breaking changes are still happening, but the general design is more or less stable.

What is Vulkan?

Vulkan is a next generation graphics API by Khronos, which successes to OpenGL. It is meant to be the lowest-possible level for graphics programming that is still cross-platform, right above hardware.

The OpenGL API was initially designed to be easy to use, which means that it abstracts a lot over the hardware and that implementations have to do a lot of work to validate and translate the user's requests into actual hardware instructions.

This approach has several problems:

  • The validations performed by the OpenGL driver are expensive and most of the time unnecessary.
  • The OpenGL API tries to be easy to use by hiding a lot of implementation details. For example you don't have any guarantee about the amount of time it takes to execute a command.
  • Since the validation and conveniences are per-driver, each driver has its own internal magic and its own bugs. Your code can run correctly on one driver, but can end up running slowly on another driver, or reveal a bug in a third one.

Vulkan, instead, looks like this:

The "validation and conveniences" stage is gone. It's the job of the user code to handle the validation (if necessary) and to provide all the details to the driver. Any invalid usage of Vulkan immediately results in undefined behavior.

What is vulkano?

Taking the schema above, vulkano is here:

If we compare the vulkano API with the OpenGL API:

  • Vulkano is still lower-level than OpenGL. Vulkano doesn't hide anything from Vulkan, so you have to perform all the additional initialization that you wouldn't have to do in OpenGL.
  • Vulkano is safer than OpenGL, while probably still being faster because lots of checks are performed at compile-time (more details below).
  • Vulkano is more predictable. For example if you ask the GPU to write to a memory location, then try to write from the CPU in that same memory location, vulkano will block until the GPU is finished writing. OpenGL on the other side would perform some dark magic and avoid blocking by writing to a different location. As you can see, this also means that vulkano lets you shoot yourself more easily in the foot when it comes to performances.

One important design goal is that vulkano should never let you trigger any undefined behavior, unless you use one of its unsafe APIs. The goal of vulkano is not to let you easily draw a triangle, but to let you use the entirety of the Vulkan API in a safe way.

Note that as of this post, this is not yet the case. Some considerations are notably blocked by ambiguities in the specs.

Example of how vulkano performs validation

To give you an overview about how vulkano enforces safety, let's comment its triangle example.

  • At lines 193 to 210, we create a buffer that holds the shape of the triangle. To do so, we create a CpuAccessibleBuffer. Vulkano provides several high-level wrappers, like the CpuAccessibleBuffer, around raw buffers. These wrappers handle details like synchronization, cache flushing, or queue ownership transfers for you. If you're not satisfied with the available wrappers, you can also write your own by implementing an unsafe trait.

  • At lines 229 to 232, we import the pre-compiled shaders. To use vulkano you are encouraged to use the vulkano-shaders crate which compiles your GLSL code in SPIR-V and performs introspection of the shader's content. This is one of the key parts of the safety of vulkano.

  • At lines 245 to 282, we invoke a macro to create several structs. These structs are safe to use but contain a lot of unsafe code. The fact that you can invoke the macro in a specific way means that vulkano knows that the unsafe code is correct.

  • Lines 295 to 351 is where we create the pipeline. A lot of checks are performed here, and one of them is that the output of the vertex shader has to match the input of the fragment shader. To do so, the vulkano-shaders crate generates structs that implement the ShaderInterfaceDef trait: one struct for the output of the vertex shader and one for the input of the fragment shader. Then the data provided by the traits is compared. This comparison is expensive, but in the future this method will be specialized to return true for structs that are known to be always compatible. This design with a generic method that is then specialized is used several times in vulkano.

  • Line 302 is an illustration of the shader introspection. The method named main_entry_point is generated by vulkano-shaders and is named like this because the entry point of our shader is the function named main.

  • Lines 360 to 364 are an illustration of the render pass macro we called earlier. The AList struct is generated by the macro and contains one field for each attachment we are going to draw upon. For the moment you can pass any type of image, but in the future vulkano will add some trait bounds to restrict the possible values to images that can serve as attachments.

  • The type of data passed at line 401 depends on the format set at line 272. In this example we are using a generic format (because we don't know in advance what the hardware supports), which means that the check is performed at runtime. But for example if we had set the format to R32Uint, then we would only be able to pass a single unsigned integer for the clear values. Thanks to strong typing and trait implementations, several parts of vulkano can use either runtime checks or compile-time checks depending on which you need.

Getting started

The best to get started for now is to read the triangle example. The documentation should already be good enough, I think.

However for now I assume that you are already familiar with graphics programming. Vulkan is harder than OpenGL, so it's recommended to learn OpenGL first. This is also why I'm not writing any blog post or long article describing what the advantages of vulkano are. I expect that people who know OpenGL and who are interested in Vulkan already know what the advantages of a wrapper are.

That being said, contrary to what many people say I'm convinced that Vulkan is meant to replace OpenGL. Tutorials and examples should eventually be ported to Vulkan so that people learn Vulkan first and no longer have to fight with OpenGL.

Keep in mind that many parts of vulkano are still changing. If you want to use this library, I recommend using one specific commit, and upgrading from time to time. If you have a question about how to upgrade or how to use a specific feature, just ask me on #rust or #rust-gamedev.

49 Likes

In addition to this, I also compiled a list of "problems" I encountered with Rust. Comments about these are welcome.

9 Likes

Awesome writeup and great work as usual @tomaka!

On a more personal note, I'm going to stop creating new projects and concentrate on the existing ones (vulkano, glutin, winit, android-rs-glue, glium, cpal, rodio, immi) and on my game project.

My goal with glium was to both to make the OpenGL API easier to use, and also wrap around the dangerous features of OpenGL 4. Unfortunately I've become disillusioned by the incredible amount of dark magic and driver bugs of OpenGL 4.

I think that at this point if you need modern features from OpenGL 4 you might as well switch to Vulkan, whose drivers already look less buggy than OpenGL's. I'm leaning towards reorienting glium to be more focused on OpenGL 3.0-3.3 and OpenGL ES 2.0-3.2/WebGL.

11 Likes

Thanks for the writeup. :thumbsup:

Amazing post. I imagine that, like Glium, Vulkano is the best (and maybe only) safe and performant binding to Vulkan. Are there competing libs for other languages that do anything similar? This looks like it could be another compelling reason to bind to Rust from other languages. Like the regex crate, Rust can have the most reliable and performant graphics bindings.

@brson I don't think it's possible to bind to this library from another language, since most of its features use traits or other features specific to Rust.

Awesome work!

Need to try this some day…

I take it that even misuse of Vulkan still cannot (say) corrupt kernel memory or otherwise violate OS security?

In theory it's forbidden by the specs.
In practice some people reported that when allocating memory they were able to get the pixels content of apps that were recently closed.

However what happens very often is you misuse Vulkan is the kernel driver crashing. On Windows the video driver is isolated from the rest of the kernel (in order to avoid a blue screen of death), so they are able to restart it. On Linux I don't really know what happens.

Seems like a security hole to me.

Great news about the project. Thanks @tomaka for all the effort!!

And sad news for me for not being able to test the code.
I installed libvulkan-dev(1.0.8) and tried to run the examples, all failed(it compiled nicely).
Initially I thought there is a problem with the code, but nope...
Minimum nVidia supported GeForce 600 series - I have 540M...
Minimum Intel HD 4000 series - I have HD Graphics 3000...

Fail :slight_smile:

That is really awesome, I just came from D to give Rust another look and I have also experimented with Vulkan.

I just built your triangle example and it takes around 10 seconds to to build in Debug mode and 30 seconds in release mode.

I assume it takes that long because you translate GLSL to SPIR-V at compile time for every compile right?

Initially I wanted to do the same thing in D, but parsing in D at compile time is pretty expensive right now. So what I wanted to do was to create a more feature rich GLSL that also compiles to SPIR-V.

But I would do this externally and only when the shader actually changed. I also wanted to export type information as toml, json etc.

Vertex:
Input:
Vec3 vertex
Vec3 normal
Vec2 uv
Output:
Vec3 interpNormal

... and so on

I would capture everything in the shader like input, output, uniforms and then parse that type information at compile time and generate type safe implementations. Not sure how much of that would be possible in Rust, though I assume all of that should be doable.

You can modify your build script to only run on certain changes:
http://doc.crates.io/build-script.html

I'm having trouble passing multiple DescriptorSet refs to the command builder functions with this library. It certainly is that I'm new to rust, but I don't understand how to do this:

let set0 = pipeline_layout::set0::Set::new(&descriptor_pool, &pipeline_layout, &pipeline_layout::set0::Descriptors {
    my_binding_name: &my_value
});
let set1 = pipeline_layout::set1::Set::new(&descriptor_pool, &pipeline_layout, &pipeline_layout::set1::Descriptors {
    my_second_binding: &another_value
});

let command_buffer = PrimaryCommandBufferBuilder::new(&device, queue.family())
    .dispatch(&pipeline, (&set0, &set1), [4 * 4, 0, 0], &())
    .build();

The failure is here:

error: set0 does not live long enough
.dispatch(&pipeline, (&set0, &set1), [4 * 4, 0, 0], &())
                       ^~~~

The implicit-tuple converter I'm having trouble with is here:

vulkano/src/descriptor/descriptor_set/collection.rs

How can I pass a tuple of borrows in this situation?

Many thanks for the great library!

Passing (set0.clone(), set1.clone()) should work. Since the variables are Arcs, it's not a performance killer.

I don't know why there's a lifetime error, but it's probably an error somewhere in vulkano.

When I try the Arc::clone() function it produces new owned Arcs and doesn't fulfill the trait requirements of being a tuple of borrows:

error: the trait bound (std::sync::Arc<pipeline_layout::set0::Set>, std::sync::Arc<compute::pipeline_layout_gen::set1::Set>): vulkano::descriptor::descriptor_set::DescriptorSetsCollection is not satisfied [E0277]
        .dispatch(&pipeline, (set0.clone(), set1.clone()), [4 * 4, 0, 0], &())
         ^~~~~~~~

The code does pick up a single owned set and implicitly convert it to a DescriptorSetCollection so I do know that works. It seems like I should be able to modify DescriptorSetsCollection to take a borrowed tuple instead of a tuple of borrows, but I'm still learning rust (2 weeks?) and I'm sure there is a missing foot down that path.

It should be fixed in https://github.com/tomaka/vulkano/pull/136

The problem was caused by a thing I could maybe add to the list of "problems of Rust": when you have complex function signatures with big where clauses, it's easy to require a trait that is in fact not needed, like 'static in this case. Although I'm not sure how you would fix that.

1 Like

Thanks that fixed my build error!